From db146046c4eaea8e980f3e7ab5f2254f93f4feae Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 23 Jan 2025 13:54:23 +0100 Subject: [PATCH] Convert `LanguageDropdown` to a functional component (#33701) --- .../compose/components/compose_form.jsx | 2 +- .../compose/components/language_dropdown.jsx | 175 +++++++++--------- .../language_detection.js} | 60 +----- 3 files changed, 99 insertions(+), 138 deletions(-) rename app/javascript/mastodon/features/compose/{containers/language_dropdown_container.js => util/language_detection.js} (51%) diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index ca67c3f63e..468125afb0 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 20fba29ecb..c80aa27e46 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 { +const getFrequentlyUsedLanguages = createSelector([ + state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()), +], languageCounters => ( + languageCounters.keySeq() + .sort((a, b) => languageCounters.get(a) - languageCounters.get(b)) + .reverse() + .toArray() +)); - static propTypes = { - value: PropTypes.string, - frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string), - guess: PropTypes.string, - intl: PropTypes.object.isRequired, - onChange: PropTypes.func, - }; +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); - state = { - open: false, - placement: 'bottom', - }; + const intl = useIntl(); - handleToggle = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); + 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(''); } + }, [text, setGuess]); - this.setState({ open: !this.state.open }); - }; + return ( +
+ - handleClose = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - - 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 ( -
- - - - {({ props, placement }) => ( -
-
- -
+ + {({ props, placement }) => ( +
+
+
- )} - -
- ); - } - -} - -export default injectIntl(LanguageDropdown); +
+ )} +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js b/app/javascript/mastodon/features/compose/util/language_detection.js similarity index 51% rename from app/javascript/mastodon/features/compose/containers/language_dropdown_container.js rename to app/javascript/mastodon/features/compose/util/language_detection.js index 64d6516a18..b3be07d516 100644 --- a/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/util/language_detection.js @@ -1,23 +1,7 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { Map as ImmutableMap } from 'immutable'; -import { connect } from 'react-redux'; - import lande from 'lande'; import { debounce } from 'lodash'; -import { changeComposeLanguage } from 'mastodon/actions/compose'; - -import LanguageDropdown from '../components/language_dropdown'; -import { urlRegex } from '../util/url_regex'; - -const getFrequentlyUsedLanguages = createSelector([ - state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()), -], languageCounters => ( - languageCounters.keySeq() - .sort((a, b) => languageCounters.get(a) - languageCounters.get(b)) - .reverse() - .toArray() -)); +import { urlRegex } from './url_regex'; const ISO_639_MAP = { afr: 'af', // Afrikaans @@ -72,47 +56,21 @@ const ISO_639_MAP = { vie: 'vi', // Vietnamese }; -const debouncedLande = debounce((text) => { +const guessLanguage = (text) => { text = text .replace(urlRegex, '') .replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, ''); - if (text.length <= 20) - return undefined; - - return lande(text); -}, 500, { trailing: true }); - -const detectedLanguage = createSelector([ - state => state.getIn(['compose', 'text']), -], text => { if (text.length > 20) { - const guesses = debouncedLande(text); - if (!guesses) - return ''; - - const [lang, confidence] = guesses[0]; - - if (confidence > 0.8) { + const [lang, confidence] = lande(text)[0]; + + if (confidence > 0.8) return ISO_639_MAP[lang]; - } } return ''; -}); +}; -const mapStateToProps = state => ({ - frequentlyUsedLanguages: getFrequentlyUsedLanguages(state), - value: state.getIn(['compose', 'language']), - guess: detectedLanguage(state), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeComposeLanguage(value)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown); +export const debouncedGuess = debounce((text, setGuess) => { + setGuess(guessLanguage(text)); +}, 500, { leading: true, trailing: true }); \ No newline at end of file