Add language dropdown to compose in web UI (#18420)
This commit is contained in:
		
							parent
							
								
									c3fac61f56
								
							
						
					
					
						commit
						0cdb077570
					
				
					 15 changed files with 513 additions and 16 deletions
				
			
		| 
						 | 
					@ -45,12 +45,12 @@ export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
 | 
				
			||||||
export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
 | 
					export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
 | 
				
			||||||
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
 | 
					export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
 | 
					export const COMPOSE_SENSITIVITY_CHANGE  = 'COMPOSE_SENSITIVITY_CHANGE';
 | 
				
			||||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
 | 
					export const COMPOSE_SPOILERNESS_CHANGE  = 'COMPOSE_SPOILERNESS_CHANGE';
 | 
				
			||||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
 | 
					export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
 | 
				
			||||||
export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 | 
					export const COMPOSE_VISIBILITY_CHANGE   = 'COMPOSE_VISIBILITY_CHANGE';
 | 
				
			||||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
 | 
					export const COMPOSE_COMPOSING_CHANGE    = 'COMPOSE_COMPOSING_CHANGE';
 | 
				
			||||||
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
 | 
					export const COMPOSE_LANGUAGE_CHANGE     = 'COMPOSE_LANGUAGE_CHANGE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
 | 
					export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -169,6 +169,7 @@ export function submitCompose(routerHistory) {
 | 
				
			||||||
        spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
 | 
					        spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
 | 
				
			||||||
        visibility: getState().getIn(['compose', 'privacy']),
 | 
					        visibility: getState().getIn(['compose', 'privacy']),
 | 
				
			||||||
        poll: getState().getIn(['compose', 'poll'], null),
 | 
					        poll: getState().getIn(['compose', 'poll'], null),
 | 
				
			||||||
 | 
					        language: getState().getIn(['compose', 'language']),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      headers: {
 | 
					      headers: {
 | 
				
			||||||
        'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
 | 
					        'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
 | 
				
			||||||
| 
						 | 
					@ -635,6 +636,11 @@ export function changeComposeSensitivity() {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const changeComposeLanguage = language => ({
 | 
				
			||||||
 | 
					  type: COMPOSE_LANGUAGE_CHANGE,
 | 
				
			||||||
 | 
					  language,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function changeComposeSpoilerness() {
 | 
					export function changeComposeSpoilerness() {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: COMPOSE_SPOILERNESS_CHANGE,
 | 
					    type: COMPOSE_SPOILERNESS_CHANGE,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								app/javascript/mastodon/actions/languages.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/javascript/mastodon/actions/languages.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { saveSettings } from './settings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const LANGUAGE_USE = 'LANGUAGE_USE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useLanguage = language => dispatch => {
 | 
				
			||||||
 | 
					  dispatch({
 | 
				
			||||||
 | 
					    type: LANGUAGE_USE,
 | 
				
			||||||
 | 
					    language,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dispatch(saveSettings());
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
 | 
				
			||||||
import PollFormContainer from '../containers/poll_form_container';
 | 
					import PollFormContainer from '../containers/poll_form_container';
 | 
				
			||||||
import UploadFormContainer from '../containers/upload_form_container';
 | 
					import UploadFormContainer from '../containers/upload_form_container';
 | 
				
			||||||
import WarningContainer from '../containers/warning_container';
 | 
					import WarningContainer from '../containers/warning_container';
 | 
				
			||||||
 | 
					import LanguageDropdown from '../containers/language_dropdown_container';
 | 
				
			||||||
import { isMobile } from '../../../is_mobile';
 | 
					import { isMobile } from '../../../is_mobile';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
import { length } from 'stringz';
 | 
					import { length } from 'stringz';
 | 
				
			||||||
| 
						 | 
					@ -204,6 +205,7 @@ class ComposeForm extends ImmutablePureComponent {
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl, onPaste, showSearch } = this.props;
 | 
					    const { intl, onPaste, showSearch } = this.props;
 | 
				
			||||||
    const disabled = this.props.isSubmitting;
 | 
					    const disabled = this.props.isSubmitting;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let publishText = '';
 | 
					    let publishText = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.props.isEditing) {
 | 
					    if (this.props.isEditing) {
 | 
				
			||||||
| 
						 | 
					@ -254,6 +256,7 @@ class ComposeForm extends ImmutablePureComponent {
 | 
				
			||||||
          autoFocus={!showSearch && !isMobile(window.innerWidth)}
 | 
					          autoFocus={!showSearch && !isMobile(window.innerWidth)}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
 | 
					          <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div className='compose-form__modifiers'>
 | 
					          <div className='compose-form__modifiers'>
 | 
				
			||||||
            <UploadFormContainer />
 | 
					            <UploadFormContainer />
 | 
				
			||||||
            <PollFormContainer />
 | 
					            <PollFormContainer />
 | 
				
			||||||
| 
						 | 
					@ -266,12 +269,18 @@ class ComposeForm extends ImmutablePureComponent {
 | 
				
			||||||
            <PollButtonContainer />
 | 
					            <PollButtonContainer />
 | 
				
			||||||
            <PrivacyDropdownContainer disabled={this.props.isEditing} />
 | 
					            <PrivacyDropdownContainer disabled={this.props.isEditing} />
 | 
				
			||||||
            <SpoilerButtonContainer />
 | 
					            <SpoilerButtonContainer />
 | 
				
			||||||
 | 
					            <LanguageDropdown />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='character-counter__wrapper'>
 | 
				
			||||||
 | 
					            <CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className='character-counter__wrapper'><CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /></div>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='compose-form__publish'>
 | 
					        <div className='compose-form__publish'>
 | 
				
			||||||
          <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={!this.canSubmit()} block /></div>
 | 
					          <div className='compose-form__publish-button-wrapper'>
 | 
				
			||||||
 | 
					            <Button text={publishText} onClick={this.handleSubmit} disabled={!this.canSubmit()} block />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,332 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { injectIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					import TextIconButton from './text_icon_button';
 | 
				
			||||||
 | 
					import Overlay from 'react-overlays/lib/Overlay';
 | 
				
			||||||
 | 
					import Motion from 'mastodon/features/ui/util/optional_motion';
 | 
				
			||||||
 | 
					import spring from 'react-motion/lib/spring';
 | 
				
			||||||
 | 
					import { supportsPassiveEvents } from 'detect-passive-events';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import { languages as preloadedLanguages } from 'mastodon/initial_state';
 | 
				
			||||||
 | 
					import fuzzysort from 'fuzzysort';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
 | 
				
			||||||
 | 
					  search: { id: 'compose.language.search', defaultMessage: 'Search languages...' },
 | 
				
			||||||
 | 
					  clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Copied from emoji-mart for consistency with emoji picker and since
 | 
				
			||||||
 | 
					// they don't export the icons in the package
 | 
				
			||||||
 | 
					const icons = {
 | 
				
			||||||
 | 
					  loupe: (
 | 
				
			||||||
 | 
					    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
 | 
				
			||||||
 | 
					      <path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  delete: (
 | 
				
			||||||
 | 
					    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
 | 
				
			||||||
 | 
					      <path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LanguageDropdownMenu extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    style: PropTypes.object,
 | 
				
			||||||
 | 
					    value: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired,
 | 
				
			||||||
 | 
					    placement: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    onClose: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onChange: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
 | 
				
			||||||
 | 
					    intl: PropTypes.object,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static defaultProps = {
 | 
				
			||||||
 | 
					    languages: preloadedLanguages,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state = {
 | 
				
			||||||
 | 
					    mounted: false,
 | 
				
			||||||
 | 
					    searchValue: '',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleDocumentClick = e => {
 | 
				
			||||||
 | 
					    if (this.node && !this.node.contains(e.target)) {
 | 
				
			||||||
 | 
					      this.props.onClose();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidMount () {
 | 
				
			||||||
 | 
					    document.addEventListener('click', this.handleDocumentClick, false);
 | 
				
			||||||
 | 
					    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
 | 
				
			||||||
 | 
					    this.setState({ mounted: true });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentWillUnmount () {
 | 
				
			||||||
 | 
					    document.removeEventListener('click', this.handleDocumentClick, false);
 | 
				
			||||||
 | 
					    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setRef = c => {
 | 
				
			||||||
 | 
					    this.node = c;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setListRef = c => {
 | 
				
			||||||
 | 
					    this.listNode = c;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleSearchChange = ({ target }) => {
 | 
				
			||||||
 | 
					    this.setState({ searchValue: target.value });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  search () {
 | 
				
			||||||
 | 
					    const { languages, value, frequentlyUsedLanguages } = this.props;
 | 
				
			||||||
 | 
					    const { searchValue } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (searchValue === '') {
 | 
				
			||||||
 | 
					      return [...languages].sort((a, b) => {
 | 
				
			||||||
 | 
					        // Push current selection to the top of the list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (a[0] === value) {
 | 
				
			||||||
 | 
					          return -1;
 | 
				
			||||||
 | 
					        } else if (b[0] === value) {
 | 
				
			||||||
 | 
					          return 1;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Sort according to frequently used languages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const indexOfA = frequentlyUsedLanguages.indexOf(a[0]);
 | 
				
			||||||
 | 
					          const indexOfB = frequentlyUsedLanguages.indexOf(b[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return fuzzysort.go(searchValue, languages, {
 | 
				
			||||||
 | 
					      keys: ['0', '1', '2'],
 | 
				
			||||||
 | 
					      limit: 5,
 | 
				
			||||||
 | 
					      threshold: -10000,
 | 
				
			||||||
 | 
					    }).map(result => result.obj);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  frequentlyUsed () {
 | 
				
			||||||
 | 
					    const { languages, value } = this.props;
 | 
				
			||||||
 | 
					    const current = languages.find(lang => lang[0] === value);
 | 
				
			||||||
 | 
					    const results = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (current) {
 | 
				
			||||||
 | 
					      results.push(current);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleClick = e => {
 | 
				
			||||||
 | 
					    const value = e.currentTarget.getAttribute('data-index');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.props.onClose();
 | 
				
			||||||
 | 
					    this.props.onChange(value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleKeyDown = e => {
 | 
				
			||||||
 | 
					    const { onClose } = this.props;
 | 
				
			||||||
 | 
					    const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let element = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch(e.key) {
 | 
				
			||||||
 | 
					    case 'Escape':
 | 
				
			||||||
 | 
					      onClose();
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'Enter':
 | 
				
			||||||
 | 
					      this.handleClick(e);
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'ArrowDown':
 | 
				
			||||||
 | 
					      element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'ArrowUp':
 | 
				
			||||||
 | 
					      element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'Tab':
 | 
				
			||||||
 | 
					      if (e.shiftKey) {
 | 
				
			||||||
 | 
					        element = this.listNode.childNodes[index - 1] || this.listNode.lastChild;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        element = this.listNode.childNodes[index + 1] || this.listNode.firstChild;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'Home':
 | 
				
			||||||
 | 
					      element = this.listNode.firstChild;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'End':
 | 
				
			||||||
 | 
					      element = this.listNode.lastChild;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (element) {
 | 
				
			||||||
 | 
					      element.focus();
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleSearchKeyDown = e => {
 | 
				
			||||||
 | 
					    const { onChange, onClose } = this.props;
 | 
				
			||||||
 | 
					    const { searchValue } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let element = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch(e.key) {
 | 
				
			||||||
 | 
					    case 'Tab':
 | 
				
			||||||
 | 
					    case 'ArrowDown':
 | 
				
			||||||
 | 
					      element = this.listNode.firstChild;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (element) {
 | 
				
			||||||
 | 
					        element.focus();
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'Enter':
 | 
				
			||||||
 | 
					      element = this.listNode.firstChild;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (element) {
 | 
				
			||||||
 | 
					        onChange(element.getAttribute('data-index'));
 | 
				
			||||||
 | 
					        onClose();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'Escape':
 | 
				
			||||||
 | 
					      if (searchValue !== '') {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        this.handleClear();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleClear = () => {
 | 
				
			||||||
 | 
					    this.setState({ searchValue: '' });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderItem = lang => {
 | 
				
			||||||
 | 
					    const { value } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
 | 
				
			||||||
 | 
					        <span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { style, placement, intl } = this.props;
 | 
				
			||||||
 | 
					    const { mounted, searchValue } = this.state;
 | 
				
			||||||
 | 
					    const isSearching = searchValue !== '';
 | 
				
			||||||
 | 
					    const results = this.search();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
 | 
				
			||||||
 | 
					        {({ opacity, scaleX, scaleY }) => (
 | 
				
			||||||
 | 
					          // It should not be transformed when mounting because the resulting
 | 
				
			||||||
 | 
					          // size will be used to determine the coordinate of the menu by
 | 
				
			||||||
 | 
					          // react-overlays
 | 
				
			||||||
 | 
					          <div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
 | 
				
			||||||
 | 
					            <div className='emoji-mart-search'>
 | 
				
			||||||
 | 
					              <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus />
 | 
				
			||||||
 | 
					              <button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? icons.loupe : icons.delete}</button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
 | 
				
			||||||
 | 
					              {results.map(this.renderItem)}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </Motion>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @injectIntl
 | 
				
			||||||
 | 
					class LanguageDropdown extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    value: PropTypes.string,
 | 
				
			||||||
 | 
					    frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    onChange: PropTypes.func,
 | 
				
			||||||
 | 
					    onClose: PropTypes.func,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state = {
 | 
				
			||||||
 | 
					    open: false,
 | 
				
			||||||
 | 
					    placement: 'bottom',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleToggle = ({ target }) => {
 | 
				
			||||||
 | 
					    const { top } = target.getBoundingClientRect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.state.open && this.activeElement) {
 | 
				
			||||||
 | 
					      this.activeElement.focus({ preventScroll: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
 | 
				
			||||||
 | 
					    this.setState({ open: !this.state.open });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleClose = () => {
 | 
				
			||||||
 | 
					    const { value, onClose } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.state.open && this.activeElement) {
 | 
				
			||||||
 | 
					      this.activeElement.focus({ preventScroll: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.setState({ open: false });
 | 
				
			||||||
 | 
					    onClose(value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChange = value => {
 | 
				
			||||||
 | 
					    const { onChange } = this.props;
 | 
				
			||||||
 | 
					    onChange(value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { value, intl, frequentlyUsedLanguages } = this.props;
 | 
				
			||||||
 | 
					    const { open, placement } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className={classNames('privacy-dropdown', { active: open })}>
 | 
				
			||||||
 | 
					        <div className='privacy-dropdown__value'>
 | 
				
			||||||
 | 
					          <TextIconButton
 | 
				
			||||||
 | 
					            className='privacy-dropdown__value-icon'
 | 
				
			||||||
 | 
					            label={value && value.toUpperCase()}
 | 
				
			||||||
 | 
					            title={intl.formatMessage(messages.changeLanguage)}
 | 
				
			||||||
 | 
					            active={open}
 | 
				
			||||||
 | 
					            onClick={this.handleToggle}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Overlay show={open} placement={placement} target={this}>
 | 
				
			||||||
 | 
					          <LanguageDropdownMenu
 | 
				
			||||||
 | 
					            value={value}
 | 
				
			||||||
 | 
					            frequentlyUsedLanguages={frequentlyUsedLanguages}
 | 
				
			||||||
 | 
					            onClose={this.handleClose}
 | 
				
			||||||
 | 
					            onChange={this.handleChange}
 | 
				
			||||||
 | 
					            placement={placement}
 | 
				
			||||||
 | 
					            intl={intl}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Overlay>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -17,11 +17,6 @@ export default class TextIconButton extends React.PureComponent {
 | 
				
			||||||
    ariaControls: PropTypes.string,
 | 
					    ariaControls: PropTypes.string,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleClick = (e) => {
 | 
					 | 
				
			||||||
    e.preventDefault();
 | 
					 | 
				
			||||||
    this.props.onClick();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { label, title, active, ariaControls } = this.props;
 | 
					    const { label, title, active, ariaControls } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +26,7 @@ export default class TextIconButton extends React.PureComponent {
 | 
				
			||||||
        aria-label={title}
 | 
					        aria-label={title}
 | 
				
			||||||
        className={`text-icon-button ${active ? 'active' : ''}`}
 | 
					        className={`text-icon-button ${active ? 'active' : ''}`}
 | 
				
			||||||
        aria-expanded={active}
 | 
					        aria-expanded={active}
 | 
				
			||||||
        onClick={this.handleClick}
 | 
					        onClick={this.props.onClick}
 | 
				
			||||||
        aria-controls={ariaControls} style={iconStyle}
 | 
					        aria-controls={ariaControls} style={iconStyle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {label}
 | 
					        {label}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import LanguageDropdown from '../components/language_dropdown';
 | 
				
			||||||
 | 
					import { changeComposeLanguage } from 'mastodon/actions/compose';
 | 
				
			||||||
 | 
					import { useLanguage } from 'mastodon/actions/languages';
 | 
				
			||||||
 | 
					import { createSelector } from 'reselect';
 | 
				
			||||||
 | 
					import { Map as ImmutableMap } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getFrequentlyUsedLanguages = createSelector([
 | 
				
			||||||
 | 
					  state => state.getIn(['settings', 'frequentlyUsedLanguages'], ImmutableMap()),
 | 
				
			||||||
 | 
					], languageCounters => (
 | 
				
			||||||
 | 
					  languageCounters.keySeq()
 | 
				
			||||||
 | 
					    .sort((a, b) => languageCounters.get(a) - languageCounters.get(b))
 | 
				
			||||||
 | 
					    .reverse()
 | 
				
			||||||
 | 
					    .toArray()
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  frequentlyUsedLanguages: getFrequentlyUsedLanguages(state),
 | 
				
			||||||
 | 
					  value: state.getIn(['compose', 'language']),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onChange (value) {
 | 
				
			||||||
 | 
					    dispatch(changeComposeLanguage(value));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onClose (value) {
 | 
				
			||||||
 | 
					    dispatch(useLanguage(value));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps, mapDispatchToProps)(LanguageDropdown);
 | 
				
			||||||
| 
						 | 
					@ -27,5 +27,6 @@ export const showTrends = getMeta('trends');
 | 
				
			||||||
export const title = getMeta('title');
 | 
					export const title = getMeta('title');
 | 
				
			||||||
export const cropImages = getMeta('crop_images');
 | 
					export const cropImages = getMeta('crop_images');
 | 
				
			||||||
export const disableSwiping = getMeta('disable_swiping');
 | 
					export const disableSwiping = getMeta('disable_swiping');
 | 
				
			||||||
 | 
					export const languages = initialState && initialState.languages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default initialState;
 | 
					export default initialState;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1257,6 +1257,23 @@
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "path": "app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.json"
 | 
					    "path": "app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.json"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "descriptors": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "defaultMessage": "Change language",
 | 
				
			||||||
 | 
					        "id": "compose.language.change"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "defaultMessage": "Search languages...",
 | 
				
			||||||
 | 
					        "id": "compose.language.search"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "defaultMessage": "Clear",
 | 
				
			||||||
 | 
					        "id": "emoji_button.clear"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "path": "app/javascript/mastodon/features/compose/components/language_dropdown.json"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    "descriptors": [
 | 
					    "descriptors": [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,6 +92,8 @@
 | 
				
			||||||
  "community.column_settings.local_only": "Local only",
 | 
					  "community.column_settings.local_only": "Local only",
 | 
				
			||||||
  "community.column_settings.media_only": "Media Only",
 | 
					  "community.column_settings.media_only": "Media Only",
 | 
				
			||||||
  "community.column_settings.remote_only": "Remote only",
 | 
					  "community.column_settings.remote_only": "Remote only",
 | 
				
			||||||
 | 
					  "compose.language.change": "Change language",
 | 
				
			||||||
 | 
					  "compose.language.search": "Search languages...",
 | 
				
			||||||
  "compose_form.direct_message_warning_learn_more": "Learn more",
 | 
					  "compose_form.direct_message_warning_learn_more": "Learn more",
 | 
				
			||||||
  "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
 | 
					  "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
 | 
				
			||||||
  "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
 | 
					  "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
 | 
				
			||||||
| 
						 | 
					@ -147,6 +149,7 @@
 | 
				
			||||||
  "embed.instructions": "Embed this post on your website by copying the code below.",
 | 
					  "embed.instructions": "Embed this post on your website by copying the code below.",
 | 
				
			||||||
  "embed.preview": "Here is what it will look like:",
 | 
					  "embed.preview": "Here is what it will look like:",
 | 
				
			||||||
  "emoji_button.activity": "Activity",
 | 
					  "emoji_button.activity": "Activity",
 | 
				
			||||||
 | 
					  "emoji_button.clear": "Clear",
 | 
				
			||||||
  "emoji_button.custom": "Custom",
 | 
					  "emoji_button.custom": "Custom",
 | 
				
			||||||
  "emoji_button.flags": "Flags",
 | 
					  "emoji_button.flags": "Flags",
 | 
				
			||||||
  "emoji_button.food": "Food & Drink",
 | 
					  "emoji_button.food": "Food & Drink",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import {
 | 
				
			||||||
  COMPOSE_SPOILERNESS_CHANGE,
 | 
					  COMPOSE_SPOILERNESS_CHANGE,
 | 
				
			||||||
  COMPOSE_SPOILER_TEXT_CHANGE,
 | 
					  COMPOSE_SPOILER_TEXT_CHANGE,
 | 
				
			||||||
  COMPOSE_VISIBILITY_CHANGE,
 | 
					  COMPOSE_VISIBILITY_CHANGE,
 | 
				
			||||||
 | 
					  COMPOSE_LANGUAGE_CHANGE,
 | 
				
			||||||
  COMPOSE_COMPOSING_CHANGE,
 | 
					  COMPOSE_COMPOSING_CHANGE,
 | 
				
			||||||
  COMPOSE_EMOJI_INSERT,
 | 
					  COMPOSE_EMOJI_INSERT,
 | 
				
			||||||
  COMPOSE_UPLOAD_CHANGE_REQUEST,
 | 
					  COMPOSE_UPLOAD_CHANGE_REQUEST,
 | 
				
			||||||
| 
						 | 
					@ -79,6 +80,7 @@ const initialState = ImmutableMap({
 | 
				
			||||||
  suggestions: ImmutableList(),
 | 
					  suggestions: ImmutableList(),
 | 
				
			||||||
  default_privacy: 'public',
 | 
					  default_privacy: 'public',
 | 
				
			||||||
  default_sensitive: false,
 | 
					  default_sensitive: false,
 | 
				
			||||||
 | 
					  default_language: 'en',
 | 
				
			||||||
  resetFileKey: Math.floor((Math.random() * 0x10000)),
 | 
					  resetFileKey: Math.floor((Math.random() * 0x10000)),
 | 
				
			||||||
  idempotencyKey: null,
 | 
					  idempotencyKey: null,
 | 
				
			||||||
  tagHistory: ImmutableList(),
 | 
					  tagHistory: ImmutableList(),
 | 
				
			||||||
| 
						 | 
					@ -117,7 +119,8 @@ function clearAll(state) {
 | 
				
			||||||
    map.set('is_changing_upload', false);
 | 
					    map.set('is_changing_upload', false);
 | 
				
			||||||
    map.set('in_reply_to', null);
 | 
					    map.set('in_reply_to', null);
 | 
				
			||||||
    map.set('privacy', state.get('default_privacy'));
 | 
					    map.set('privacy', state.get('default_privacy'));
 | 
				
			||||||
    map.set('sensitive', false);
 | 
					    map.set('sensitive', state.get('default_sensitive'));
 | 
				
			||||||
 | 
					    map.set('language', state.get('default_language'));
 | 
				
			||||||
    map.update('media_attachments', list => list.clear());
 | 
					    map.update('media_attachments', list => list.clear());
 | 
				
			||||||
    map.set('poll', null);
 | 
					    map.set('poll', null);
 | 
				
			||||||
    map.set('idempotencyKey', uuid());
 | 
					    map.set('idempotencyKey', uuid());
 | 
				
			||||||
| 
						 | 
					@ -440,6 +443,7 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
      map.set('caretPosition', null);
 | 
					      map.set('caretPosition', null);
 | 
				
			||||||
      map.set('idempotencyKey', uuid());
 | 
					      map.set('idempotencyKey', uuid());
 | 
				
			||||||
      map.set('sensitive', action.status.get('sensitive'));
 | 
					      map.set('sensitive', action.status.get('sensitive'));
 | 
				
			||||||
 | 
					      map.set('language', action.status.get('language'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (action.status.get('spoiler_text').length > 0) {
 | 
					      if (action.status.get('spoiler_text').length > 0) {
 | 
				
			||||||
        map.set('spoiler', true);
 | 
					        map.set('spoiler', true);
 | 
				
			||||||
| 
						 | 
					@ -468,6 +472,7 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
      map.set('caretPosition', null);
 | 
					      map.set('caretPosition', null);
 | 
				
			||||||
      map.set('idempotencyKey', uuid());
 | 
					      map.set('idempotencyKey', uuid());
 | 
				
			||||||
      map.set('sensitive', action.status.get('sensitive'));
 | 
					      map.set('sensitive', action.status.get('sensitive'));
 | 
				
			||||||
 | 
					      map.set('language', action.status.get('language'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (action.spoiler_text.length > 0) {
 | 
					      if (action.spoiler_text.length > 0) {
 | 
				
			||||||
        map.set('spoiler', true);
 | 
					        map.set('spoiler', true);
 | 
				
			||||||
| 
						 | 
					@ -497,6 +502,8 @@ export default function compose(state = initialState, action) {
 | 
				
			||||||
    return state.updateIn(['poll', 'options'], options => options.delete(action.index));
 | 
					    return state.updateIn(['poll', 'options'], options => options.delete(action.index));
 | 
				
			||||||
  case COMPOSE_POLL_SETTINGS_CHANGE:
 | 
					  case COMPOSE_POLL_SETTINGS_CHANGE:
 | 
				
			||||||
    return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
 | 
					    return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
 | 
				
			||||||
 | 
					  case COMPOSE_LANGUAGE_CHANGE:
 | 
				
			||||||
 | 
					    return state.set('language', action.language);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
 | 
				
			||||||
import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns';
 | 
					import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns';
 | 
				
			||||||
import { STORE_HYDRATE } from '../actions/store';
 | 
					import { STORE_HYDRATE } from '../actions/store';
 | 
				
			||||||
import { EMOJI_USE } from '../actions/emojis';
 | 
					import { EMOJI_USE } from '../actions/emojis';
 | 
				
			||||||
 | 
					import { LANGUAGE_USE } from '../actions/languages';
 | 
				
			||||||
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
 | 
					import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
 | 
				
			||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
 | 
					import { Map as ImmutableMap, fromJS } from 'immutable';
 | 
				
			||||||
import uuid from '../uuid';
 | 
					import uuid from '../uuid';
 | 
				
			||||||
| 
						 | 
					@ -129,6 +130,8 @@ const changeColumnParams = (state, uuid, path, value) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
 | 
					const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const updateFrequentLanguages = (state, language) => state.update('frequentlyUsedLanguages', ImmutableMap(), map => map.update(language, 0, count => count + 1)).set('saved', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
 | 
					const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function settings(state = initialState, action) {
 | 
					export default function settings(state = initialState, action) {
 | 
				
			||||||
| 
						 | 
					@ -154,6 +157,8 @@ export default function settings(state = initialState, action) {
 | 
				
			||||||
    return changeColumnParams(state, action.uuid, action.path, action.value);
 | 
					    return changeColumnParams(state, action.uuid, action.path, action.value);
 | 
				
			||||||
  case EMOJI_USE:
 | 
					  case EMOJI_USE:
 | 
				
			||||||
    return updateFrequentEmojis(state, action.emoji);
 | 
					    return updateFrequentEmojis(state, action.emoji);
 | 
				
			||||||
 | 
					  case LANGUAGE_USE:
 | 
				
			||||||
 | 
					    return updateFrequentLanguages(state, action.language);
 | 
				
			||||||
  case SETTING_SAVE:
 | 
					  case SETTING_SAVE:
 | 
				
			||||||
    return state.set('saved', true);
 | 
					    return state.set('saved', true);
 | 
				
			||||||
  case LIST_FETCH_FAIL:
 | 
					  case LIST_FETCH_FAIL:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4349,7 +4349,6 @@ a.status-card.compact:hover {
 | 
				
			||||||
  background: $simple-background-color;
 | 
					  background: $simple-background-color;
 | 
				
			||||||
  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
					  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  margin-left: 40px;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  z-index: 2;
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4450,6 +4449,71 @@ a.status-card.compact:hover {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.language-dropdown {
 | 
				
			||||||
 | 
					  &__dropdown {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    background: $simple-background-color;
 | 
				
			||||||
 | 
					    box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    z-index: 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.top {
 | 
				
			||||||
 | 
					      transform-origin: 50% 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.bottom {
 | 
				
			||||||
 | 
					      transform-origin: 50% 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .emoji-mart-search {
 | 
				
			||||||
 | 
					      padding-right: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .emoji-mart-search-icon {
 | 
				
			||||||
 | 
					      right: 10px + 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .emoji-mart-scroll {
 | 
				
			||||||
 | 
					      padding: 0 10px 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__results {
 | 
				
			||||||
 | 
					      &__item {
 | 
				
			||||||
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					        color: $inverted-text-color;
 | 
				
			||||||
 | 
					        font-weight: 500;
 | 
				
			||||||
 | 
					        padding: 10px;
 | 
				
			||||||
 | 
					        border-radius: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:focus,
 | 
				
			||||||
 | 
					        &:active,
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background: $ui-secondary-color;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &__common-name {
 | 
				
			||||||
 | 
					          color: $darker-text-color;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.active {
 | 
				
			||||||
 | 
					          background: $ui-highlight-color;
 | 
				
			||||||
 | 
					          color: $primary-text-color;
 | 
				
			||||||
 | 
					          outline: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .language-dropdown__dropdown__results__item__common-name {
 | 
				
			||||||
 | 
					            color: $secondary-text-color;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            background: lighten($ui-highlight-color, 4%);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search {
 | 
					.search {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InitialStateSerializer < ActiveModel::Serializer
 | 
					class InitialStateSerializer < ActiveModel::Serializer
 | 
				
			||||||
  attributes :meta, :compose, :accounts,
 | 
					  attributes :meta, :compose, :accounts,
 | 
				
			||||||
             :media_attachments, :settings
 | 
					             :media_attachments, :settings,
 | 
				
			||||||
 | 
					             :languages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 | 
					  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,6 +60,7 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
				
			||||||
      store[:me]                = object.current_account.id.to_s
 | 
					      store[:me]                = object.current_account.id.to_s
 | 
				
			||||||
      store[:default_privacy]   = object.visibility || object.current_account.user.setting_default_privacy
 | 
					      store[:default_privacy]   = object.visibility || object.current_account.user.setting_default_privacy
 | 
				
			||||||
      store[:default_sensitive] = object.current_account.user.setting_default_sensitive
 | 
					      store[:default_sensitive] = object.current_account.user.setting_default_sensitive
 | 
				
			||||||
 | 
					      store[:default_language]  = object.current_account.user.preferred_posting_language
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    store[:text] = object.text if object.text
 | 
					    store[:text] = object.text if object.text
 | 
				
			||||||
| 
						 | 
					@ -77,6 +79,10 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
				
			||||||
    { accept_content_types: MediaAttachment.supported_file_extensions + MediaAttachment.supported_mime_types }
 | 
					    { accept_content_types: MediaAttachment.supported_file_extensions + MediaAttachment.supported_mime_types }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def languages
 | 
				
			||||||
 | 
					    LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def instance_presenter
 | 
					  def instance_presenter
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,6 +66,7 @@
 | 
				
			||||||
    "express": "^4.18.1",
 | 
					    "express": "^4.18.1",
 | 
				
			||||||
    "file-loader": "^6.2.0",
 | 
					    "file-loader": "^6.2.0",
 | 
				
			||||||
    "font-awesome": "^4.7.0",
 | 
					    "font-awesome": "^4.7.0",
 | 
				
			||||||
 | 
					    "fuzzysort": "^1.9.0",
 | 
				
			||||||
    "glob": "^8.0.1",
 | 
					    "glob": "^8.0.1",
 | 
				
			||||||
    "history": "^4.10.1",
 | 
					    "history": "^4.10.1",
 | 
				
			||||||
    "http-link-header": "^1.0.4",
 | 
					    "http-link-header": "^1.0.4",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5254,6 +5254,11 @@ functions-have-names@^1.2.2:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
 | 
					  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
 | 
				
			||||||
  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 | 
					  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fuzzysort@^1.9.0:
 | 
				
			||||||
 | 
					  version "1.9.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.9.0.tgz#d36d27949eae22340bb6f7ba30ea6751b92a181c"
 | 
				
			||||||
 | 
					  integrity sha512-MOxCT0qLTwLqmEwc7UtU045RKef7mc8Qz8eR4r2bLNEq9dy/c3ZKMEFp6IEst69otkQdFZ4FfgH2dmZD+ddX1g==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gauge@^4.0.3:
 | 
					gauge@^4.0.3:
 | 
				
			||||||
  version "4.0.4"
 | 
					  version "4.0.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce"
 | 
					  resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue