From 32db7c6fad27d1e5bae6e57e5562142b94b186c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 17 May 2024 18:49:27 +0200 Subject: [PATCH] Remember frequently used languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/actions/compose.ts | 5 ++++ src/actions/languages.ts | 16 +++++++++++++ .../compose/components/language-dropdown.tsx | 23 ++++++++++++++++--- src/locales/en.json | 1 + src/reducers/settings.ts | 9 +++++++- 5 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/actions/languages.ts diff --git a/src/actions/compose.ts b/src/actions/compose.ts index a930e3270..b2e1b4066 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -15,6 +15,7 @@ import { getFeatures, parseVersion } from 'soapbox/utils/features'; import { chooseEmoji } from './emojis'; import { importFetchedAccounts } from './importer'; +import { rememberLanguageUse } from './languages'; import { uploadFile, updateMedia } from './media'; import { openModal, closeModal } from './modals'; import { getSettings } from './settings'; @@ -361,6 +362,10 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => dispatch(submitComposeRequest(composeId)); dispatch(closeModal()); + if (compose.language && !statusId) { + dispatch(rememberLanguageUse(compose.language)); + } + const idempotencyKey = compose.idempotencyKey; const params: Record = { diff --git a/src/actions/languages.ts b/src/actions/languages.ts new file mode 100644 index 000000000..08255eff1 --- /dev/null +++ b/src/actions/languages.ts @@ -0,0 +1,16 @@ +import { saveSettings } from './settings'; + +import type { AppDispatch } from 'soapbox/store'; + +const LANGUAGE_USE = 'LANGUAGE_USE' as const; + +const rememberLanguageUse = (language: string) => (dispatch: AppDispatch) => { + dispatch({ + type: LANGUAGE_USE, + language, + }); + + dispatch(saveSettings()); +}; + +export { LANGUAGE_USE, rememberLanguageUse }; diff --git a/src/features/compose/components/language-dropdown.tsx b/src/features/compose/components/language-dropdown.tsx index f5803a18d..99a157bd3 100644 --- a/src/features/compose/components/language-dropdown.tsx +++ b/src/features/compose/components/language-dropdown.tsx @@ -2,13 +2,24 @@ import { offset, useFloating, flip, arrow, shift } from '@floating-ui/react'; import clsx from 'clsx'; import { supportsPassiveEvents } from 'detect-passive-events'; import fuzzysort from 'fuzzysort'; +import { Map as ImmutableMap } from 'immutable'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { createSelector } from 'reselect'; import { changeComposeLanguage } from 'soapbox/actions/compose'; import { Button, Icon, Input, Portal } from 'soapbox/components/ui'; import { type Language, languages as languagesObject } from 'soapbox/features/preferences'; -import { useAppDispatch, useCompose } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useCompose } from 'soapbox/hooks'; + +const getFrequentlyUsedLanguages = createSelector([ + state => state.settings.get('frequentlyUsedLanguages', ImmutableMap()), +], (languageCounters: ImmutableMap) => ( + languageCounters.keySeq() + .sort((a, b) => languageCounters.get(a, 0) - languageCounters.get(b, 0)) + .reverse() + .toArray() +)); const listenerOptions = supportsPassiveEvents ? { passive: true } : false; @@ -26,14 +37,15 @@ interface ILanguageDropdown { const LanguageDropdown: React.FC = ({ composeId }) => { const intl = useIntl(); const dispatch = useAppDispatch(); + const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages); const node = useRef(null); const focusedItem = useRef(null); + const arrowRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [searchValue, setSearchValue] = useState(''); - const arrowRef = useRef(null); const { x, y, strategy, refs, middlewareData, placement } = useFloating({ placement: 'top', @@ -131,7 +143,12 @@ const LanguageDropdown: React.FC = ({ composeId }) => { } else if (b[0] === language) { return 1; } else { - return 0; + // 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)); } }); } diff --git a/src/locales/en.json b/src/locales/en.json index 1f6e32347..ab89fd258 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -415,6 +415,7 @@ "compose.edit_success": "Your post was edited", "compose.invalid_schedule": "You must schedule a post at least 5 minutes out.", "compose.language_dropdown.prompt": "Select language", + "compose.language_dropdown.search": "Search language…", "compose.reply_group_indicator.message": "Posting to {groupLink}", "compose.submit_success": "Your post was sent!", "compose_event.create": "Create", diff --git a/src/reducers/settings.ts b/src/reducers/settings.ts index f92de981d..2bc977fe6 100644 --- a/src/reducers/settings.ts +++ b/src/reducers/settings.ts @@ -1,6 +1,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { AnyAction } from 'redux'; +import { LANGUAGE_USE } from 'soapbox/actions/languages'; import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; import { EMOJI_CHOOSE } from '../actions/emojis'; @@ -18,7 +19,11 @@ import type { APIEntity } from 'soapbox/types/entities'; type State = ImmutableMap; -const updateFrequentEmojis = (state: State, emoji: Emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, (count: number) => count + 1)).set('saved', false); +const updateFrequentEmojis = (state: State, emoji: Emoji) => + state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, (count: number) => count + 1)).set('saved', false); + +const updateFrequentLanguages = (state: State, language: string) => + state.update('frequentlyUsedLanguages', ImmutableMap(), map => map.update(language, 0, (count: number) => count + 1)).set('saved', false); const importSettings = (state: State, account: APIEntity) => { account = fromJS(account); @@ -45,6 +50,8 @@ const settings = ( .set('saved', false); case EMOJI_CHOOSE: return updateFrequentEmojis(state, action.emoji); + case LANGUAGE_USE: + return updateFrequentLanguages(state, action.language); case SETTING_SAVE: return state.set('saved', true); case SETTINGS_UPDATE: