Convert LanguageDropdown to a functional component (#33701)

This commit is contained in:
Claire 2025-01-23 13:54:23 +01:00 committed by GitHub
parent 3178acc5cb
commit db146046c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 99 additions and 138 deletions

View File

@ -14,7 +14,6 @@ import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { Button } from '../../../components/button'; import { Button } from '../../../components/button';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import LanguageDropdown from '../containers/language_dropdown_container';
import PollButtonContainer from '../containers/poll_button_container'; import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import SpoilerButtonContainer from '../containers/spoiler_button_container'; import SpoilerButtonContainer from '../containers/spoiler_button_container';
@ -24,6 +23,7 @@ import { countableText } from '../util/counter';
import { CharacterCounter } from './character_counter'; import { CharacterCounter } from './character_counter';
import { EditIndicator } from './edit_indicator'; import { EditIndicator } from './edit_indicator';
import { LanguageDropdown } from './language_dropdown';
import { NavigationBar } from './navigation_bar'; import { NavigationBar } from './navigation_bar';
import { PollForm } from "./poll_form"; import { PollForm } from "./poll_form";
import { ReplyIndicator } from './reply_indicator'; import { ReplyIndicator } from './reply_indicator';

View File

@ -1,10 +1,13 @@
import PropTypes from 'prop-types'; 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 classNames from 'classnames';
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import fuzzysort from 'fuzzysort'; import fuzzysort from 'fuzzysort';
import Overlay from 'react-overlays/Overlay'; 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 CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import TranslateIcon from '@/material-icons/400-24px/translate.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 { Icon } from 'mastodon/components/icon';
import { languages as preloadedLanguages } from 'mastodon/initial_state'; import { languages as preloadedLanguages } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { debouncedGuess } from '../util/language_detection';
const messages = defineMessages({ const messages = defineMessages({
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
@ -237,75 +244,75 @@ 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 = { export const LanguageDropdown = () => {
value: PropTypes.string, const [open, setOpen] = useState(false);
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string), const [placement, setPlacement] = useState('bottom');
guess: PropTypes.string, const [guess, setGuess] = useState('');
intl: PropTypes.object.isRequired, const activeElementRef = useRef(null);
onChange: PropTypes.func, const targetRef = useRef(null);
};
state = { const intl = useIntl();
open: false,
placement: 'bottom',
};
handleToggle = () => { const dispatch = useAppDispatch();
if (this.state.open && this.activeElement) { const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages);
this.activeElement.focus({ preventScroll: true }); const value = useAppSelector((state) => state.compose.get('language'));
} const text = useAppSelector((state) => state.compose.get('text'));
this.setState({ open: !this.state.open });
};
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) ?? []; 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]);
return ( return (
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}> <div ref={targetRef}>
<button <button
type='button' type='button'
title={intl.formatMessage(messages.changeLanguage)} title={intl.formatMessage(messages.changeLanguage)}
aria-expanded={open} aria-expanded={open}
onClick={this.handleToggle} onClick={handleToggle}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
className={classNames('dropdown-button', { active: open, warning: guess !== '' && guess !== value })} className={classNames('dropdown-button', { active: open, warning: guess !== '' && guess !== value })}
> >
<Icon icon={TranslateIcon} /> <Icon icon={TranslateIcon} />
<span className='dropdown-button__label'>{current[2] ?? value}</span> <span className='dropdown-button__label'>{current[2] ?? value}</span>
</button> </button>
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> <Overlay show={open} offset={[5, 5]} placement={placement} flip target={targetRef} popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}>
{({ props, placement }) => ( {({ props, placement }) => (
<div {...props}> <div {...props}>
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} > <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
@ -313,8 +320,8 @@ class LanguageDropdown extends PureComponent {
value={value} value={value}
guess={guess} guess={guess}
frequentlyUsedLanguages={frequentlyUsedLanguages} frequentlyUsedLanguages={frequentlyUsedLanguages}
onClose={this.handleClose} onClose={handleClose}
onChange={this.handleChange} onChange={handleChange}
intl={intl} intl={intl}
/> />
</div> </div>
@ -323,8 +330,4 @@ class LanguageDropdown extends PureComponent {
</Overlay> </Overlay>
</div> </div>
); );
} };
}
export default injectIntl(LanguageDropdown);

View File

@ -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 lande from 'lande';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { changeComposeLanguage } from 'mastodon/actions/compose'; import { urlRegex } from './url_regex';
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()
));
const ISO_639_MAP = { const ISO_639_MAP = {
afr: 'af', // Afrikaans afr: 'af', // Afrikaans
@ -72,47 +56,21 @@ const ISO_639_MAP = {
vie: 'vi', // Vietnamese vie: 'vi', // Vietnamese
}; };
const debouncedLande = debounce((text) => { const guessLanguage = (text) => {
text = text text = text
.replace(urlRegex, '') .replace(urlRegex, '')
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, ''); .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) { if (text.length > 20) {
const guesses = debouncedLande(text); const [lang, confidence] = lande(text)[0];
if (!guesses)
return '';
const [lang, confidence] = guesses[0]; if (confidence > 0.8)
if (confidence > 0.8) {
return ISO_639_MAP[lang]; return ISO_639_MAP[lang];
} }
}
return ''; return '';
}); };
const mapStateToProps = state => ({ export const debouncedGuess = debounce((text, setGuess) => {
frequentlyUsedLanguages: getFrequentlyUsedLanguages(state), setGuess(guessLanguage(text));
value: state.getIn(['compose', 'language']), }, 500, { leading: true, trailing: true });
guess: detectedLanguage(state),
});
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeComposeLanguage(value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);