diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json index b32e4026d2..ca9156fdaa 100644 --- a/.devcontainer/codespaces/devcontainer.json +++ b/.devcontainer/codespaces/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/sshd:1": {}, + "ghcr.io/devcontainers/features/sshd:1": {} }, "runServices": ["app", "db", "redis"], @@ -15,16 +15,16 @@ "portsAttributes": { "3000": { "label": "web", - "onAutoForward": "notify", + "onAutoForward": "notify" }, "4000": { "label": "stream", - "onAutoForward": "silent", - }, + "onAutoForward": "silent" + } }, "otherPortsAttributes": { - "onAutoForward": "silent", + "onAutoForward": "silent" }, "remoteEnv": { @@ -33,7 +33,7 @@ "STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev", "DISABLE_FORGERY_REQUEST_PROTECTION": "true", "ES_ENABLED": "", - "LIBRE_TRANSLATE_ENDPOINT": "", + "LIBRE_TRANSLATE_ENDPOINT": "" }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", @@ -43,7 +43,7 @@ "customizations": { "vscode": { "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"], - }, - }, + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] + } + } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ed71235b3b..fa8d6542c1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "features": { - "ghcr.io/devcontainers/features/sshd:1": {}, + "ghcr.io/devcontainers/features/sshd:1": {} }, "forwardPorts": [3000, 4000], @@ -14,17 +14,17 @@ "3000": { "label": "web", "onAutoForward": "notify", - "requireLocalPort": true, + "requireLocalPort": true }, "4000": { "label": "stream", "onAutoForward": "silent", - "requireLocalPort": true, - }, + "requireLocalPort": true + } }, "otherPortsAttributes": { - "onAutoForward": "silent", + "onAutoForward": "silent" }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", @@ -34,7 +34,7 @@ "customizations": { "vscode": { "settings": {}, - "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"], - }, - }, + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] + } + } } diff --git a/.eslintrc.js b/.eslintrc.js index ae13b0711c..ecef527ed0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -355,7 +355,6 @@ module.exports = defineConfig({ 'plugin:import/typescript', 'plugin:promise/recommended', 'plugin:jsdoc/recommended-typescript', - 'plugin:prettier/recommended', ], parserOptions: { @@ -364,6 +363,9 @@ module.exports = defineConfig({ }, rules: { + // Disable formatting rules that have been enabled in the base config + 'indent': 'off', + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml new file mode 100644 index 0000000000..2d483b5022 --- /dev/null +++ b/.github/workflows/format-check.yml @@ -0,0 +1,18 @@ +name: Check formatting +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Set up Javascript environment + uses: ./.github/actions/setup-javascript + + - name: Check formatting with Prettier + run: yarn format:check diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index 7229bec582..e5f4874877 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -43,4 +43,4 @@ jobs: - run: echo "::add-matcher::.github/stylelint-matcher.json" - name: Stylelint - run: yarn lint:sass + run: yarn lint:css diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 8dcab845ee..25615b720d 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/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml deleted file mode 100644 index 7796bf92c4..0000000000 --- a/.github/workflows/lint-json.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: JSON Linting -on: - push: - branches-ignore: - - 'dependabot/**' - - 'renovate/**' - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '.prettier*' - - '**/*.json' - - '.github/workflows/lint-json.yml' - - '!app/javascript/mastodon/locales/*.json' - - pull_request: - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '.prettier*' - - '**/*.json' - - '.github/workflows/lint-json.yml' - - '!app/javascript/mastodon/locales/*.json' - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Prettier - run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml deleted file mode 100644 index 51c59937a3..0000000000 --- a/.github/workflows/lint-md.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Markdown Linting -on: - push: - branches-ignore: - - 'dependabot/**' - - 'renovate/**' - paths: - - '.github/workflows/lint-md.yml' - - '.nvmrc' - - '.prettier*' - - '**/*.md' - - '!AUTHORS.md' - - 'package.json' - - 'yarn.lock' - - pull_request: - paths: - - '.github/workflows/lint-md.yml' - - '.nvmrc' - - '.prettier*' - - '**/*.md' - - '!AUTHORS.md' - - 'package.json' - - 'yarn.lock' - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Prettier - run: yarn lint:md diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml deleted file mode 100644 index 908bdef5cc..0000000000 --- a/.github/workflows/lint-yml.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: YML Linting -on: - push: - branches-ignore: - - 'dependabot/**' - - 'renovate/**' - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '.prettier*' - - '**/*.yaml' - - '**/*.yml' - - '.github/workflows/lint-yml.yml' - - '!config/locales/*.yml' - - pull_request: - paths: - - 'package.json' - - 'yarn.lock' - - '.nvmrc' - - '.prettier*' - - '**/*.yaml' - - '**/*.yml' - - '.github/workflows/lint-yml.yml' - - '!config/locales/*.yml' - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Javascript environment - uses: ./.github/actions/setup-javascript - - - name: Prettier - run: yarn lint:yml diff --git a/.prettierignore b/.prettierignore index 66ca9ee5b0..ad88ad3f97 100644 --- a/.prettierignore +++ b/.prettierignore @@ -54,6 +54,13 @@ # Ignore Docker option files docker-compose.override.yml +# Ignore public +/public/assets +/public/emoji +/public/packs +/public/packs-test +/public/system + # Ignore emoji map file /app/javascript/mastodon/features/emoji/emoji_map.json @@ -74,6 +81,7 @@ app/javascript/styles/mastodon/reset.scss # Ignore the generated AUTHORS.md AUTHORS.md +# Process a few selected JS files !lint-staged.config.js # Ignore glitch-soc emoji map file diff --git a/Gemfile.lock b/Gemfile.lock index 734ed6ec8a..076cf915d0 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 00f5c7c11e..490a45e018 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -180,7 +180,7 @@ class ApplicationController < ActionController::Base 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 d1c7572640..8e335131a9 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -45,7 +45,7 @@ const hideSelectAll = () => { Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => { const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); - [].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => { + document.querySelectorAll(batchCheckboxClassName).forEach((content) => { content.checked = target.checked; }); @@ -80,8 +80,11 @@ Rails.delegate(document, batchCheckboxClassName, 'change', () => { const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); if (checkAllElement) { - checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); - checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + const allCheckboxes = Array.from( + document.querySelectorAll(batchCheckboxClassName) + ); + checkAllElement.checked = allCheckboxes.every((content) => content.checked); + checkAllElement.indeterminate = !checkAllElement.checked && allCheckboxes.some((content) => content.checked); if (selectAllMatchingElement) { if (checkAllElement.checked) { @@ -93,18 +96,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(); }); @@ -144,11 +135,11 @@ Rails.delegate(document, '#form_admin_settings_enable_bootstrap_timeline_account const onChangeRegistrationMode = (target) => { const enabled = target.value === 'approved'; - [].forEach.call(document.querySelectorAll('.form_admin_settings_registrations_mode .warning-hint'), (warning_hint) => { + document.querySelectorAll('.form_admin_settings_registrations_mode .warning-hint').forEach((warning_hint) => { warning_hint.style.display = target.value === 'open' ? 'inline' : 'none'; }); - [].forEach.call(document.querySelectorAll('#form_admin_settings_require_invite_text'), (input) => { + document.querySelectorAll('#form_admin_settings_require_invite_text').forEach((input) => { input.disabled = !enabled; if (enabled) { let element = input; @@ -194,8 +185,9 @@ ready(() => { const checkAllElement = document.querySelector('#batch_checkbox_all'); if (checkAllElement) { - checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); - checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + const allCheckboxes = Array.from(document.querySelectorAll(batchCheckboxClassName)); + checkAllElement.checked = allCheckboxes.every( (content) => content.checked); + checkAllElement.indeterminate = !checkAllElement.checked && allCheckboxes.some((content) => content.checked); } document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => { @@ -208,7 +200,7 @@ ready(() => { } }); - [].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => { + document.querySelectorAll('input[type="datetime-local"]').forEach(element => { if (element.value) { element.value = convertUTCDateTimeToLocal(element.value); } @@ -218,7 +210,7 @@ ready(() => { }); Rails.delegate(document, 'form', 'submit', ({ target }) => { - [].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => { + target.querySelectorAll('input[type="datetime-local"]').forEach(element => { if (element.value && element.validity.valid) { element.value = convertLocalDatetimeToUTC(element.value); } diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index 52aac5ebe4..a7b20bc249 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -80,7 +80,7 @@ export default class MediaContainer extends PureComponent { return ( <> - {[].map.call(components, (component, i) => { + {Array.from(components).map((component, i) => { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props')); 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 0ced5a04ad..9774d4260e 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 db1ce9cece..8edf75203f 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 8a49f71511..c99f18545b 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 0000000000..03a0b76d23 --- /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 9b8947c447..59d569a999 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 caa6784cb9..ed2cbb04f2 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 (
- +