mirror of
https://github.com/mastodon/mastodon.git
synced 2025-01-24 02:51:43 +01:00
Convert LanguageDropdown
to a functional component (#33701)
This commit is contained in:
parent
3178acc5cb
commit
db146046c4
@ -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';
|
||||||
|
@ -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);
|
|
||||||
|
@ -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);
|
|
Loading…
x
Reference in New Issue
Block a user