diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 8dcab845ee085c..25615b720d45ad 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -36,4 +36,4 @@ jobs: - name: Run haml-lint run: | echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" - bundle exec haml-lint + bundle exec haml-lint --reporter github diff --git a/Gemfile.lock b/Gemfile.lock index 734ed6ec8a19d4..076cf915d05bfd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -333,7 +333,7 @@ GEM http-form_data (2.3.0) http_accept_language (2.1.1) httpclient (2.8.3) - httplog (1.6.2) + httplog (1.6.3) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.1) @@ -744,7 +744,7 @@ GEM terrapin (1.0.1) climate_control test-prof (1.3.1) - thor (1.3.0) + thor (1.3.1) tilt (2.3.0) timeout (0.4.1) tpm-key_attestation (0.12.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 00f5c7c11e0223..490a45e018bf82 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -180,7 +180,7 @@ def check_self_destruct! use_pack 'error' render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] end - format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code } + format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 } end end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index d1c75726404cf5..655133290acb2a 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -93,18 +93,6 @@ Rails.delegate(document, batchCheckboxClassName, 'change', () => { } }); -Rails.delegate(document, '.media-spoiler-show-button', 'click', () => { - [].forEach.call(document.querySelectorAll('button.media-spoiler'), (element) => { - element.click(); - }); -}); - -Rails.delegate(document, '.media-spoiler-hide-button', 'click', () => { - [].forEach.call(document.querySelectorAll('.spoiler-button.spoiler-button--visible button'), (element) => { - element.click(); - }); -}); - Rails.delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => { target.form.submit(); }); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx index 0ced5a04ad0d14..9774d4260eb462 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx @@ -5,7 +5,7 @@ import Overlay from 'react-overlays/Overlay'; import { IconButton } from 'flavours/glitch/components/icon_button'; -import DropdownMenu from './dropdown_menu'; +import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; export const DropdownIconButton = ({ value, disabled, icon, onChange, iconComponent, title, options }) => { const containerRef = useRef(null); @@ -53,7 +53,7 @@ export const DropdownIconButton = ({ value, disabled, icon, onChange, iconCompon {({ props, placement }) => (
- { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - e.stopPropagation(); - } - }; - - handleKeyDown = e => { - const { items } = this.props; - const value = e.currentTarget.getAttribute('data-index'); - const index = items.findIndex(item => { - return (item.value === value); - }); - let element = null; - - switch(e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } - }; - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - render () { - const { style, items, value } = this.props; - - return ( -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
-
- ))} -
- ); - } - -} - -export default DropdownMenu; diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx index db1ce9cecec561..8edf75203facec 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx @@ -141,6 +141,7 @@ class LanguageDropdownMenu extends PureComponent { case 'Escape': onClose(); break; + case ' ': case 'Enter': this.handleClick(e); break; diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx index 8a49f71511e008..c99f18545ba605 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx @@ -5,16 +5,16 @@ import { injectIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; -import { supportsPassiveEvents } from 'detect-passive-events'; import Overlay from 'react-overlays/Overlay'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; +import { PrivacyDropdownMenu } from './privacy_dropdown_menu'; + const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, @@ -28,126 +28,6 @@ const messages = defineMessages({ unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; - -class PrivacyDropdownMenu extends PureComponent { - - static propTypes = { - style: PropTypes.object, - items: PropTypes.array.isRequired, - value: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - e.stopPropagation(); - } - }; - - handleKeyDown = e => { - const { items } = this.props; - const value = e.currentTarget.getAttribute('data-index'); - const index = items.findIndex(item => { - return (item.value === value); - }); - let element = null; - - switch(e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } - }; - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - render () { - const { style, items, value } = this.props; - - return ( -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
- - {item.extra && ( -
- -
- )} -
- ))} -
- ); - } - -} - class PrivacyDropdown extends PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx new file mode 100644 index 00000000000000..03a0b76d23914d --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx @@ -0,0 +1,128 @@ +import PropTypes from 'prop-types'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import classNames from 'classnames'; + +import { supportsPassiveEvents } from 'detect-passive-events'; + +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; + +export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => { + const nodeRef = useRef(null); + const focusedItemRef = useRef(null); + const [currentValue, setCurrentValue] = useState(value); + + const handleDocumentClick = useCallback((e) => { + if (nodeRef.current && !nodeRef.current.contains(e.target)) { + onClose(); + e.stopPropagation(); + } + }, [nodeRef, onClose]); + + const handleClick = useCallback((e) => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + onClose(); + onChange(value); + }, [onClose, onChange]); + + const handleKeyDown = useCallback((e) => { + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => (item.value === value)); + + let element = null; + + switch (e.key) { + case 'Escape': + onClose(); + break; + case ' ': + case 'Enter': + handleClick(e); + break; + case 'ArrowDown': + element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; + break; + case 'ArrowUp': + element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild; + } else { + element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild; + } + break; + case 'Home': + element = nodeRef.current.firstChild; + break; + case 'End': + element = nodeRef.current.lastChild; + break; + } + + if (element) { + element.focus(); + setCurrentValue(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }, [nodeRef, items, onClose, handleClick, setCurrentValue]); + + useEffect(() => { + document.addEventListener('click', handleDocumentClick, { capture: true }); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + focusedItemRef.current?.focus({ preventScroll: true }); + + return () => { + document.removeEventListener('click', handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', handleDocumentClick, listenerOptions); + }; + }, [handleDocumentClick]); + + return ( +
    + {items.map(item => ( +
  • +
    + +
    + +
    + {item.text} + {item.meta} +
    + + {item.extra && ( +
    + +
    + )} +
  • + ))} +
+ ); +}; + +PrivacyDropdownMenu.propTypes = { + style: PropTypes.object, + items: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx b/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx index 9b8947c447b505..59d569a999dd09 100644 --- a/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx @@ -8,8 +8,8 @@ import { IconButton } from 'flavours/glitch/components/icon_button'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; const messages = defineMessages({ - enable_threaded_mode: { id: 'compose.enable_threaded_mode', defaultMessage: 'Enable threaded more' }, - disable_threaded_mode: { id: 'compose.disable_threaded_mode', defaultMessage: 'Disable threaded more' }, + enable_threaded_mode: { id: 'compose.enable_threaded_mode', defaultMessage: 'Enable threaded mode' }, + disable_threaded_mode: { id: 'compose.disable_threaded_mode', defaultMessage: 'Disable threaded mode' }, }); export const ThreadModeButton = () => { diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx index caa6784cb9a420..ed2cbb04f27223 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx @@ -7,10 +7,14 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import PhotoLibraryIcon from '@/material-icons/400-20px/photo_library.svg?react'; -import { IconButton } from 'flavours/glitch/components/icon_button'; +import BrushIcon from '@/material-icons/400-24px/brush.svg?react'; +import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react'; + +import { DropdownIconButton } from './dropdown_icon_button'; const messages = defineMessages({ upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' }, + doodle: { id: 'compose.attach.doodle', defaultMessage: 'Draw something' }, }); const makeMapStateToProps = () => { @@ -21,16 +25,12 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const iconStyle = { - height: null, - lineHeight: '27px', -}; - class UploadButton extends ImmutablePureComponent { static propTypes = { disabled: PropTypes.bool, onSelectFile: PropTypes.func.isRequired, + onDoodleOpen: PropTypes.func.isRequired, style: PropTypes.object, resetFileKey: PropTypes.number, acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, @@ -43,8 +43,12 @@ class UploadButton extends ImmutablePureComponent { } }; - handleClick = () => { - this.fileElement.click(); + handleSelect = (value) => { + if (value === 'upload') { + this.fileElement.click(); + } else { + this.props.onDoodleOpen(); + } }; setRef = (c) => { @@ -56,9 +60,32 @@ class UploadButton extends ImmutablePureComponent { const message = intl.formatMessage(messages.upload); + const options = [ + { + icon: 'cloud-upload', + iconComponent: UploadFileIcon, + value: 'upload', + text: intl.formatMessage(messages.upload), + }, + { + icon: 'paint-brush', + iconComponent: BrushIcon, + value: 'doodle', + text: intl.formatMessage(messages.doodle), + }, + ]; + return (
- +