From e4615b70f7ba8d2be0f3cfa6645503d6f2c4abf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 7 Oct 2024 01:32:55 +0200 Subject: [PATCH 1/3] pl-fe: WIP migrate settings store to zustand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-fe/src/actions/chats.ts | 5 +- packages/pl-fe/src/actions/compose.ts | 6 +- packages/pl-fe/src/actions/me.ts | 4 + packages/pl-fe/src/actions/notifications.ts | 14 +- packages/pl-fe/src/actions/pl-fe.ts | 4 + packages/pl-fe/src/actions/remote-timeline.ts | 12 +- packages/pl-fe/src/actions/settings.ts | 209 ++++++++---------- packages/pl-fe/src/actions/statuses.ts | 6 +- packages/pl-fe/src/actions/timelines.ts | 21 +- .../src/api/hooks/streaming/useUserStream.ts | 32 ++- .../pl-fe/src/components/sidebar-menu.tsx | 6 +- .../features/account/components/header.tsx | 27 ++- .../compose/components/language-dropdown.tsx | 20 +- .../pl-fe/src/features/developers/index.tsx | 5 +- .../features/developers/settings-store.tsx | 16 +- .../directory/components/account-card.tsx | 4 +- .../components/draft-status-action-bar.tsx | 5 +- .../components/emoji-picker-dropdown.tsx | 26 +-- .../notifications/components/notification.tsx | 31 ++- .../pl-fe-config/components/site-preview.tsx | 6 +- .../scheduled-status-action-bar.tsx | 5 +- .../src/features/status/components/thread.tsx | 5 +- packages/pl-fe/src/hooks/useLocale.ts | 5 +- packages/pl-fe/src/hooks/useSettings.ts | 12 +- packages/pl-fe/src/reducers/index.ts | 4 +- packages/pl-fe/src/reducers/pl-fe.ts | 4 +- packages/pl-fe/src/reducers/settings.ts | 50 +---- packages/pl-fe/src/schemas/pl-fe/settings.ts | 15 ++ packages/pl-fe/src/selectors/index.ts | 8 +- packages/pl-fe/src/stores/settings.ts | 74 +++++++ packages/pl-fe/src/utils/timelines.ts | 16 +- 31 files changed, 351 insertions(+), 306 deletions(-) create mode 100644 packages/pl-fe/src/stores/settings.ts diff --git a/packages/pl-fe/src/actions/chats.ts b/packages/pl-fe/src/actions/chats.ts index 84feba642..cfccec407 100644 --- a/packages/pl-fe/src/actions/chats.ts +++ b/packages/pl-fe/src/actions/chats.ts @@ -1,10 +1,11 @@ -import { getSettings, changeSetting } from 'pl-fe/actions/settings'; +import { changeSetting } from 'pl-fe/actions/settings'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import type { AppDispatch, RootState } from 'pl-fe/store'; const toggleMainWindow = () => (dispatch: AppDispatch, getState: () => RootState) => { - const main = getSettings(getState()).getIn(['chats', 'mainWindow']) as 'minimized' | 'open'; + const main = useSettingsStore.getState().settings.chats.mainWindow; const state = main === 'minimized' ? 'open' : 'minimized'; return dispatch(changeSetting(['chats', 'mainWindow'], state)); }; diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index d5fc0e3d2..77ffd1bcf 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -8,6 +8,7 @@ import { Language } from 'pl-fe/features/preferences'; import { selectAccount, selectOwnAccount, makeGetAccount } from 'pl-fe/selectors'; import { tagHistory } from 'pl-fe/settings'; import { useModalsStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import toast from 'pl-fe/toast'; import { isLoggedIn } from 'pl-fe/utils/auth'; @@ -15,7 +16,6 @@ import { chooseEmoji } from './emojis'; import { importFetchedAccounts } from './importer'; import { rememberLanguageUse } from './languages'; import { uploadFile, updateMedia } from './media'; -import { getSettings } from './settings'; import { createStatus } from './statuses'; import type { EditorState } from 'lexical'; @@ -178,7 +178,7 @@ const replyCompose = ( const state = getState(); const client = getClient(state); const { createStatusExplicitAddressing: explicitAddressing } = client.features; - const preserveSpoilers = !!getSettings(state).get('preserveSpoilers'); + const preserveSpoilers = useSettingsStore.getState().settings.preserveSpoilers; const account = selectOwnAccount(state); if (!account) return; @@ -321,7 +321,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c const needsDescriptions = (state: RootState, composeId: string) => { const media = state.compose.get(composeId)!.media_attachments; - const missingDescriptionModal = getSettings(state).get('missingDescriptionModal'); + const missingDescriptionModal = useSettingsStore.getState().settings.missingDescriptionModal; const hasMissing = media.filter(item => !item.description).size > 0; diff --git a/packages/pl-fe/src/actions/me.ts b/packages/pl-fe/src/actions/me.ts index 25d5fe41d..03a374ef4 100644 --- a/packages/pl-fe/src/actions/me.ts +++ b/packages/pl-fe/src/actions/me.ts @@ -1,12 +1,14 @@ import { selectAccount } from 'pl-fe/selectors'; import { setSentryAccount } from 'pl-fe/sentry'; import KVStore from 'pl-fe/storage/kv-store'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { getAuthUserId, getAuthUserUrl } from 'pl-fe/utils/auth'; import { getClient } from '../api'; import { loadCredentials } from './auth'; import { importFetchedAccount } from './importer'; +import { FE_NAME } from './settings'; import type { CredentialAccount, UpdateCredentialsParams } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -95,6 +97,8 @@ const fetchMeRequest = () => ({ const fetchMeSuccess = (account: CredentialAccount) => { setSentryAccount(account); + useSettingsStore.getState().loadUserSettings(account.settings_store?.[FE_NAME]); + return { type: ME_FETCH_SUCCESS, me: account, diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts index ce23dcd5e..59c30394c 100644 --- a/packages/pl-fe/src/actions/notifications.ts +++ b/packages/pl-fe/src/actions/notifications.ts @@ -6,6 +6,7 @@ import { getClient } from 'pl-fe/api'; import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification'; import { normalizeNotification, normalizeNotifications, type Notification } from 'pl-fe/normalizers'; import { getFilters, regexFromFilters } from 'pl-fe/selectors'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { isLoggedIn } from 'pl-fe/utils/auth'; import { compareId } from 'pl-fe/utils/comparators'; import { unescapeHTML } from 'pl-fe/utils/html'; @@ -20,7 +21,7 @@ import { importFetchedStatuses, } from './importer'; import { saveMarker } from './markers'; -import { getSettings, saveSettings } from './settings'; +import { saveSettings } from './settings'; import type { Account, Notification as BaseNotification, PaginatedResponse, Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -72,7 +73,8 @@ const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array (dispatch: AppDispatch, getState: () => RootState) => { - const showInColumn = getSettings(getState()).getIn(['notifications', 'shows', notification.type], true); + const selectedFilter = useSettingsStore().settings.notifications.quickFilter.active; + const showInColumn = selectedFilter === 'all' ? true : (FILTER_TYPES[selectedFilter as FilterType] || [notification.type]).includes(notification.type); if (notification.account) { dispatch(importFetchedAccount(notification.account)); @@ -105,7 +107,7 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages: if (notification.type === 'chat_mention') return; // Drop chat notifications, handle them per-chat const filters = getFilters(getState(), { contextType: 'notifications' }); - const playSound = getSettings(getState()).getIn(['notifications', 'sounds', notification.type]); + const playSound = useSettingsStore.getState().settings.notifications.sounds[notification.type]; const status = getNotificationStatus(notification); @@ -195,7 +197,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an const state = getState(); const features = state.auth.client.features; - const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as FilterType; + const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType; const notifications = state.notifications; if (notifications.isLoading) { @@ -291,8 +293,8 @@ const scrollTopNotifications = (top: boolean) => }; const setFilter = (filterType: FilterType, abort?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - const activeFilter = getSettings(getState()).getIn(['notifications', 'quickFilter', 'active']); + (dispatch: AppDispatch) => { + const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType; dispatch({ type: NOTIFICATIONS_FILTER_SET, diff --git a/packages/pl-fe/src/actions/pl-fe.ts b/packages/pl-fe/src/actions/pl-fe.ts index 899be612c..f4c981ec7 100644 --- a/packages/pl-fe/src/actions/pl-fe.ts +++ b/packages/pl-fe/src/actions/pl-fe.ts @@ -3,6 +3,7 @@ import { createSelector } from 'reselect'; import { getHost } from 'pl-fe/actions/instance'; import { normalizePlFeConfig } from 'pl-fe/normalizers'; import KVStore from 'pl-fe/storage/kv-store'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { getClient, staticFetch } from '../api'; @@ -77,6 +78,9 @@ const importPlFeConfig = (plFeConfig: APIEntity, host: string | null) => { if (!plFeConfig.brandColor) { plFeConfig.brandColor = '#d80482'; } + + useSettingsStore.getState().loadDefaultSettings(plFeConfig?.defaultSettings); + return { type: PLFE_CONFIG_REQUEST_SUCCESS, plFeConfig, diff --git a/packages/pl-fe/src/actions/remote-timeline.ts b/packages/pl-fe/src/actions/remote-timeline.ts index b518a7bfc..5d431f08b 100644 --- a/packages/pl-fe/src/actions/remote-timeline.ts +++ b/packages/pl-fe/src/actions/remote-timeline.ts @@ -1,11 +1,11 @@ -import { getSettings, changeSetting } from 'pl-fe/actions/settings'; +import { changeSetting } from 'pl-fe/actions/settings'; +import { useSettingsStore } from 'pl-fe/stores/settings'; -import type { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { AppDispatch, RootState } from 'pl-fe/store'; const getPinnedHosts = (state: RootState) => { - const settings = getSettings(state); - return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableList | ImmutableOrderedSet; + const { settings } = useSettingsStore.getState(); + return settings.remote_timeline.pinnedHosts; }; const pinHost = (host: string) => @@ -13,7 +13,7 @@ const pinHost = (host: string) => const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().add(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], [...pinnedHosts, host])); }; const unpinHost = (host: string) => @@ -21,7 +21,7 @@ const unpinHost = (host: string) => const state = getState(); const pinnedHosts = getPinnedHosts(state); - return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().remove(host))); + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.filter(value => value !== host))); }; export { diff --git a/packages/pl-fe/src/actions/settings.ts b/packages/pl-fe/src/actions/settings.ts index 9fcd7a717..e55f78660 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -1,20 +1,17 @@ -import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import { defineMessage } from 'react-intl'; -import { createSelector } from 'reselect'; import { patchMe } from 'pl-fe/actions/me'; import { getClient } from 'pl-fe/api'; import messages from 'pl-fe/messages'; import { makeGetAccount } from 'pl-fe/selectors'; import KVStore from 'pl-fe/storage/kv-store'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import toast from 'pl-fe/toast'; import { isLoggedIn } from 'pl-fe/utils/auth'; import type { AppDispatch, RootState } from 'pl-fe/store'; const SETTING_CHANGE = 'SETTING_CHANGE' as const; -const SETTING_SAVE = 'SETTING_SAVE' as const; -const SETTINGS_UPDATE = 'SETTINGS_UPDATE' as const; const FE_NAME = 'pl_fe'; @@ -28,114 +25,109 @@ type SettingOpts = { const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }); -const defaultSettings = ImmutableMap({ - onboarded: false, - skinTone: 1, - reduceMotion: false, - underlineLinks: false, - autoPlayGif: true, - displayMedia: 'default', - displaySpoilers: false, - unfollowModal: true, - boostModal: false, - deleteModal: true, - missingDescriptionModal: true, - defaultPrivacy: 'public', - defaultContentType: 'text/plain', - themeMode: 'system', - locale: navigator.language || 'en', - showExplanationBox: true, - explanationBox: true, - autoloadTimelines: true, - autoloadMore: false, - preserveSpoilers: true, - autoTranslate: false, - knownLanguages: ImmutableOrderedSet(), +// const defaultSettings = ImmutableMap({ +// onboarded: false, +// skinTone: 1, +// reduceMotion: false, +// underlineLinks: false, +// autoPlayGif: true, +// displayMedia: 'default', +// displaySpoilers: false, +// unfollowModal: true, +// boostModal: false, +// deleteModal: true, +// missingDescriptionModal: true, +// defaultPrivacy: 'public', +// defaultContentType: 'text/plain', +// themeMode: 'system', +// locale: navigator.language || 'en', +// showExplanationBox: true, +// explanationBox: true, +// autoloadTimelines: true, +// autoloadMore: false, +// preserveSpoilers: true, +// autoTranslate: false, +// knownLanguages: ImmutableOrderedSet(), - systemFont: false, - demetricator: false, +// systemFont: false, +// demetricator: false, - isDeveloper: false, +// isDeveloper: false, - chats: ImmutableMap({ - panes: ImmutableList(), - mainWindow: 'minimized', - sound: true, - }), +// chats: ImmutableMap({ +// panes: ImmutableList(), +// mainWindow: 'minimized', +// sound: true, +// }), - home: ImmutableMap({ - shows: ImmutableMap({ - reblog: true, - reply: true, - direct: false, - }), - }), +// home: ImmutableMap({ +// shows: ImmutableMap({ +// reblog: true, +// reply: true, +// direct: false, +// }), +// }), - notifications: ImmutableMap({ - quickFilter: ImmutableMap({ - active: 'all', - show: true, - advanced: false, - }), +// notifications: ImmutableMap({ +// quickFilter: ImmutableMap({ +// active: 'all', +// show: true, +// advanced: false, +// }), - sounds: ImmutableMap({ - follow: false, - follow_request: false, - favourite: false, - reblog: false, - mention: false, - poll: false, - move: false, - emoji_reaction: false, - }), - }), +// sounds: ImmutableMap({ +// follow: false, +// follow_request: false, +// favourite: false, +// reblog: false, +// mention: false, +// poll: false, +// move: false, +// emoji_reaction: false, +// }), +// }), - 'public:local': ImmutableMap({ - shows: ImmutableMap({ - reblog: false, - reply: true, - direct: false, - }), - other: ImmutableMap({ - onlyMedia: false, - }), - }), +// 'public:local': ImmutableMap({ +// shows: ImmutableMap({ +// reblog: false, +// reply: true, +// direct: false, +// }), +// other: ImmutableMap({ +// onlyMedia: false, +// }), +// }), - public: ImmutableMap({ - shows: ImmutableMap({ - reblog: true, - reply: true, - direct: false, - }), - other: ImmutableMap({ - onlyMedia: false, - }), - }), +// public: ImmutableMap({ +// shows: ImmutableMap({ +// reblog: true, +// reply: true, +// direct: false, +// }), +// other: ImmutableMap({ +// onlyMedia: false, +// }), +// }), - direct: ImmutableMap({ - }), +// direct: ImmutableMap({ +// }), - account_timeline: ImmutableMap({ - shows: ImmutableMap({ - reblog: true, - pinned: true, - direct: false, - }), - }), +// account_timeline: ImmutableMap({ +// shows: ImmutableMap({ +// reblog: true, +// pinned: true, +// direct: false, +// }), +// }), - trends: ImmutableMap({ - show: true, - }), +// trends: ImmutableMap({ +// show: true, +// }), - remote_timeline: ImmutableMap({ - pinnedHosts: ImmutableList(), - }), -}); - -const getSettings = createSelector([ - (state: RootState) => state.plfe.get('defaultSettings'), - (state: RootState) => state.settings, -], (plFeSettings, settings) => defaultSettings.mergeDeep(plFeSettings).mergeDeep(settings)); +// remote_timeline: ImmutableMap({ +// pinnedHosts: ImmutableList(), +// }), +// }); interface SettingChangeAction { type: typeof SETTING_CHANGE; @@ -171,13 +163,13 @@ const saveSettings = (opts?: SettingOpts) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - const state = getState(); - if (getSettings(state).getIn(['saved'])) return; + const { userSettings, userSettingsSaving } = useSettingsStore.getState(); + if (userSettings.saved) return; - const data = state.settings.delete('saved').toJS(); + const { saved, ...data } = userSettings; dispatch(updateSettingsStore(data)).then(() => { - dispatch({ type: SETTING_SAVE }); + userSettingsSaving(); if (opts?.showAlert) { toast.success(saveSuccessMessage); @@ -216,23 +208,18 @@ const updateSettingsStore = (settings: any) => } }; -const getLocale = (state: RootState, fallback = 'en') => { - const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-'); +const getLocale = (fallback = 'en') => { + const localeWithVariant = useSettingsStore.getState().settings.locale.replace('_', '-'); const locale = localeWithVariant.split('-')[0]; return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback; }; type SettingsAction = | SettingChangeAction - | { type: typeof SETTING_SAVE } export { SETTING_CHANGE, - SETTING_SAVE, - SETTINGS_UPDATE, FE_NAME, - defaultSettings, - getSettings, changeSettingImmediate, changeSetting, saveSettings, diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 5be8c1341..b20023637 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -1,4 +1,5 @@ import { useModalsStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { isLoggedIn } from 'pl-fe/utils/auth'; import { shouldHaveCard } from 'pl-fe/utils/status'; @@ -6,7 +7,6 @@ import { getClient } from '../api'; import { setComposeToStatus } from './compose'; import { importFetchedStatus, importFetchedStatuses } from './importer'; -import { getSettings } from './settings'; import { deleteFromTimelines } from './timelines'; import type { CreateStatusParams, Status as BaseStatus } from 'pl-api'; @@ -114,7 +114,7 @@ const fetchStatus = (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_FETCH_REQUEST, statusId }); - const params = intl && getSettings(getState()).get('autoTranslate') ? { + const params = intl && useSettingsStore.getState().settings.autoTranslate ? { language: intl.locale, } : undefined; @@ -159,7 +159,7 @@ const fetchContext = (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: CONTEXT_FETCH_REQUEST, statusId }); - const params = intl && getSettings(getState()).get('autoTranslate') ? { + const params = intl && useSettingsStore.getState().settings.autoTranslate ? { language: intl.locale, } : undefined; diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 0d7a8b76a..b2e1db7f7 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -1,6 +1,7 @@ import { Map as ImmutableMap } from 'immutable'; -import { getLocale, getSettings } from 'pl-fe/actions/settings'; +import { getLocale } from 'pl-fe/actions/settings'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { shouldFilter } from 'pl-fe/utils/timelines'; import { getClient } from '../api'; @@ -31,12 +32,12 @@ const processTimelineUpdate = (timeline: string, status: BaseStatus) => const ownStatus = status.account?.id === me; const hasPendingStatuses = !getState().pending_statuses.isEmpty(); - const columnSettings = getSettings(getState()).get(timeline, ImmutableMap()); + const columnSettings = useSettingsStore.getState().settings.timelines[timeline]; const shouldSkipQueue = shouldFilter({ in_reply_to_id: status.in_reply_to_id, visibility: status.visibility, reblog_id: status.reblog?.id || null, - }, columnSettings as any); + }, columnSettings); if (ownStatus && hasPendingStatuses) { // WebSockets push statuses without the Idempotency-Key, @@ -183,7 +184,7 @@ const fetchHomeTimeline = (expand = false, done = noOp) => const state = getState(); const params: HomeTimelineParams = {}; - if (getSettings(state).get('autoTranslate')) params.language = getLocale(state); + if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); if (expand && state.timelines.get('home')?.isLoading) return; @@ -198,7 +199,7 @@ const fetchPublicTimeline = ({ onlyMedia, local, instance }: Record const timelineId = `${instance ? 'remote' : 'public'}${local ? ':local' : ''}${onlyMedia ? ':media' : ''}${instance ? `:${instance}` : ''}`; const params: PublicTimelineParams = { only_media: onlyMedia, local: instance ? false : local, instance }; - if (getSettings(state).get('autoTranslate')) params.language = getLocale(state); + if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); if (expand && state.timelines.get(timelineId)?.isLoading) return; @@ -213,7 +214,7 @@ const fetchBubbleTimeline = ({ onlyMedia }: Record = {}, expand = f const timelineId = `bubble${onlyMedia ? ':media' : ''}`; const params: PublicTimelineParams = { only_media: onlyMedia }; - if (getSettings(state).get('autoTranslate')) params.language = getLocale(state); + if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); if (expand && state.timelines.get(timelineId)?.isLoading) return; @@ -229,7 +230,7 @@ const fetchAccountTimeline = (accountId: string, { exclude_replies, pinned, only const params: GetAccountStatusesParams = { exclude_replies, pinned, only_media, limit }; if (pinned || only_media) params.with_muted = true; - if (getSettings(state).get('autoTranslate')) params.language = getLocale(state); + if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); if (expand && state.timelines.get(timelineId)?.isLoading) return; @@ -244,7 +245,7 @@ const fetchListTimeline = (listId: string, expand = false, done = noOp) => const timelineId = `list:${listId}`; const params: ListTimelineParams = {}; - if (getSettings(state).get('autoTranslate')) params.language = getLocale(state); + if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); if (expand && state.timelines.get(timelineId)?.isLoading) return; @@ -260,7 +261,7 @@ const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record = { if (expand && state.timelines.get(timelineId)?.isLoading) return; - if (getSettings(state).get('autoTranslate')) params.language = getLocale(state); + if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.hashtagTimeline(hashtag, params); diff --git a/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts b/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts index 976db0d4a..c60297266 100644 --- a/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts @@ -4,7 +4,7 @@ import { updateConversations } from 'pl-fe/actions/conversations'; import { fetchFilters } from 'pl-fe/actions/filters'; import { MARKER_FETCH_SUCCESS } from 'pl-fe/actions/markers'; import { updateNotificationsQueue } from 'pl-fe/actions/notifications'; -import { getLocale, getSettings } from 'pl-fe/actions/settings'; +import { getLocale } from 'pl-fe/actions/settings'; import { updateStatus } from 'pl-fe/actions/statuses'; import { deleteFromTimelines, processTimelineUpdate } from 'pl-fe/actions/timelines'; import { useStatContext } from 'pl-fe/contexts/stat-context'; @@ -14,6 +14,7 @@ import { selectEntity } from 'pl-fe/entity-store/selectors'; import { useAppDispatch, useLoggedIn } from 'pl-fe/hooks'; import messages from 'pl-fe/messages'; import { queryClient } from 'pl-fe/queries/client'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { getUnreadChatsCount, updateChatListItem } from 'pl-fe/utils/chats'; import { play, soundCache } from 'pl-fe/utils/sounds'; @@ -101,6 +102,7 @@ const useUserStream = () => { const { isLoggedIn } = useLoggedIn(); const dispatch = useAppDispatch(); const statContext = useStatContext(); + const { settings } = useSettingsStore(); const listener = useCallback((event: StreamingEvent) => { switch (event.event) { @@ -114,20 +116,17 @@ const useUserStream = () => { dispatch(deleteFromTimelines(event.payload)); break; case 'notification': - dispatch((dispatch, getState) => { - const locale = getLocale(getState()); - messages[locale]().then(messages => { - dispatch( - updateNotificationsQueue( - event.payload, - messages, - locale, - window.location.pathname, - ), - ); - }).catch(error => { - console.error(error); - }); + messages[getLocale()]().then(messages => { + dispatch( + updateNotificationsQueue( + event.payload, + messages, + getLocale(), + window.location.pathname, + ), + ); + }).catch(error => { + console.error(error); }); break; case 'conversation': @@ -141,13 +140,12 @@ const useUserStream = () => { const chat = event.payload; const me = getState().me; const messageOwned = chat.last_message?.account_id === me; - const settings = getSettings(getState()); // Don't update own messages from streaming if (!messageOwned) { updateChatListItem(chat); - if (settings.getIn(['chats', 'sound'])) { + if (settings.chats.sound) { play(soundCache.chat); } diff --git a/packages/pl-fe/src/components/sidebar-menu.tsx b/packages/pl-fe/src/components/sidebar-menu.tsx index d0d9f31cd..54b841c4e 100644 --- a/packages/pl-fe/src/components/sidebar-menu.tsx +++ b/packages/pl-fe/src/components/sidebar-menu.tsx @@ -5,7 +5,6 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Link, NavLink } from 'react-router-dom'; import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth'; -import { getSettings } from 'pl-fe/actions/settings'; import { useAccount } from 'pl-fe/api/hooks'; import Account from 'pl-fe/components/account'; import { Stack, Divider, HStack, Icon, Text } from 'pl-fe/components/ui'; @@ -13,6 +12,7 @@ import ProfileStats from 'pl-fe/features/ui/components/profile-stats'; import { useAppDispatch, useAppSelector, useFeatures, useInstance, useRegistrationStatus } from 'pl-fe/hooks'; import { makeGetOtherAccounts } from 'pl-fe/selectors'; import { useUiStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import sourceCode from 'pl-fe/utils/code'; import type { List as ImmutableList } from 'immutable'; @@ -86,7 +86,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { const me = useAppSelector((state) => state.me); const { account } = useAccount(me || undefined); const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); - const settings = useAppSelector((state) => getSettings(state)); + const { settings } = useSettingsStore(); const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); const draftCount = useAppSelector((state) => state.draft_statuses.size); @@ -343,7 +343,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { /> )} - {settings.get('isDeveloper') && ( + {settings.isDeveloper && ( = ({ account }) => { const { account: ownAccount } = useOwnAccount(); const { follow } = useFollow(); const { openModal } = useModalsStore(); + const { settings } = useSettingsStore(); const { software } = useAppSelector((state) => state.auth.client.features.version); @@ -221,19 +222,17 @@ const Header: React.FC = ({ account }) => { }; const onRemoveFromFollowers = () => { - dispatch((_, getState) => { - const unfollowModal = getSettings(getState()).get('unfollowModal'); - if (unfollowModal) { - openModal('CONFIRM', { - heading: @{account.acct} }} />, - message: @{account.acct} }} />, - confirm: intl.formatMessage(messages.removeFromFollowersConfirm), - onConfirm: () => dispatch(removeFromFollowers(account.id)), - }); - } else { - dispatch(removeFromFollowers(account.id)); - } - }); + const unfollowModal = settings.unfollowModal; + if (unfollowModal) { + openModal('CONFIRM', { + heading: @{account.acct} }} />, + message: @{account.acct} }} />, + confirm: intl.formatMessage(messages.removeFromFollowersConfirm), + onConfirm: () => dispatch(removeFromFollowers(account.id)), + }); + } else { + dispatch(removeFromFollowers(account.id)); + } }; const onSearch = () => { diff --git a/packages/pl-fe/src/features/compose/components/language-dropdown.tsx b/packages/pl-fe/src/features/compose/components/language-dropdown.tsx index aa084bbf1..cb5b633ea 100644 --- a/packages/pl-fe/src/features/compose/components/language-dropdown.tsx +++ b/packages/pl-fe/src/features/compose/components/language-dropdown.tsx @@ -1,24 +1,19 @@ import clsx from 'clsx'; 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 { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'pl-fe/actions/compose'; import DropdownMenu from 'pl-fe/components/dropdown-menu'; import { Button, Icon, Input } from 'pl-fe/components/ui'; import { type Language, languages as languagesObject } from 'pl-fe/features/preferences'; -import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'pl-fe/hooks'; +import { useAppDispatch, useCompose, useFeatures, useSettings } from 'pl-fe/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 getFrequentlyUsedLanguages = (languageCounters: Record) => ( + Object.keys(languageCounters) + .toSorted((a, b) => languageCounters[a] - languageCounters[b]) + .toReversed() +); const languages = Object.entries(languagesObject) as Array<[Language, string]>; @@ -39,7 +34,8 @@ const getLanguageDropdown = (composeId: string): React.FC => const intl = useIntl(); const features = useFeatures(); const dispatch = useAppDispatch(); - const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages); + const settings = useSettings(); + const frequentlyUsedLanguages = useMemo(() => getFrequentlyUsedLanguages(settings.frequentlyUsedLanguages), [settings.frequentlyUsedLanguages]); const node = useRef(null); const focusedItem = useRef(null); diff --git a/packages/pl-fe/src/features/developers/index.tsx b/packages/pl-fe/src/features/developers/index.tsx index 16b27b49f..0d436c683 100644 --- a/packages/pl-fe/src/features/developers/index.tsx +++ b/packages/pl-fe/src/features/developers/index.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import { getSettings } from 'pl-fe/actions/settings'; -import { useAppSelector } from 'pl-fe/hooks'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import DevelopersChallenge from './developers-challenge'; import DevelopersMenu from './developers-menu'; const Developers: React.FC = () => { - const isDeveloper = useAppSelector((state) => getSettings(state).get('isDeveloper')); + const { isDeveloper } = useSettingsStore().settings; return isDeveloper ? : ; }; diff --git a/packages/pl-fe/src/features/developers/settings-store.tsx b/packages/pl-fe/src/features/developers/settings-store.tsx index fbe0f0f26..75f3f3fe4 100644 --- a/packages/pl-fe/src/features/developers/settings-store.tsx +++ b/packages/pl-fe/src/features/developers/settings-store.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { SETTINGS_UPDATE, changeSetting, updateSettingsStore } from 'pl-fe/actions/settings'; +import { changeSetting, updateSettingsStore } from 'pl-fe/actions/settings'; import List, { ListItem } from 'pl-fe/components/list'; import { CardHeader, @@ -14,7 +14,8 @@ import { Textarea, } from 'pl-fe/components/ui'; import SettingToggle from 'pl-fe/features/notifications/components/setting-toggle'; -import { useAppSelector, useAppDispatch, useSettings } from 'pl-fe/hooks'; +import { useAppDispatch } from 'pl-fe/hooks'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import toast from 'pl-fe/toast'; const isJSONValid = (text: any): boolean => { @@ -35,10 +36,9 @@ const messages = defineMessages({ const SettingsStore: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const settings = useSettings(); - const settingsStore = useAppSelector(state => state.settings); + const { settings, userSettings, loadUserSettings } = useSettingsStore(); - const [rawJSON, setRawJSON] = useState(JSON.stringify(settingsStore, null, 2)); + const [rawJSON, setRawJSON] = useState(JSON.stringify(userSettings, null, 2)); const [jsonValid, setJsonValid] = useState(true); const [isLoading, setLoading] = useState(false); @@ -57,7 +57,7 @@ const SettingsStore: React.FC = () => { setLoading(true); dispatch(updateSettingsStore(settings)).then(() => { - dispatch({ type: SETTINGS_UPDATE, settings }); + loadUserSettings(settings); setLoading(false); }).catch(error => { toast.showAlertForError(error); @@ -66,9 +66,9 @@ const SettingsStore: React.FC = () => { }; useEffect(() => { - setRawJSON(JSON.stringify(settingsStore, null, 2)); + setRawJSON(JSON.stringify(userSettings, null, 2)); setJsonValid(true); - }, [settingsStore]); + }, [userSettings]); return ( diff --git a/packages/pl-fe/src/features/directory/components/account-card.tsx b/packages/pl-fe/src/features/directory/components/account-card.tsx index 8d04b1377..768e60096 100644 --- a/packages/pl-fe/src/features/directory/components/account-card.tsx +++ b/packages/pl-fe/src/features/directory/components/account-card.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import { getSettings } from 'pl-fe/actions/settings'; import { useAccount } from 'pl-fe/api/hooks'; import Account from 'pl-fe/components/account'; import Badge from 'pl-fe/components/badge'; @@ -12,6 +11,7 @@ import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; import { Avatar, Stack, Text } from 'pl-fe/components/ui'; import ActionButton from 'pl-fe/features/ui/components/action-button'; import { useAppSelector } from 'pl-fe/hooks'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { shortNumberFormat } from 'pl-fe/utils/numbers'; interface IAccountCard { @@ -21,7 +21,7 @@ interface IAccountCard { const AccountCard: React.FC = ({ id }) => { const me = useAppSelector((state) => state.me); const { account } = useAccount(id); - const autoPlayGif = useAppSelector((state) => getSettings(state).get('autoPlayGif')); + const { autoPlayGif } = useSettingsStore().settings; if (!account) return null; diff --git a/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx b/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx index fd63c5f24..d1320d70d 100644 --- a/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx +++ b/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx @@ -3,10 +3,10 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { setComposeToStatus } from 'pl-fe/actions/compose'; import { cancelDraftStatus } from 'pl-fe/actions/draft-statuses'; -import { getSettings } from 'pl-fe/actions/settings'; import { Button, HStack } from 'pl-fe/components/ui'; import { useAppDispatch } from 'pl-fe/hooks'; import { useModalsStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import type { Status as StatusEntity } from 'pl-fe/normalizers'; import type { DraftStatus } from 'pl-fe/reducers/draft-statuses'; @@ -26,12 +26,13 @@ const DraftStatusActionBar: React.FC = ({ source, status const intl = useIntl(); const { openModal } = useModalsStore(); + const { settings } = useSettingsStore(); const dispatch = useAppDispatch(); const handleCancelClick = () => { dispatch((_, getState) => { - const deleteModal = getSettings(getState()).get('deleteModal'); + const deleteModal = settings.deleteModal; if (!deleteModal) { dispatch(cancelDraftStatus(source.draft_id)); } else { diff --git a/packages/pl-fe/src/features/emoji/components/emoji-picker-dropdown.tsx b/packages/pl-fe/src/features/emoji/components/emoji-picker-dropdown.tsx index eae9250c5..cd9f59068 100644 --- a/packages/pl-fe/src/features/emoji/components/emoji-picker-dropdown.tsx +++ b/packages/pl-fe/src/features/emoji/components/emoji-picker-dropdown.tsx @@ -1,11 +1,10 @@ -import { Map as ImmutableMap } from 'immutable'; -import React, { useEffect, useState, useLayoutEffect, Suspense } from 'react'; +import React, { useEffect, useState, useLayoutEffect, Suspense, useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { createSelector } from 'reselect'; import { chooseEmoji } from 'pl-fe/actions/emojis'; import { changeSetting } from 'pl-fe/actions/settings'; -import { useAppDispatch, useAppSelector, useTheme } from 'pl-fe/hooks'; +import { useAppDispatch, useAppSelector, useSettings, useTheme } from 'pl-fe/hooks'; import { RootState } from 'pl-fe/store'; import { buildCustomEmojis } from '../../emoji'; @@ -71,15 +70,11 @@ const DEFAULTS = [ 'ok_hand', ]; -const getFrequentlyUsedEmojis = createSelector([ - (state: RootState) => state.settings.get('frequentlyUsedEmojis', ImmutableMap()), -], (emojiCounters: ImmutableMap) => { - let emojis = emojiCounters - .keySeq() - .sort((a, b) => emojiCounters.get(a)! - emojiCounters.get(b)!) - .reverse() - .slice(0, perLine * lines) - .toArray(); +const getFrequentlyUsedEmojis = (emojiCounters: Record) => { + let emojis = Object.keys(emojiCounters) + .toSorted((a, b) => emojiCounters[a] - emojiCounters[b]) + .toReversed() + .slice(0, perLine * lines); if (emojis.length < DEFAULTS.length) { const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji)); @@ -87,7 +82,7 @@ const getFrequentlyUsedEmojis = createSelector([ } return emojis; -}); +}; const getCustomEmojis = createSelector([ (state: RootState) => state.custom_emojis, @@ -132,7 +127,9 @@ const EmojiPickerDropdown: React.FC = ({ const theme = useTheme(); const customEmojis = useAppSelector((state) => getCustomEmojis(state)); - const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state)); + + const settings = useSettings(); + const frequentlyUsedEmojis = useMemo(() => getFrequentlyUsedEmojis(settings.frequentlyUsedEmojis), [settings.frequentlyUsedEmojis]); const handlePick = (emoji: any) => { setVisible(false); @@ -238,6 +235,5 @@ const EmojiPickerDropdown: React.FC = ({ export { messages, type IEmojiPickerDropdown, - getFrequentlyUsedEmojis, EmojiPickerDropdown as default, }; diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index 33beeb5cb..a5312b537 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -4,7 +4,6 @@ import { Link, useHistory } from 'react-router-dom'; import { mentionCompose } from 'pl-fe/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'pl-fe/actions/interactions'; -import { getSettings } from 'pl-fe/actions/settings'; import { toggleStatusMediaHidden } from 'pl-fe/actions/statuses'; import Icon from 'pl-fe/components/icon'; import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; @@ -15,6 +14,7 @@ import { HotKeys } from 'pl-fe/features/ui/components/hotkeys'; import { useAppDispatch, useAppSelector, useInstance, useLoggedIn } from 'pl-fe/hooks'; import { makeGetNotification } from 'pl-fe/selectors'; import { useModalsStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { NotificationType } from 'pl-fe/utils/notification'; import type { Notification as BaseNotification } from 'pl-api'; @@ -196,6 +196,7 @@ const Notification: React.FC = (props) => { const { me } = useLoggedIn(); const { openModal } = useModalsStore(); + const { settings } = useSettingsStore(); const notification = useAppSelector((state) => getNotification(state, props.notification)); const history = useHistory(); @@ -252,23 +253,21 @@ const Notification: React.FC = (props) => { const handleHotkeyBoost = useCallback((e?: KeyboardEvent) => { if (status && typeof status === 'object') { - dispatch((_, getState) => { - const boostModal = getSettings(getState()).get('boostModal'); - if (status.reblogged) { - dispatch(unreblog(status)); + const boostModal = settings.boostModal; + if (status.reblogged) { + dispatch(unreblog(status)); + } else { + if (e?.shiftKey || !boostModal) { + dispatch(reblog(status)); } else { - if (e?.shiftKey || !boostModal) { - dispatch(reblog(status)); - } else { - openModal('BOOST', { - statusId: status.id, - onReblog: (status) => { - dispatch(reblog(status)); - }, - }); - } + openModal('BOOST', { + statusId: status.id, + onReblog: (status) => { + dispatch(reblog(status)); + }, + }); } - }); + } } }, [status]); diff --git a/packages/pl-fe/src/features/pl-fe-config/components/site-preview.tsx b/packages/pl-fe/src/features/pl-fe-config/components/site-preview.tsx index 96994dd54..ffd4e40de 100644 --- a/packages/pl-fe/src/features/pl-fe-config/components/site-preview.tsx +++ b/packages/pl-fe/src/features/pl-fe-config/components/site-preview.tsx @@ -2,10 +2,10 @@ import clsx from 'clsx'; import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; -import { defaultSettings } from 'pl-fe/actions/settings'; import BackgroundShapes from 'pl-fe/features/ui/components/background-shapes'; import { useSystemTheme } from 'pl-fe/hooks'; import { normalizePlFeConfig } from 'pl-fe/normalizers'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { generateThemeCss } from 'pl-fe/utils/theme'; interface ISitePreview { @@ -16,9 +16,9 @@ interface ISitePreview { /** Renders a preview of the website's style with the configuration applied. */ const SitePreview: React.FC = ({ plFe }) => { const plFeConfig = useMemo(() => normalizePlFeConfig(plFe), [plFe]); - const settings = defaultSettings.mergeDeep(plFeConfig.defaultSettings); + const { defaultSettings } = useSettingsStore(); - const userTheme = settings.get('themeMode'); + const userTheme = defaultSettings.themeMode; const systemTheme = useSystemTheme(); const dark = ['dark', 'black'].includes(userTheme as string) || (userTheme === 'system' && systemTheme === 'black'); diff --git a/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status-action-bar.tsx b/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status-action-bar.tsx index 53b5e7cab..e6ecf4949 100644 --- a/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status-action-bar.tsx +++ b/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status-action-bar.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { cancelScheduledStatus } from 'pl-fe/actions/scheduled-statuses'; -import { getSettings } from 'pl-fe/actions/settings'; import { Button, HStack } from 'pl-fe/components/ui'; import { useAppDispatch } from 'pl-fe/hooks'; import { useModalsStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import type { Status as StatusEntity } from 'pl-fe/normalizers'; @@ -25,11 +25,12 @@ const ScheduledStatusActionBar: React.FC = ({ status const dispatch = useAppDispatch(); const { openModal } = useModalsStore(); + const { settings } = useSettingsStore(); const handleCancelClick = () => { dispatch((_, getState) => { - const deleteModal = getSettings(getState()).get('deleteModal'); + const deleteModal = settings.deleteModal; if (!deleteModal) { dispatch(cancelScheduledStatus(status.id)); } else { diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index 84ba46c5d..7929499a8 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -8,7 +8,6 @@ import { useHistory } from 'react-router-dom'; import { type ComposeReplyAction, mentionCompose, replyCompose } from 'pl-fe/actions/compose'; import { reblog, toggleFavourite, unreblog } from 'pl-fe/actions/interactions'; -import { getSettings } from 'pl-fe/actions/settings'; import { toggleStatusMediaHidden } from 'pl-fe/actions/statuses'; import ScrollableList from 'pl-fe/components/scrollable-list'; import StatusActionBar from 'pl-fe/components/status-action-bar'; @@ -20,6 +19,7 @@ import PendingStatus from 'pl-fe/features/ui/components/pending-status'; import { useAppDispatch, useAppSelector } from 'pl-fe/hooks'; import { RootState } from 'pl-fe/store'; import { useModalsStore } from 'pl-fe/stores'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { textForScreenReader } from 'pl-fe/utils/status'; import DetailedStatus from './detailed-status'; @@ -93,6 +93,7 @@ const Thread: React.FC = ({ const intl = useIntl(); const { openModal } = useModalsStore(); + const { settings } = useSettingsStore(); const { ancestorsIds, descendantsIds } = useAppSelector((state) => { let ancestorsIds = ImmutableOrderedSet(); @@ -136,7 +137,7 @@ const Thread: React.FC = ({ const handleReblogClick = (status: SelectedStatus, e?: React.MouseEvent) => { dispatch((_, getState) => { - const boostModal = getSettings(getState()).get('boostModal'); + const boostModal = settings.boostModal; if (status.reblogged) { dispatch(unreblog(status)); } else { diff --git a/packages/pl-fe/src/hooks/useLocale.ts b/packages/pl-fe/src/hooks/useLocale.ts index 78216a019..da57cae62 100644 --- a/packages/pl-fe/src/hooks/useLocale.ts +++ b/packages/pl-fe/src/hooks/useLocale.ts @@ -1,7 +1,5 @@ import { getLocale } from 'pl-fe/actions/settings'; -import { useAppSelector } from './useAppSelector'; - /** Locales which should be presented in right-to-left. */ const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he']; @@ -12,7 +10,8 @@ interface UseLocaleResult { /** Get valid locale from settings. */ const useLocale = (fallback = 'en'): UseLocaleResult => { - const locale = useAppSelector((state) => getLocale(state, fallback)); + // TODO use useSettingsStore directly + const locale = getLocale(fallback); const direction: 'ltr' | 'rtl' = RTL_LOCALES.includes(locale) diff --git a/packages/pl-fe/src/hooks/useSettings.ts b/packages/pl-fe/src/hooks/useSettings.ts index 149e4b585..b05b569e8 100644 --- a/packages/pl-fe/src/hooks/useSettings.ts +++ b/packages/pl-fe/src/hooks/useSettings.ts @@ -1,14 +1,6 @@ -import { useMemo } from 'react'; - -import { getSettings } from 'pl-fe/actions/settings'; -import { settingsSchema } from 'pl-fe/schemas/pl-fe/settings'; - -import { useAppSelector } from './useAppSelector'; +import { useSettingsStore } from 'pl-fe/stores/settings'; /** Get the user settings from the store */ -const useSettings = () => { - const data = useAppSelector((state) => getSettings(state)); - return useMemo(() => settingsSchema.parse(data.toJS()), [data]); -}; +const useSettings = () => useSettingsStore().settings; export { useSettings }; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index dbdfcc41f..2b4d0f49b 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -37,7 +37,7 @@ import push_notifications from './push-notifications'; import scheduled_statuses from './scheduled-statuses'; import search from './search'; import security from './security'; -import settings from './settings'; +// import settings from './settings'; import status_lists from './status-lists'; import statuses from './statuses'; import suggestions from './suggestions'; @@ -81,7 +81,7 @@ const reducers = { scheduled_statuses, search, security, - settings, + // settings, status_lists, statuses, suggestions, diff --git a/packages/pl-fe/src/reducers/pl-fe.ts b/packages/pl-fe/src/reducers/pl-fe.ts index ad087cc8e..509a560ef 100644 --- a/packages/pl-fe/src/reducers/pl-fe.ts +++ b/packages/pl-fe/src/reducers/pl-fe.ts @@ -46,7 +46,7 @@ const persistPlFeConfig = (plFeConfig: ImmutableMap, host: string) } }; -const importPlFeConfig = (state: ImmutableMap, plFeConfig: ImmutableMap, host: string) => { +const importPlFeConfig = (plFeConfig: ImmutableMap, host: string) => { persistPlFeConfig(plFeConfig, host); return plFeConfig; }; @@ -58,7 +58,7 @@ const plfe = (state = initialState, action: Record) => { case PLFE_CONFIG_REMEMBER_SUCCESS: return fromJS(action.plFeConfig); case PLFE_CONFIG_REQUEST_SUCCESS: - return importPlFeConfig(state, fromJS(action.plFeConfig) as ImmutableMap, action.host); + return importPlFeConfig(fromJS(action.plFeConfig) as ImmutableMap, action.host); case PLFE_CONFIG_REQUEST_FAIL: return fallbackState.mergeDeep(state); case ADMIN_CONFIG_UPDATE_SUCCESS: diff --git a/packages/pl-fe/src/reducers/settings.ts b/packages/pl-fe/src/reducers/settings.ts index 1910a49f0..888f9866f 100644 --- a/packages/pl-fe/src/reducers/settings.ts +++ b/packages/pl-fe/src/reducers/settings.ts @@ -1,61 +1,31 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { produce } from 'immer'; import { AnyAction } from 'redux'; -import { LANGUAGE_USE } from 'pl-fe/actions/languages'; -import { ME_FETCH_SUCCESS } from 'pl-fe/actions/me'; +import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings'; -import { EMOJI_CHOOSE } from '../actions/emojis'; import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; import { SEARCH_FILTER_SET } from '../actions/search'; -import { - SETTING_CHANGE, - SETTING_SAVE, - SETTINGS_UPDATE, - FE_NAME, -} from '../actions/settings'; +import { SETTING_CHANGE } from '../actions/settings'; -import type { Emoji } from 'pl-fe/features/emoji'; -import type { APIEntity } from 'pl-fe/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 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); - const prefs = account.getIn(['settings_store', FE_NAME], ImmutableMap()); - return state.merge(prefs) as State; -}; +type State = Partial; // Default settings are in action/settings.js // // Settings should be accessed with `getSettings(getState()).getIn(...)` // instead of directly from the state. const settings = ( - state: State = ImmutableMap({ saved: true }), + state: State = settingsSchema.partial().parse({}), action: AnyAction, ): State => { switch (action.type) { - case ME_FETCH_SUCCESS: - return importSettings(state, action.me); case NOTIFICATIONS_FILTER_SET: case SEARCH_FILTER_SET: case SETTING_CHANGE: - return state - .setIn(action.path, action.value) - .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: - return ImmutableMap(fromJS(action.settings)); + return produce(state, draft => { + // @ts-ignore + draft[action.path] = action.value; + draft.saved = false; + }); default: return state; } diff --git a/packages/pl-fe/src/schemas/pl-fe/settings.ts b/packages/pl-fe/src/schemas/pl-fe/settings.ts index 65764239d..a57c1679f 100644 --- a/packages/pl-fe/src/schemas/pl-fe/settings.ts +++ b/packages/pl-fe/src/schemas/pl-fe/settings.ts @@ -77,9 +77,24 @@ const settingsSchema = z.object({ advanced: z.boolean().catch(false), show: z.boolean().catch(true), }), + sounds: z.record(z.boolean()).catch({}), }), autoTranslate: z.boolean().catch(false), knownLanguages: z.array(z.string()).catch([]), + frequentlyUsedEmojis: z.record(z.number()).catch({}), + frequentlyUsedLanguages: z.record(z.number()).catch({}), + timelines: z.record(coerceObject({ + shows: coerceObject({ + reblog: z.boolean().catch(true), + reply: z.boolean().catch(true), + direct: z.boolean().catch(false), + }), + other: coerceObject({ + onlyMedia: z.boolean().catch(false), + }), + })).catch({}), + + saved: z.boolean().catch(true), }); type Settings = z.infer; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 5f12bf473..4b42e0ea0 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -1,13 +1,13 @@ import { - Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, } from 'immutable'; import { createSelector } from 'reselect'; -import { getLocale, getSettings } from 'pl-fe/actions/settings'; +import { getLocale } from 'pl-fe/actions/settings'; import { Entities } from 'pl-fe/entity-store/entities'; +import { useSettingsStore } from 'pl-fe/stores/settings'; import { getDomain } from 'pl-fe/utils/accounts'; import { validId } from 'pl-fe/utils/auth'; import ConfigDB from 'pl-fe/utils/config-db'; @@ -137,7 +137,7 @@ const makeGetStatus = () => createSelector( getFilters, (state: RootState) => state.me, (state: RootState) => state.auth.client.features, - (state: RootState) => getLocale(state, 'en'), + (state: RootState) => getLocale('en'), ], (statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features, locale) => { @@ -333,7 +333,7 @@ const makeGetRemoteInstance = () => type ColumnQuery = { type: string; prefix?: string }; const makeGetStatusIds = () => createSelector([ - (state: RootState, { type, prefix }: ColumnQuery) => getSettings(state).get(prefix || type, ImmutableMap()), + (state: RootState, { type, prefix }: ColumnQuery) => useSettingsStore.getState().settings.timelines[prefix || type], (state: RootState, { type }: ColumnQuery) => state.timelines.get(type)?.items || ImmutableOrderedSet(), (state: RootState) => state.statuses, ], (columnSettings: any, statusIds: ImmutableOrderedSet, statuses) => diff --git a/packages/pl-fe/src/stores/settings.ts b/packages/pl-fe/src/stores/settings.ts new file mode 100644 index 000000000..018d2aa3c --- /dev/null +++ b/packages/pl-fe/src/stores/settings.ts @@ -0,0 +1,74 @@ +import { produce } from 'immer'; +import { create } from 'zustand'; + +import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings'; + +import type { Emoji } from 'pl-fe/features/emoji'; +import type { APIEntity } from 'pl-fe/types/entities'; + +const settingsSchemaPartial = settingsSchema.partial(); + +type State = { + defaultSettings: Settings; + userSettings: Partial; + + settings: Settings; + + loadDefaultSettings: (settings: APIEntity) => void; + loadUserSettings: (settings: APIEntity) => void; + userSettingsSaving: () => void; + rememberEmojiUse: (emoji: Emoji) => void; + rememberLanguageUse: (language: string) => void; +} + +const mergeSettings = (state: State) => state.settings = { ...state.defaultSettings, ...state.userSettings }; + +const useSettingsStore = create((set) => ({ + defaultSettings: settingsSchema.parse({}), + userSettings: {}, + + settings: settingsSchema.parse({}), + + loadDefaultSettings: (settings: APIEntity) => set(produce((state: State) => { + if (typeof settings !== 'object') return; + + state.defaultSettings = settingsSchema.parse(settings); + mergeSettings(state); + })), + + loadUserSettings: (settings?: APIEntity) => set(produce((state: State) => { + if (typeof settings !== 'object') return; + + state.userSettings = settingsSchemaPartial.parse(settings); + mergeSettings(state); + })), + + userSettingsSaving: () => set(produce((state: State) => { + state.userSettings.saved = true; + + mergeSettings(state); + })), + + rememberEmojiUse: (emoji: Emoji) => set(produce((state: State) => { + const settings = state.userSettings; + if (!settings.frequentlyUsedEmojis) settings.frequentlyUsedEmojis = {}; + + settings.frequentlyUsedEmojis[emoji.id] = (settings.frequentlyUsedEmojis[emoji.id] || 0) + 1; + settings.saved = false; + + mergeSettings(state); + })), + + rememberLanguageUse: (language: string) => set(produce((state: State) => { + const settings = state.userSettings; + if (!settings.frequentlyUsedLanguages) settings.frequentlyUsedLanguages = {}; + + settings.frequentlyUsedLanguages[language] = (settings.frequentlyUsedLanguages[language] || 0) + 1; + settings.saved = false; + + mergeSettings(state); + })), +})); + +export { useSettingsStore }; + diff --git a/packages/pl-fe/src/utils/timelines.ts b/packages/pl-fe/src/utils/timelines.ts index 8014b5ba5..2472ece07 100644 --- a/packages/pl-fe/src/utils/timelines.ts +++ b/packages/pl-fe/src/utils/timelines.ts @@ -1,18 +1,24 @@ -import { Map as ImmutableMap, type Collection } from 'immutable'; +import { Settings } from 'pl-fe/schemas/pl-fe/settings'; import type { Status } from 'pl-fe/normalizers'; const shouldFilter = ( status: Pick, - columnSettings: Collection, + columnSettings: Settings['timelines'][''], ) => { - const shows = ImmutableMap({ + const fallback = { + reblog: true, + reply: true, + direct: false, + }; + + const shows = { reblog: status.reblog_id !== null, reply: status.in_reply_to_id !== null, direct: status.visibility === 'direct', - }); + }; - return shows.some((value, key) => columnSettings.getIn(['shows', key]) === false && value); + return Object.entries(shows).some(([key, value]) => (columnSettings?.shows || fallback)[key as 'reblog' | 'reply' | 'direct'] === false && value); }; export { shouldFilter }; From 97d1ce2f471e3041f714f383cc6478a136f408a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 7 Oct 2024 18:37:50 +0200 Subject: [PATCH 2/3] pl-fe: Complete migration of settings store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-fe/src/actions/notifications.ts | 12 +++---- packages/pl-fe/src/actions/search.ts | 5 ++- packages/pl-fe/src/actions/settings.ts | 34 +------------------ .../developers/developers-challenge.tsx | 4 +-- .../features/developers/developers-menu.tsx | 4 +-- .../status/components/status-type-icon.tsx | 4 +-- packages/pl-fe/src/globals.ts | 4 +-- packages/pl-fe/src/reducers/compose.ts | 30 ++++++++-------- packages/pl-fe/src/reducers/index.ts | 2 -- packages/pl-fe/src/reducers/settings.test.ts | 11 ------ packages/pl-fe/src/reducers/settings.ts | 34 ------------------- packages/pl-fe/src/stores/settings.ts | 17 ++++++++++ 12 files changed, 51 insertions(+), 110 deletions(-) delete mode 100644 packages/pl-fe/src/reducers/settings.test.ts delete mode 100644 packages/pl-fe/src/reducers/settings.ts diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts index 59c30394c..41d757fff 100644 --- a/packages/pl-fe/src/actions/notifications.ts +++ b/packages/pl-fe/src/actions/notifications.ts @@ -294,14 +294,14 @@ const scrollTopNotifications = (top: boolean) => const setFilter = (filterType: FilterType, abort?: boolean) => (dispatch: AppDispatch) => { - const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType; + const settingsStore = useSettingsStore.getState(); + const activeFilter = settingsStore.settings.notifications.quickFilter.active as FilterType; - dispatch({ - type: NOTIFICATIONS_FILTER_SET, - path: ['notifications', 'quickFilter', 'active'], - value: filterType, - }); + settingsStore.changeSetting(['notifications', 'quickFilter', 'active'], filterType); + + dispatch({ type: NOTIFICATIONS_FILTER_SET }); dispatch(expandNotifications(undefined, undefined, abort)); + if (activeFilter !== filterType) dispatch(saveSettings()); }; diff --git a/packages/pl-fe/src/actions/search.ts b/packages/pl-fe/src/actions/search.ts index 57a1b6fdb..a72e253f1 100644 --- a/packages/pl-fe/src/actions/search.ts +++ b/packages/pl-fe/src/actions/search.ts @@ -1,3 +1,5 @@ +import { useSettingsStore } from 'pl-fe/stores/settings'; + import { getClient } from '../api'; import { fetchRelationships } from './accounts'; @@ -88,9 +90,10 @@ const setFilter = (value: string, filterType: SearchFilter) => (dispatch: AppDispatch) => { dispatch(submitSearch(value, filterType)); + useSettingsStore.getState().changeSetting(['search', 'filter'], filterType); + return dispatch({ type: SEARCH_FILTER_SET, - path: ['search', 'filter'], value: filterType, }); }; diff --git a/packages/pl-fe/src/actions/settings.ts b/packages/pl-fe/src/actions/settings.ts index e55f78660..0b5726f73 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -11,8 +11,6 @@ import { isLoggedIn } from 'pl-fe/utils/auth'; import type { AppDispatch, RootState } from 'pl-fe/store'; -const SETTING_CHANGE = 'SETTING_CHANGE' as const; - const FE_NAME = 'pl_fe'; const getAccount = makeGetAccount(); @@ -129,33 +127,9 @@ const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultM // }), // }); -interface SettingChangeAction { - type: typeof SETTING_CHANGE; - path: string[]; - value: any; -} - -const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) => - (dispatch: AppDispatch) => { - const action: SettingChangeAction = { - type: SETTING_CHANGE, - path, - value, - }; - - dispatch(action); - dispatch(saveSettings(opts)); - }; - const changeSetting = (path: string[], value: any, opts?: SettingOpts) => (dispatch: AppDispatch) => { - const action: SettingChangeAction = { - type: SETTING_CHANGE, - path, - value, - }; - - dispatch(action); + useSettingsStore.getState().changeSetting(path, value); return dispatch(saveSettings(opts)); }; @@ -214,16 +188,10 @@ const getLocale = (fallback = 'en') => { return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback; }; -type SettingsAction = - | SettingChangeAction - export { - SETTING_CHANGE, FE_NAME, - changeSettingImmediate, changeSetting, saveSettings, updateSettingsStore, getLocale, - type SettingsAction, }; diff --git a/packages/pl-fe/src/features/developers/developers-challenge.tsx b/packages/pl-fe/src/features/developers/developers-challenge.tsx index 8c1f53eb6..69cb18ceb 100644 --- a/packages/pl-fe/src/features/developers/developers-challenge.tsx +++ b/packages/pl-fe/src/features/developers/developers-challenge.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { changeSettingImmediate } from 'pl-fe/actions/settings'; +import { changeSetting } from 'pl-fe/actions/settings'; import { Column, Button, Form, FormActions, FormGroup, Input, Text } from 'pl-fe/components/ui'; import { useAppDispatch } from 'pl-fe/hooks'; import toast from 'pl-fe/toast'; @@ -26,7 +26,7 @@ const DevelopersChallenge = () => { const handleSubmit = () => { if (answer === 'fe-pl') { - dispatch(changeSettingImmediate(['isDeveloper'], true)); + dispatch(changeSetting(['isDeveloper'], true)); toast.success(intl.formatMessage(messages.success)); } else { toast.error(intl.formatMessage(messages.fail)); diff --git a/packages/pl-fe/src/features/developers/developers-menu.tsx b/packages/pl-fe/src/features/developers/developers-menu.tsx index 679042604..234e8fa46 100644 --- a/packages/pl-fe/src/features/developers/developers-menu.tsx +++ b/packages/pl-fe/src/features/developers/developers-menu.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; -import { changeSettingImmediate } from 'pl-fe/actions/settings'; +import { changeSetting } from 'pl-fe/actions/settings'; import { Column, Text } from 'pl-fe/components/ui'; import SvgIcon from 'pl-fe/components/ui/icon/svg-icon'; import { useAppDispatch } from 'pl-fe/hooks'; @@ -38,7 +38,7 @@ const Developers: React.FC = () => { const leaveDevelopers = (e: React.MouseEvent) => { e.preventDefault(); - dispatch(changeSettingImmediate(['isDeveloper'], false)); + dispatch(changeSetting(['isDeveloper'], false)); toast.success(intl.formatMessage(messages.leave)); history.push('/'); }; diff --git a/packages/pl-fe/src/features/status/components/status-type-icon.tsx b/packages/pl-fe/src/features/status/components/status-type-icon.tsx index b39eab085..7b67a3718 100644 --- a/packages/pl-fe/src/features/status/components/status-type-icon.tsx +++ b/packages/pl-fe/src/features/status/components/status-type-icon.tsx @@ -22,8 +22,8 @@ const STATUS_TYPE_ICONS: Record = { direct: require('@tabler/icons/outline/mail.svg'), private: require('@tabler/icons/outline/lock.svg'), mutuals_only: require('@tabler/icons/outline/users-group.svg'), - local: require('@tabler/icons/outline/affiliate.svg'), - list: require('@tabler/icons/outline/list.svg'), + local: require('@tabler/icons/outline/affiliate.svg'), + list: require('@tabler/icons/outline/list.svg'), }; const StatusTypeIcon: React.FC = ({ status }) => { diff --git a/packages/pl-fe/src/globals.ts b/packages/pl-fe/src/globals.ts index 6b3a9af99..6a14cd9be 100644 --- a/packages/pl-fe/src/globals.ts +++ b/packages/pl-fe/src/globals.ts @@ -2,7 +2,7 @@ * globals: do things through the console. * This feature is for developers. */ -import { changeSettingImmediate } from 'pl-fe/actions/settings'; +import { changeSetting } from 'pl-fe/actions/settings'; import type { Store } from 'pl-fe/store'; @@ -14,7 +14,7 @@ const createGlobals = (store: Store) => { if (![true, false].includes(bool)) { throw `Invalid option ${bool}. Must be true or false.`; } - store.dispatch(changeSettingImmediate(['isDeveloper'], bool) as any); + store.dispatch(changeSetting(['isDeveloper'], bool) as any); return bool; }, }; diff --git a/packages/pl-fe/src/reducers/compose.ts b/packages/pl-fe/src/reducers/compose.ts index 8a262974e..665f23575 100644 --- a/packages/pl-fe/src/reducers/compose.ts +++ b/packages/pl-fe/src/reducers/compose.ts @@ -62,7 +62,7 @@ import { } from '../actions/compose'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; -import { SETTING_CHANGE, FE_NAME, SettingsAction } from '../actions/settings'; +import { FE_NAME } from '../actions/settings'; import { TIMELINE_DELETE, TimelineAction } from '../actions/timelines'; import { unescapeHTML } from '../utils/html'; @@ -263,17 +263,17 @@ const importAccount = (compose: Compose, account: CredentialAccount) => { }); }; -const updateSetting = (compose: Compose, path: string[], value: string) => { - const pathString = path.join(','); - switch (pathString) { - case 'defaultPrivacy': - return compose.set('privacy', value); - case 'defaultContentType': - return compose.set('content_type', value); - default: - return compose; - } -}; +// const updateSetting = (compose: Compose, path: string[], value: string) => { +// const pathString = path.join(','); +// switch (pathString) { +// case 'defaultPrivacy': +// return compose.set('privacy', value); +// case 'defaultContentType': +// return compose.set('content_type', value); +// default: +// return compose; +// } +// }; const updateCompose = (state: State, key: string, updater: (compose: Compose) => Compose) => state.update(key, state.get('default')!, updater); @@ -282,7 +282,7 @@ const initialState: State = ImmutableMap({ default: ReducerCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }), }); -const compose = (state = initialState, action: ComposeAction | EventsAction | MeAction | SettingsAction | TimelineAction) => { +const compose = (state = initialState, action: ComposeAction | EventsAction | MeAction | TimelineAction) => { switch (action.type) { case COMPOSE_TYPE_CHANGE: return updateCompose(state, action.composeId, compose => compose.withMutations(map => { @@ -544,8 +544,8 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me case ME_FETCH_SUCCESS: case ME_PATCH_SUCCESS: return updateCompose(state, 'default', compose => importAccount(compose, action.me)); - case SETTING_CHANGE: - return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value)); + // case SETTING_CHANGE: + // return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value)); case COMPOSE_EDITOR_STATE_SET: return updateCompose(state, action.composeId, compose => compose .setIn(!compose.modified_language || compose.modified_language === compose.language ? ['editorState'] : ['editorStateMap', compose.modified_language], action.editorState as string) diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 2b4d0f49b..110fdacee 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -37,7 +37,6 @@ import push_notifications from './push-notifications'; import scheduled_statuses from './scheduled-statuses'; import search from './search'; import security from './security'; -// import settings from './settings'; import status_lists from './status-lists'; import statuses from './statuses'; import suggestions from './suggestions'; @@ -81,7 +80,6 @@ const reducers = { scheduled_statuses, search, security, - // settings, status_lists, statuses, suggestions, diff --git a/packages/pl-fe/src/reducers/settings.test.ts b/packages/pl-fe/src/reducers/settings.test.ts deleted file mode 100644 index 1623a9a96..000000000 --- a/packages/pl-fe/src/reducers/settings.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from './settings'; - -describe('settings reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({ - saved: true, - })); - }); -}); diff --git a/packages/pl-fe/src/reducers/settings.ts b/packages/pl-fe/src/reducers/settings.ts deleted file mode 100644 index 888f9866f..000000000 --- a/packages/pl-fe/src/reducers/settings.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { produce } from 'immer'; -import { AnyAction } from 'redux'; - -import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings'; - -import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; -import { SEARCH_FILTER_SET } from '../actions/search'; -import { SETTING_CHANGE } from '../actions/settings'; - -type State = Partial; - -// Default settings are in action/settings.js -// -// Settings should be accessed with `getSettings(getState()).getIn(...)` -// instead of directly from the state. -const settings = ( - state: State = settingsSchema.partial().parse({}), - action: AnyAction, -): State => { - switch (action.type) { - case NOTIFICATIONS_FILTER_SET: - case SEARCH_FILTER_SET: - case SETTING_CHANGE: - return produce(state, draft => { - // @ts-ignore - draft[action.path] = action.value; - draft.saved = false; - }); - default: - return state; - } -}; - -export { settings as default }; diff --git a/packages/pl-fe/src/stores/settings.ts b/packages/pl-fe/src/stores/settings.ts index 018d2aa3c..2b76c4da5 100644 --- a/packages/pl-fe/src/stores/settings.ts +++ b/packages/pl-fe/src/stores/settings.ts @@ -17,10 +17,21 @@ type State = { loadDefaultSettings: (settings: APIEntity) => void; loadUserSettings: (settings: APIEntity) => void; userSettingsSaving: () => void; + changeSetting: (path: string[], value: any) => void; rememberEmojiUse: (emoji: Emoji) => void; rememberLanguageUse: (language: string) => void; } +const changeSetting = (object: APIEntity, path: string[], value: any) => { + if (path.length === 1) { + object[path[0]] = value; + return; + } + + if (typeof object[path[0]] !== 'object') object[path[0]] = {}; + return changeSetting(object[path[0]], path.slice(1), value); +}; + const mergeSettings = (state: State) => state.settings = { ...state.defaultSettings, ...state.userSettings }; const useSettingsStore = create((set) => ({ @@ -49,6 +60,12 @@ const useSettingsStore = create((set) => ({ mergeSettings(state); })), + changeSetting: (path: string[], value: any) => set(produce((state: State) => { + changeSetting(state.userSettings, path, value); + + mergeSettings(state); + })), + rememberEmojiUse: (emoji: Emoji) => set(produce((state: State) => { const settings = state.userSettings; if (!settings.frequentlyUsedEmojis) settings.frequentlyUsedEmojis = {}; From 14e3df65b3e68cdb2f0ae5da22e6cbbc0cf5ea17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 7 Oct 2024 18:54:23 +0200 Subject: [PATCH 3/3] pl-fe: Remove unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-fe/src/actions/settings.ts | 113 +----------------- .../src/features/bubble-timeline/index.tsx | 2 +- .../src/features/community-timeline/index.tsx | 2 +- .../src/features/public-timeline/index.tsx | 2 +- .../src/features/remote-timeline/index.tsx | 2 +- packages/pl-fe/src/schemas/pl-fe/settings.ts | 83 +++++-------- 6 files changed, 41 insertions(+), 163 deletions(-) diff --git a/packages/pl-fe/src/actions/settings.ts b/packages/pl-fe/src/actions/settings.ts index 0b5726f73..9d2fbf957 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -23,115 +23,10 @@ type SettingOpts = { const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }); -// const defaultSettings = ImmutableMap({ -// onboarded: false, -// skinTone: 1, -// reduceMotion: false, -// underlineLinks: false, -// autoPlayGif: true, -// displayMedia: 'default', -// displaySpoilers: false, -// unfollowModal: true, -// boostModal: false, -// deleteModal: true, -// missingDescriptionModal: true, -// defaultPrivacy: 'public', -// defaultContentType: 'text/plain', -// themeMode: 'system', -// locale: navigator.language || 'en', -// showExplanationBox: true, -// explanationBox: true, -// autoloadTimelines: true, -// autoloadMore: false, -// preserveSpoilers: true, -// autoTranslate: false, -// knownLanguages: ImmutableOrderedSet(), - -// systemFont: false, -// demetricator: false, - -// isDeveloper: false, - -// chats: ImmutableMap({ -// panes: ImmutableList(), -// mainWindow: 'minimized', -// sound: true, -// }), - -// home: ImmutableMap({ -// shows: ImmutableMap({ -// reblog: true, -// reply: true, -// direct: false, -// }), -// }), - -// notifications: ImmutableMap({ -// quickFilter: ImmutableMap({ -// active: 'all', -// show: true, -// advanced: false, -// }), - -// sounds: ImmutableMap({ -// follow: false, -// follow_request: false, -// favourite: false, -// reblog: false, -// mention: false, -// poll: false, -// move: false, -// emoji_reaction: false, -// }), -// }), - -// 'public:local': ImmutableMap({ -// shows: ImmutableMap({ -// reblog: false, -// reply: true, -// direct: false, -// }), -// other: ImmutableMap({ -// onlyMedia: false, -// }), -// }), - -// public: ImmutableMap({ -// shows: ImmutableMap({ -// reblog: true, -// reply: true, -// direct: false, -// }), -// other: ImmutableMap({ -// onlyMedia: false, -// }), -// }), - -// direct: ImmutableMap({ -// }), - -// account_timeline: ImmutableMap({ -// shows: ImmutableMap({ -// reblog: true, -// pinned: true, -// direct: false, -// }), -// }), - -// trends: ImmutableMap({ -// show: true, -// }), - -// remote_timeline: ImmutableMap({ -// pinnedHosts: ImmutableList(), -// }), -// }); - -const changeSetting = (path: string[], value: any, opts?: SettingOpts) => - (dispatch: AppDispatch) => { - useSettingsStore.getState().changeSetting(path, value); - return dispatch(saveSettings(opts)); - }; +const changeSetting = (path: string[], value: any, opts?: SettingOpts) => { + useSettingsStore.getState().changeSetting(path, value); + return saveSettings(opts); +}; const saveSettings = (opts?: SettingOpts) => (dispatch: AppDispatch, getState: () => RootState) => { diff --git a/packages/pl-fe/src/features/bubble-timeline/index.tsx b/packages/pl-fe/src/features/bubble-timeline/index.tsx index f12b1f3a1..40743a433 100644 --- a/packages/pl-fe/src/features/bubble-timeline/index.tsx +++ b/packages/pl-fe/src/features/bubble-timeline/index.tsx @@ -19,7 +19,7 @@ const BubbleTimeline = () => { const theme = useTheme(); const settings = useSettings(); - const onlyMedia = settings.bubble.other.onlyMedia; + const onlyMedia = settings.timelines.bubble?.other.onlyMedia ?? false; const timelineId = 'bubble'; const isMobile = useIsMobile(); diff --git a/packages/pl-fe/src/features/community-timeline/index.tsx b/packages/pl-fe/src/features/community-timeline/index.tsx index f45f35b87..7f9d3a482 100644 --- a/packages/pl-fe/src/features/community-timeline/index.tsx +++ b/packages/pl-fe/src/features/community-timeline/index.tsx @@ -20,7 +20,7 @@ const CommunityTimeline = () => { const theme = useTheme(); const settings = useSettings(); - const onlyMedia = settings['public:local'].other.onlyMedia; + const onlyMedia = settings.timelines['public:local']?.other.onlyMedia ?? false; const timelineId = 'public:local'; const isMobile = useIsMobile(); diff --git a/packages/pl-fe/src/features/public-timeline/index.tsx b/packages/pl-fe/src/features/public-timeline/index.tsx index d717b594b..37a2a8e13 100644 --- a/packages/pl-fe/src/features/public-timeline/index.tsx +++ b/packages/pl-fe/src/features/public-timeline/index.tsx @@ -25,7 +25,7 @@ const CommunityTimeline = () => { const instance = useInstance(); const settings = useSettings(); - const onlyMedia = settings.public.other.onlyMedia; + const onlyMedia = settings.timelines.public?.other.onlyMedia ?? false; const timelineId = 'public'; const isMobile = useIsMobile(); diff --git a/packages/pl-fe/src/features/remote-timeline/index.tsx b/packages/pl-fe/src/features/remote-timeline/index.tsx index 7f12d23e7..d23f1ff3d 100644 --- a/packages/pl-fe/src/features/remote-timeline/index.tsx +++ b/packages/pl-fe/src/features/remote-timeline/index.tsx @@ -29,7 +29,7 @@ const RemoteTimeline: React.FC = ({ params }) => { const settings = useSettings(); const timelineId = 'remote'; - const onlyMedia = settings.remote.other.onlyMedia; + const onlyMedia = settings.timelines.remote?.other.onlyMedia ?? false; const pinned = settings.remote_timeline.pinnedHosts.includes(instance); const isMobile = useIsMobile(); diff --git a/packages/pl-fe/src/schemas/pl-fe/settings.ts b/packages/pl-fe/src/schemas/pl-fe/settings.ts index a57c1679f..6d2ecd9e5 100644 --- a/packages/pl-fe/src/schemas/pl-fe/settings.ts +++ b/packages/pl-fe/src/schemas/pl-fe/settings.ts @@ -16,11 +16,10 @@ const settingsSchema = z.object({ autoPlayGif: z.boolean().catch(true), displayMedia: z.enum(['default', 'hide_all', 'show_all']).catch('default'), displaySpoilers: z.boolean().catch(false), - preserveSpoilers: z.boolean().catch(false), - unfollowModal: z.boolean().catch(false), + unfollowModal: z.boolean().catch(true), boostModal: z.boolean().catch(false), deleteModal: z.boolean().catch(true), - missingDescriptionModal: z.boolean().catch(false), + missingDescriptionModal: z.boolean().catch(true), defaultPrivacy: z.enum(['public', 'unlisted', 'private', 'direct']).catch('public'), defaultContentType: z.enum(['text/plain', 'text/markdown']).catch('text/plain'), themeMode: z.enum(['system', 'light', 'dark', 'black']).catch('system'), @@ -29,60 +28,20 @@ const settingsSchema = z.object({ explanationBox: z.boolean().catch(true), autoloadTimelines: z.boolean().catch(true), autoloadMore: z.boolean().catch(true), + preserveSpoilers: z.boolean().catch(false), + autoTranslate: z.boolean().catch(false), + knownLanguages: z.array(z.string()).catch([]), + systemFont: z.boolean().catch(false), demetricator: z.boolean().catch(false), + isDeveloper: z.boolean().catch(false), - demo: z.boolean().catch(false), + chats: coerceObject({ mainWindow: z.enum(['minimized', 'open']).catch('minimized'), sound: z.boolean().catch(true), }), - home: coerceObject({ - shows: coerceObject({ - reblog: z.boolean().catch(true), - reply: z.boolean().catch(true), - }), - }), - account_timeline: coerceObject({ - shows: coerceObject({ - pinned: z.boolean().catch(true), - }), - }), - remote_timeline: coerceObject({ - pinnedHosts: z.string().array().catch([]), - }), - public: coerceObject({ - other: coerceObject({ - onlyMedia: z.boolean().catch(false), - }), - }), - 'public:local': coerceObject({ - other: coerceObject({ - onlyMedia: z.boolean().catch(false), - }), - }), - remote: coerceObject({ - other: coerceObject({ - onlyMedia: z.boolean().catch(false), - }), - }), - bubble: coerceObject({ - other: coerceObject({ - onlyMedia: z.boolean().catch(false), - }), - }), - notifications: coerceObject({ - quickFilter: coerceObject({ - active: z.string().catch('all'), - advanced: z.boolean().catch(false), - show: z.boolean().catch(true), - }), - sounds: z.record(z.boolean()).catch({}), - }), - autoTranslate: z.boolean().catch(false), - knownLanguages: z.array(z.string()).catch([]), - frequentlyUsedEmojis: z.record(z.number()).catch({}), - frequentlyUsedLanguages: z.record(z.number()).catch({}), + timelines: z.record(coerceObject({ shows: coerceObject({ reblog: z.boolean().catch(true), @@ -94,7 +53,31 @@ const settingsSchema = z.object({ }), })).catch({}), + account_timeline: coerceObject({ + shows: coerceObject({ + pinned: z.boolean().catch(true), + }), + }), + + remote_timeline: coerceObject({ + pinnedHosts: z.string().array().catch([]), + }), + + notifications: coerceObject({ + quickFilter: coerceObject({ + active: z.string().catch('all'), + advanced: z.boolean().catch(false), + show: z.boolean().catch(true), + }), + sounds: z.record(z.boolean()).catch({}), + }), + + frequentlyUsedEmojis: z.record(z.number()).catch({}), + frequentlyUsedLanguages: z.record(z.number()).catch({}), + saved: z.boolean().catch(true), + + demo: z.boolean().catch(false), }); type Settings = z.infer;