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..41d757fff 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,15 +293,15 @@ 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 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/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/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 9fcd7a717..9d2fbf957 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -1,21 +1,16 @@ -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'; const getAccount = makeGetAccount(); @@ -28,156 +23,22 @@ 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 getSettings = createSelector([ - (state: RootState) => state.plfe.get('defaultSettings'), - (state: RootState) => state.settings, -], (plFeSettings, settings) => defaultSettings.mergeDeep(plFeSettings).mergeDeep(settings)); - -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); - 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) => { 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,27 +77,16 @@ 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, updateSettingsStore, getLocale, - type SettingsAction, }; 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/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/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/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/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/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/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/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/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/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/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/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 dbdfcc41f..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/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.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 1910a49f0..000000000 --- a/packages/pl-fe/src/reducers/settings.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; -import { AnyAction } from 'redux'; - -import { LANGUAGE_USE } from 'pl-fe/actions/languages'; -import { ME_FETCH_SUCCESS } from 'pl-fe/actions/me'; - -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 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; -}; - -// 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 }), - 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)); - default: - return state; - } -}; - -export { settings as default }; diff --git a/packages/pl-fe/src/schemas/pl-fe/settings.ts b/packages/pl-fe/src/schemas/pl-fe/settings.ts index 65764239d..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,57 +28,56 @@ 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({ + + 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({}), + 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({}), + + saved: z.boolean().catch(true), + + demo: z.boolean().catch(false), }); 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..2b76c4da5 --- /dev/null +++ b/packages/pl-fe/src/stores/settings.ts @@ -0,0 +1,91 @@ +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; + 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) => ({ + 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); + })), + + 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 = {}; + + 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 };