diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index ca67c3f63e264c..468125afb08d2e 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -14,7 +14,6 @@ import AutosuggestInput from '../../../components/autosuggest_input'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import { Button } from '../../../components/button'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; -import LanguageDropdown from '../containers/language_dropdown_container'; import PollButtonContainer from '../containers/poll_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; @@ -24,6 +23,7 @@ import { countableText } from '../util/counter'; import { CharacterCounter } from './character_counter'; import { EditIndicator } from './edit_indicator'; +import { LanguageDropdown } from './language_dropdown'; import { NavigationBar } from './navigation_bar'; import { PollForm } from "./poll_form"; import { ReplyIndicator } from './reply_indicator'; diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx index 20fba29ecba884..c80aa27e46a6ef 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx @@ -1,10 +1,13 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useCallback, useRef, useState, useEffect, PureComponent } from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; +import { createSelector } from '@reduxjs/toolkit'; +import { Map as ImmutableMap } from 'immutable'; + import { supportsPassiveEvents } from 'detect-passive-events'; import fuzzysort from 'fuzzysort'; import Overlay from 'react-overlays/Overlay'; @@ -12,8 +15,12 @@ import Overlay from 'react-overlays/Overlay'; import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import TranslateIcon from '@/material-icons/400-24px/translate.svg?react'; +import { changeComposeLanguage } from 'mastodon/actions/compose'; import { Icon } from 'mastodon/components/icon'; import { languages as preloadedLanguages } from 'mastodon/initial_state'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +import { debouncedGuess } from '../util/language_detection'; const messages = defineMessages({ changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, @@ -237,94 +244,90 @@ class LanguageDropdownMenu extends PureComponent { } -class LanguageDropdown extends PureComponent { - - static propTypes = { - value: PropTypes.string, - frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string), - guess: PropTypes.string, - intl: PropTypes.object.isRequired, - onChange: PropTypes.func, - }; - - state = { - open: false, - placement: 'bottom', - }; - - handleToggle = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - - this.setState({ open: !this.state.open }); - }; - - handleClose = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); +const getFrequentlyUsedLanguages = createSelector([ + state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()), +], languageCounters => ( + languageCounters.keySeq() + .sort((a, b) => languageCounters.get(a) - languageCounters.get(b)) + .reverse() + .toArray() +)); + +export const LanguageDropdown = () => { + const [open, setOpen] = useState(false); + const [placement, setPlacement] = useState('bottom'); + const [guess, setGuess] = useState(''); + const activeElementRef = useRef(null); + const targetRef = useRef(null); + + const intl = useIntl(); + + const dispatch = useAppDispatch(); + const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages); + const value = useAppSelector((state) => state.compose.get('language')); + const text = useAppSelector((state) => state.compose.get('text')); + + const current = preloadedLanguages.find(lang => lang[0] === value) ?? []; + + const handleToggle = useCallback(() => { + if (open && activeElementRef.current) + activeElementRef.current.focus({ preventScroll: true }); + + setOpen(!open); + }, [open, setOpen]); + + const handleClose = useCallback(() => { + if (open && activeElementRef.current) + activeElementRef.current.focus({ preventScroll: true }); + + setOpen(false); + }, [open, setOpen]); + + const handleChange = useCallback((value) => { + dispatch(changeComposeLanguage(value)); + }, [dispatch]); + + const handleOverlayEnter = useCallback(({ placement }) => { + setPlacement(placement); + }, [setPlacement]); + + useEffect(() => { + if (text.length > 20) { + debouncedGuess(text, setGuess); + } else { + setGuess(''); } - - this.setState({ open: false }); - }; - - handleChange = value => { - const { onChange } = this.props; - onChange(value); - }; - - setTargetRef = c => { - this.target = c; - }; - - findTarget = () => { - return this.target; - }; - - handleOverlayEnter = (state) => { - this.setState({ placement: state.placement }); - }; - - render () { - const { value, guess, intl, frequentlyUsedLanguages } = this.props; - const { open, placement } = this.state; - const current = preloadedLanguages.find(lang => lang[0] === value) ?? []; - - return ( -