pl-fe: WIP migrate settings store to zustand

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-07 01:32:55 +02:00
parent 419877adcc
commit e4615b70f7
31 changed files with 351 additions and 306 deletions

View file

@ -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'; import type { AppDispatch, RootState } from 'pl-fe/store';
const toggleMainWindow = () => const toggleMainWindow = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (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'; const state = main === 'minimized' ? 'open' : 'minimized';
return dispatch(changeSetting(['chats', 'mainWindow'], state)); return dispatch(changeSetting(['chats', 'mainWindow'], state));
}; };

View file

@ -8,6 +8,7 @@ import { Language } from 'pl-fe/features/preferences';
import { selectAccount, selectOwnAccount, makeGetAccount } from 'pl-fe/selectors'; import { selectAccount, selectOwnAccount, makeGetAccount } from 'pl-fe/selectors';
import { tagHistory } from 'pl-fe/settings'; import { tagHistory } from 'pl-fe/settings';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import toast from 'pl-fe/toast'; import toast from 'pl-fe/toast';
import { isLoggedIn } from 'pl-fe/utils/auth'; import { isLoggedIn } from 'pl-fe/utils/auth';
@ -15,7 +16,6 @@ import { chooseEmoji } from './emojis';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { rememberLanguageUse } from './languages'; import { rememberLanguageUse } from './languages';
import { uploadFile, updateMedia } from './media'; import { uploadFile, updateMedia } from './media';
import { getSettings } from './settings';
import { createStatus } from './statuses'; import { createStatus } from './statuses';
import type { EditorState } from 'lexical'; import type { EditorState } from 'lexical';
@ -178,7 +178,7 @@ const replyCompose = (
const state = getState(); const state = getState();
const client = getClient(state); const client = getClient(state);
const { createStatusExplicitAddressing: explicitAddressing } = client.features; const { createStatusExplicitAddressing: explicitAddressing } = client.features;
const preserveSpoilers = !!getSettings(state).get('preserveSpoilers'); const preserveSpoilers = useSettingsStore.getState().settings.preserveSpoilers;
const account = selectOwnAccount(state); const account = selectOwnAccount(state);
if (!account) return; if (!account) return;
@ -321,7 +321,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c
const needsDescriptions = (state: RootState, composeId: string) => { const needsDescriptions = (state: RootState, composeId: string) => {
const media = state.compose.get(composeId)!.media_attachments; 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; const hasMissing = media.filter(item => !item.description).size > 0;

View file

@ -1,12 +1,14 @@
import { selectAccount } from 'pl-fe/selectors'; import { selectAccount } from 'pl-fe/selectors';
import { setSentryAccount } from 'pl-fe/sentry'; import { setSentryAccount } from 'pl-fe/sentry';
import KVStore from 'pl-fe/storage/kv-store'; import KVStore from 'pl-fe/storage/kv-store';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { getAuthUserId, getAuthUserUrl } from 'pl-fe/utils/auth'; import { getAuthUserId, getAuthUserUrl } from 'pl-fe/utils/auth';
import { getClient } from '../api'; import { getClient } from '../api';
import { loadCredentials } from './auth'; import { loadCredentials } from './auth';
import { importFetchedAccount } from './importer'; import { importFetchedAccount } from './importer';
import { FE_NAME } from './settings';
import type { CredentialAccount, UpdateCredentialsParams } from 'pl-api'; import type { CredentialAccount, UpdateCredentialsParams } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store'; import type { AppDispatch, RootState } from 'pl-fe/store';
@ -95,6 +97,8 @@ const fetchMeRequest = () => ({
const fetchMeSuccess = (account: CredentialAccount) => { const fetchMeSuccess = (account: CredentialAccount) => {
setSentryAccount(account); setSentryAccount(account);
useSettingsStore.getState().loadUserSettings(account.settings_store?.[FE_NAME]);
return { return {
type: ME_FETCH_SUCCESS, type: ME_FETCH_SUCCESS,
me: account, me: account,

View file

@ -6,6 +6,7 @@ import { getClient } from 'pl-fe/api';
import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification'; import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification';
import { normalizeNotification, normalizeNotifications, type Notification } from 'pl-fe/normalizers'; import { normalizeNotification, normalizeNotifications, type Notification } from 'pl-fe/normalizers';
import { getFilters, regexFromFilters } from 'pl-fe/selectors'; import { getFilters, regexFromFilters } from 'pl-fe/selectors';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { isLoggedIn } from 'pl-fe/utils/auth'; import { isLoggedIn } from 'pl-fe/utils/auth';
import { compareId } from 'pl-fe/utils/comparators'; import { compareId } from 'pl-fe/utils/comparators';
import { unescapeHTML } from 'pl-fe/utils/html'; import { unescapeHTML } from 'pl-fe/utils/html';
@ -20,7 +21,7 @@ import {
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { saveMarker } from './markers'; 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 { Account, Notification as BaseNotification, PaginatedResponse, Status } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store'; import type { AppDispatch, RootState } from 'pl-fe/store';
@ -72,7 +73,8 @@ const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array<B
const updateNotifications = (notification: BaseNotification) => const updateNotifications = (notification: BaseNotification) =>
(dispatch: AppDispatch, getState: () => RootState) => { (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) { if (notification.account) {
dispatch(importFetchedAccount(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 if (notification.type === 'chat_mention') return; // Drop chat notifications, handle them per-chat
const filters = getFilters(getState(), { contextType: 'notifications' }); 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); const status = getNotificationStatus(notification);
@ -195,7 +197,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
const state = getState(); const state = getState();
const features = state.auth.client.features; 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; const notifications = state.notifications;
if (notifications.isLoading) { if (notifications.isLoading) {
@ -291,8 +293,8 @@ const scrollTopNotifications = (top: boolean) =>
}; };
const setFilter = (filterType: FilterType, abort?: boolean) => const setFilter = (filterType: FilterType, abort?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch) => {
const activeFilter = getSettings(getState()).getIn(['notifications', 'quickFilter', 'active']); const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType;
dispatch({ dispatch({
type: NOTIFICATIONS_FILTER_SET, type: NOTIFICATIONS_FILTER_SET,

View file

@ -3,6 +3,7 @@ import { createSelector } from 'reselect';
import { getHost } from 'pl-fe/actions/instance'; import { getHost } from 'pl-fe/actions/instance';
import { normalizePlFeConfig } from 'pl-fe/normalizers'; import { normalizePlFeConfig } from 'pl-fe/normalizers';
import KVStore from 'pl-fe/storage/kv-store'; import KVStore from 'pl-fe/storage/kv-store';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { getClient, staticFetch } from '../api'; import { getClient, staticFetch } from '../api';
@ -77,6 +78,9 @@ const importPlFeConfig = (plFeConfig: APIEntity, host: string | null) => {
if (!plFeConfig.brandColor) { if (!plFeConfig.brandColor) {
plFeConfig.brandColor = '#d80482'; plFeConfig.brandColor = '#d80482';
} }
useSettingsStore.getState().loadDefaultSettings(plFeConfig?.defaultSettings);
return { return {
type: PLFE_CONFIG_REQUEST_SUCCESS, type: PLFE_CONFIG_REQUEST_SUCCESS,
plFeConfig, plFeConfig,

View file

@ -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'; import type { AppDispatch, RootState } from 'pl-fe/store';
const getPinnedHosts = (state: RootState) => { const getPinnedHosts = (state: RootState) => {
const settings = getSettings(state); const { settings } = useSettingsStore.getState();
return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableList<string> | ImmutableOrderedSet<string>; return settings.remote_timeline.pinnedHosts;
}; };
const pinHost = (host: string) => const pinHost = (host: string) =>
@ -13,7 +13,7 @@ const pinHost = (host: string) =>
const state = getState(); const state = getState();
const pinnedHosts = getPinnedHosts(state); 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) => const unpinHost = (host: string) =>
@ -21,7 +21,7 @@ const unpinHost = (host: string) =>
const state = getState(); const state = getState();
const pinnedHosts = getPinnedHosts(state); 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 { export {

View file

@ -1,20 +1,17 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
import { defineMessage } from 'react-intl'; import { defineMessage } from 'react-intl';
import { createSelector } from 'reselect';
import { patchMe } from 'pl-fe/actions/me'; import { patchMe } from 'pl-fe/actions/me';
import { getClient } from 'pl-fe/api'; import { getClient } from 'pl-fe/api';
import messages from 'pl-fe/messages'; import messages from 'pl-fe/messages';
import { makeGetAccount } from 'pl-fe/selectors'; import { makeGetAccount } from 'pl-fe/selectors';
import KVStore from 'pl-fe/storage/kv-store'; import KVStore from 'pl-fe/storage/kv-store';
import { useSettingsStore } from 'pl-fe/stores/settings';
import toast from 'pl-fe/toast'; import toast from 'pl-fe/toast';
import { isLoggedIn } from 'pl-fe/utils/auth'; import { isLoggedIn } from 'pl-fe/utils/auth';
import type { AppDispatch, RootState } from 'pl-fe/store'; import type { AppDispatch, RootState } from 'pl-fe/store';
const SETTING_CHANGE = 'SETTING_CHANGE' as const; 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 FE_NAME = 'pl_fe';
@ -28,114 +25,109 @@ type SettingOpts = {
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }); const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
const defaultSettings = ImmutableMap({ // const defaultSettings = ImmutableMap({
onboarded: false, // onboarded: false,
skinTone: 1, // skinTone: 1,
reduceMotion: false, // reduceMotion: false,
underlineLinks: false, // underlineLinks: false,
autoPlayGif: true, // autoPlayGif: true,
displayMedia: 'default', // displayMedia: 'default',
displaySpoilers: false, // displaySpoilers: false,
unfollowModal: true, // unfollowModal: true,
boostModal: false, // boostModal: false,
deleteModal: true, // deleteModal: true,
missingDescriptionModal: true, // missingDescriptionModal: true,
defaultPrivacy: 'public', // defaultPrivacy: 'public',
defaultContentType: 'text/plain', // defaultContentType: 'text/plain',
themeMode: 'system', // themeMode: 'system',
locale: navigator.language || 'en', // locale: navigator.language || 'en',
showExplanationBox: true, // showExplanationBox: true,
explanationBox: true, // explanationBox: true,
autoloadTimelines: true, // autoloadTimelines: true,
autoloadMore: false, // autoloadMore: false,
preserveSpoilers: true, // preserveSpoilers: true,
autoTranslate: false, // autoTranslate: false,
knownLanguages: ImmutableOrderedSet(), // knownLanguages: ImmutableOrderedSet(),
systemFont: false, // systemFont: false,
demetricator: false, // demetricator: false,
isDeveloper: false, // isDeveloper: false,
chats: ImmutableMap({ // chats: ImmutableMap({
panes: ImmutableList(), // panes: ImmutableList(),
mainWindow: 'minimized', // mainWindow: 'minimized',
sound: true, // sound: true,
}), // }),
home: ImmutableMap({ // home: ImmutableMap({
shows: ImmutableMap({ // shows: ImmutableMap({
reblog: true, // reblog: true,
reply: true, // reply: true,
direct: false, // direct: false,
}), // }),
}), // }),
notifications: ImmutableMap({ // notifications: ImmutableMap({
quickFilter: ImmutableMap({ // quickFilter: ImmutableMap({
active: 'all', // active: 'all',
show: true, // show: true,
advanced: false, // advanced: false,
}), // }),
sounds: ImmutableMap({ // sounds: ImmutableMap({
follow: false, // follow: false,
follow_request: false, // follow_request: false,
favourite: false, // favourite: false,
reblog: false, // reblog: false,
mention: false, // mention: false,
poll: false, // poll: false,
move: false, // move: false,
emoji_reaction: false, // emoji_reaction: false,
}), // }),
}), // }),
'public:local': ImmutableMap({ // 'public:local': ImmutableMap({
shows: ImmutableMap({ // shows: ImmutableMap({
reblog: false, // reblog: false,
reply: true, // reply: true,
direct: false, // direct: false,
}), // }),
other: ImmutableMap({ // other: ImmutableMap({
onlyMedia: false, // onlyMedia: false,
}), // }),
}), // }),
public: ImmutableMap({ // public: ImmutableMap({
shows: ImmutableMap({ // shows: ImmutableMap({
reblog: true, // reblog: true,
reply: true, // reply: true,
direct: false, // direct: false,
}), // }),
other: ImmutableMap({ // other: ImmutableMap({
onlyMedia: false, // onlyMedia: false,
}), // }),
}), // }),
direct: ImmutableMap({ // direct: ImmutableMap({
}), // }),
account_timeline: ImmutableMap({ // account_timeline: ImmutableMap({
shows: ImmutableMap({ // shows: ImmutableMap({
reblog: true, // reblog: true,
pinned: true, // pinned: true,
direct: false, // direct: false,
}), // }),
}), // }),
trends: ImmutableMap({ // trends: ImmutableMap({
show: true, // show: true,
}), // }),
remote_timeline: ImmutableMap({ // remote_timeline: ImmutableMap({
pinnedHosts: ImmutableList(), // pinnedHosts: ImmutableList(),
}), // }),
}); // });
const getSettings = createSelector([
(state: RootState) => state.plfe.get('defaultSettings'),
(state: RootState) => state.settings,
], (plFeSettings, settings) => defaultSettings.mergeDeep(plFeSettings).mergeDeep(settings));
interface SettingChangeAction { interface SettingChangeAction {
type: typeof SETTING_CHANGE; type: typeof SETTING_CHANGE;
@ -171,13 +163,13 @@ const saveSettings = (opts?: SettingOpts) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
const state = getState(); const { userSettings, userSettingsSaving } = useSettingsStore.getState();
if (getSettings(state).getIn(['saved'])) return; if (userSettings.saved) return;
const data = state.settings.delete('saved').toJS(); const { saved, ...data } = userSettings;
dispatch(updateSettingsStore(data)).then(() => { dispatch(updateSettingsStore(data)).then(() => {
dispatch({ type: SETTING_SAVE }); userSettingsSaving();
if (opts?.showAlert) { if (opts?.showAlert) {
toast.success(saveSuccessMessage); toast.success(saveSuccessMessage);
@ -216,23 +208,18 @@ const updateSettingsStore = (settings: any) =>
} }
}; };
const getLocale = (state: RootState, fallback = 'en') => { const getLocale = (fallback = 'en') => {
const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-'); const localeWithVariant = useSettingsStore.getState().settings.locale.replace('_', '-');
const locale = localeWithVariant.split('-')[0]; const locale = localeWithVariant.split('-')[0];
return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback; return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback;
}; };
type SettingsAction = type SettingsAction =
| SettingChangeAction | SettingChangeAction
| { type: typeof SETTING_SAVE }
export { export {
SETTING_CHANGE, SETTING_CHANGE,
SETTING_SAVE,
SETTINGS_UPDATE,
FE_NAME, FE_NAME,
defaultSettings,
getSettings,
changeSettingImmediate, changeSettingImmediate,
changeSetting, changeSetting,
saveSettings, saveSettings,

View file

@ -1,4 +1,5 @@
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { isLoggedIn } from 'pl-fe/utils/auth'; import { isLoggedIn } from 'pl-fe/utils/auth';
import { shouldHaveCard } from 'pl-fe/utils/status'; import { shouldHaveCard } from 'pl-fe/utils/status';
@ -6,7 +7,6 @@ import { getClient } from '../api';
import { setComposeToStatus } from './compose'; import { setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import { getSettings } from './settings';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import type { CreateStatusParams, Status as BaseStatus } from 'pl-api'; import type { CreateStatusParams, Status as BaseStatus } from 'pl-api';
@ -114,7 +114,7 @@ const fetchStatus = (statusId: string, intl?: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_FETCH_REQUEST, statusId }); dispatch({ type: STATUS_FETCH_REQUEST, statusId });
const params = intl && getSettings(getState()).get('autoTranslate') ? { const params = intl && useSettingsStore.getState().settings.autoTranslate ? {
language: intl.locale, language: intl.locale,
} : undefined; } : undefined;
@ -159,7 +159,7 @@ const fetchContext = (statusId: string, intl?: IntlShape) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CONTEXT_FETCH_REQUEST, statusId }); dispatch({ type: CONTEXT_FETCH_REQUEST, statusId });
const params = intl && getSettings(getState()).get('autoTranslate') ? { const params = intl && useSettingsStore.getState().settings.autoTranslate ? {
language: intl.locale, language: intl.locale,
} : undefined; } : undefined;

View file

@ -1,6 +1,7 @@
import { Map as ImmutableMap } from 'immutable'; 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 { shouldFilter } from 'pl-fe/utils/timelines';
import { getClient } from '../api'; import { getClient } from '../api';
@ -31,12 +32,12 @@ const processTimelineUpdate = (timeline: string, status: BaseStatus) =>
const ownStatus = status.account?.id === me; const ownStatus = status.account?.id === me;
const hasPendingStatuses = !getState().pending_statuses.isEmpty(); const hasPendingStatuses = !getState().pending_statuses.isEmpty();
const columnSettings = getSettings(getState()).get(timeline, ImmutableMap()); const columnSettings = useSettingsStore.getState().settings.timelines[timeline];
const shouldSkipQueue = shouldFilter({ const shouldSkipQueue = shouldFilter({
in_reply_to_id: status.in_reply_to_id, in_reply_to_id: status.in_reply_to_id,
visibility: status.visibility, visibility: status.visibility,
reblog_id: status.reblog?.id || null, reblog_id: status.reblog?.id || null,
}, columnSettings as any); }, columnSettings);
if (ownStatus && hasPendingStatuses) { if (ownStatus && hasPendingStatuses) {
// WebSockets push statuses without the Idempotency-Key, // WebSockets push statuses without the Idempotency-Key,
@ -183,7 +184,7 @@ const fetchHomeTimeline = (expand = false, done = noOp) =>
const state = getState(); const state = getState();
const params: HomeTimelineParams = {}; 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; if (expand && state.timelines.get('home')?.isLoading) return;
@ -198,7 +199,7 @@ const fetchPublicTimeline = ({ onlyMedia, local, instance }: Record<string, any>
const timelineId = `${instance ? 'remote' : 'public'}${local ? ':local' : ''}${onlyMedia ? ':media' : ''}${instance ? `:${instance}` : ''}`; const timelineId = `${instance ? 'remote' : 'public'}${local ? ':local' : ''}${onlyMedia ? ':media' : ''}${instance ? `:${instance}` : ''}`;
const params: PublicTimelineParams = { only_media: onlyMedia, local: instance ? false : local, 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; if (expand && state.timelines.get(timelineId)?.isLoading) return;
@ -213,7 +214,7 @@ const fetchBubbleTimeline = ({ onlyMedia }: Record<string, any> = {}, expand = f
const timelineId = `bubble${onlyMedia ? ':media' : ''}`; const timelineId = `bubble${onlyMedia ? ':media' : ''}`;
const params: PublicTimelineParams = { only_media: onlyMedia }; 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; 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 }; const params: GetAccountStatusesParams = { exclude_replies, pinned, only_media, limit };
if (pinned || only_media) params.with_muted = true; 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; 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 timelineId = `list:${listId}`;
const params: ListTimelineParams = {}; 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; if (expand && state.timelines.get(timelineId)?.isLoading) return;
@ -260,7 +261,7 @@ const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record<strin
const params: GroupTimelineParams = { only_media, limit }; const params: GroupTimelineParams = { only_media, limit };
if (only_media) params.with_muted = true; if (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; if (expand && state.timelines.get(timelineId)?.isLoading) return;
@ -282,7 +283,7 @@ const fetchHashtagTimeline = (hashtag: string, { tags }: Record<string, any> = {
if (expand && state.timelines.get(timelineId)?.isLoading) return; 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); const fn = (expand && state.timelines.get(timelineId)?.next?.()) || getClient(state).timelines.hashtagTimeline(hashtag, params);

View file

@ -4,7 +4,7 @@ import { updateConversations } from 'pl-fe/actions/conversations';
import { fetchFilters } from 'pl-fe/actions/filters'; import { fetchFilters } from 'pl-fe/actions/filters';
import { MARKER_FETCH_SUCCESS } from 'pl-fe/actions/markers'; import { MARKER_FETCH_SUCCESS } from 'pl-fe/actions/markers';
import { updateNotificationsQueue } from 'pl-fe/actions/notifications'; 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 { updateStatus } from 'pl-fe/actions/statuses';
import { deleteFromTimelines, processTimelineUpdate } from 'pl-fe/actions/timelines'; import { deleteFromTimelines, processTimelineUpdate } from 'pl-fe/actions/timelines';
import { useStatContext } from 'pl-fe/contexts/stat-context'; 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 { useAppDispatch, useLoggedIn } from 'pl-fe/hooks';
import messages from 'pl-fe/messages'; import messages from 'pl-fe/messages';
import { queryClient } from 'pl-fe/queries/client'; import { queryClient } from 'pl-fe/queries/client';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { getUnreadChatsCount, updateChatListItem } from 'pl-fe/utils/chats'; import { getUnreadChatsCount, updateChatListItem } from 'pl-fe/utils/chats';
import { play, soundCache } from 'pl-fe/utils/sounds'; import { play, soundCache } from 'pl-fe/utils/sounds';
@ -101,6 +102,7 @@ const useUserStream = () => {
const { isLoggedIn } = useLoggedIn(); const { isLoggedIn } = useLoggedIn();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const statContext = useStatContext(); const statContext = useStatContext();
const { settings } = useSettingsStore();
const listener = useCallback((event: StreamingEvent) => { const listener = useCallback((event: StreamingEvent) => {
switch (event.event) { switch (event.event) {
@ -114,20 +116,17 @@ const useUserStream = () => {
dispatch(deleteFromTimelines(event.payload)); dispatch(deleteFromTimelines(event.payload));
break; break;
case 'notification': case 'notification':
dispatch((dispatch, getState) => { messages[getLocale()]().then(messages => {
const locale = getLocale(getState()); dispatch(
messages[locale]().then(messages => { updateNotificationsQueue(
dispatch( event.payload,
updateNotificationsQueue( messages,
event.payload, getLocale(),
messages, window.location.pathname,
locale, ),
window.location.pathname, );
), }).catch(error => {
); console.error(error);
}).catch(error => {
console.error(error);
});
}); });
break; break;
case 'conversation': case 'conversation':
@ -141,13 +140,12 @@ const useUserStream = () => {
const chat = event.payload; const chat = event.payload;
const me = getState().me; const me = getState().me;
const messageOwned = chat.last_message?.account_id === me; const messageOwned = chat.last_message?.account_id === me;
const settings = getSettings(getState());
// Don't update own messages from streaming // Don't update own messages from streaming
if (!messageOwned) { if (!messageOwned) {
updateChatListItem(chat); updateChatListItem(chat);
if (settings.getIn(['chats', 'sound'])) { if (settings.chats.sound) {
play(soundCache.chat); play(soundCache.chat);
} }

View file

@ -5,7 +5,6 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth'; import { fetchOwnAccounts, logOut, switchAccount } from 'pl-fe/actions/auth';
import { getSettings } from 'pl-fe/actions/settings';
import { useAccount } from 'pl-fe/api/hooks'; import { useAccount } from 'pl-fe/api/hooks';
import Account from 'pl-fe/components/account'; import Account from 'pl-fe/components/account';
import { Stack, Divider, HStack, Icon, Text } from 'pl-fe/components/ui'; 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 { useAppDispatch, useAppSelector, useFeatures, useInstance, useRegistrationStatus } from 'pl-fe/hooks';
import { makeGetOtherAccounts } from 'pl-fe/selectors'; import { makeGetOtherAccounts } from 'pl-fe/selectors';
import { useUiStore } from 'pl-fe/stores'; import { useUiStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import sourceCode from 'pl-fe/utils/code'; import sourceCode from 'pl-fe/utils/code';
import type { List as ImmutableList } from 'immutable'; 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 me = useAppSelector((state) => state.me);
const { account } = useAccount(me || undefined); const { account } = useAccount(me || undefined);
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state)); const otherAccounts: ImmutableList<AccountEntity> = 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 followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const draftCount = useAppSelector((state) => state.draft_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 && (
<SidebarLink <SidebarLink
to='/developers' to='/developers'
icon={require('@tabler/icons/outline/code.svg')} icon={require('@tabler/icons/outline/code.svg')}

View file

@ -10,7 +10,6 @@ import { blockDomain, unblockDomain } from 'pl-fe/actions/domain-blocks';
import { initMuteModal } from 'pl-fe/actions/mutes'; import { initMuteModal } from 'pl-fe/actions/mutes';
import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports';
import { setSearchAccount } from 'pl-fe/actions/search'; import { setSearchAccount } from 'pl-fe/actions/search';
import { getSettings } from 'pl-fe/actions/settings';
import { useFollow } from 'pl-fe/api/hooks'; import { useFollow } from 'pl-fe/api/hooks';
import Badge from 'pl-fe/components/badge'; import Badge from 'pl-fe/components/badge';
import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu'; import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu';
@ -24,6 +23,7 @@ import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'pl-f
import { useChats } from 'pl-fe/queries/chats'; import { useChats } from 'pl-fe/queries/chats';
import { queryClient } from 'pl-fe/queries/client'; import { queryClient } from 'pl-fe/queries/client';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import toast from 'pl-fe/toast'; import toast from 'pl-fe/toast';
import { isDefaultHeader } from 'pl-fe/utils/accounts'; import { isDefaultHeader } from 'pl-fe/utils/accounts';
import copy from 'pl-fe/utils/copy'; import copy from 'pl-fe/utils/copy';
@ -90,6 +90,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
const { account: ownAccount } = useOwnAccount(); const { account: ownAccount } = useOwnAccount();
const { follow } = useFollow(); const { follow } = useFollow();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const { software } = useAppSelector((state) => state.auth.client.features.version); const { software } = useAppSelector((state) => state.auth.client.features.version);
@ -221,19 +222,17 @@ const Header: React.FC<IHeader> = ({ account }) => {
}; };
const onRemoveFromFollowers = () => { const onRemoveFromFollowers = () => {
dispatch((_, getState) => { const unfollowModal = settings.unfollowModal;
const unfollowModal = getSettings(getState()).get('unfollowModal'); if (unfollowModal) {
if (unfollowModal) { openModal('CONFIRM', {
openModal('CONFIRM', { heading: <FormattedMessage id='confirmations.remove_from_followers.heading' defaultMessage='Remove {name} from followers' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
heading: <FormattedMessage id='confirmations.remove_from_followers.heading' defaultMessage='Remove {name} from followers' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />, message: <FormattedMessage id='confirmations.remove_from_followers.message' defaultMessage='Are you sure you want to remove {name} from your followers?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
message: <FormattedMessage id='confirmations.remove_from_followers.message' defaultMessage='Are you sure you want to remove {name} from your followers?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />, confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
confirm: intl.formatMessage(messages.removeFromFollowersConfirm), onConfirm: () => dispatch(removeFromFollowers(account.id)),
onConfirm: () => dispatch(removeFromFollowers(account.id)), });
}); } else {
} else { dispatch(removeFromFollowers(account.id));
dispatch(removeFromFollowers(account.id)); }
}
});
}; };
const onSearch = () => { const onSearch = () => {

View file

@ -1,24 +1,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import fuzzysort from 'fuzzysort'; import fuzzysort from 'fuzzysort';
import { Map as ImmutableMap } from 'immutable';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
import { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'pl-fe/actions/compose'; import { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'pl-fe/actions/compose';
import DropdownMenu from 'pl-fe/components/dropdown-menu'; import DropdownMenu from 'pl-fe/components/dropdown-menu';
import { Button, Icon, Input } from 'pl-fe/components/ui'; import { Button, Icon, Input } from 'pl-fe/components/ui';
import { type Language, languages as languagesObject } from 'pl-fe/features/preferences'; 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([ const getFrequentlyUsedLanguages = (languageCounters: Record<string, number>) => (
state => state.settings.get('frequentlyUsedLanguages', ImmutableMap()), Object.keys(languageCounters)
], (languageCounters: ImmutableMap<Language, number>) => ( .toSorted((a, b) => languageCounters[a] - languageCounters[b])
languageCounters.keySeq() .toReversed()
.sort((a, b) => languageCounters.get(a, 0) - languageCounters.get(b, 0)) );
.reverse()
.toArray()
));
const languages = Object.entries(languagesObject) as Array<[Language, string]>; const languages = Object.entries(languagesObject) as Array<[Language, string]>;
@ -39,7 +34,8 @@ const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> =>
const intl = useIntl(); const intl = useIntl();
const features = useFeatures(); const features = useFeatures();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages); const settings = useSettings();
const frequentlyUsedLanguages = useMemo(() => getFrequentlyUsedLanguages(settings.frequentlyUsedLanguages), [settings.frequentlyUsedLanguages]);
const node = useRef<HTMLDivElement>(null); const node = useRef<HTMLDivElement>(null);
const focusedItem = useRef<HTMLButtonElement>(null); const focusedItem = useRef<HTMLButtonElement>(null);

View file

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { getSettings } from 'pl-fe/actions/settings'; import { useSettingsStore } from 'pl-fe/stores/settings';
import { useAppSelector } from 'pl-fe/hooks';
import DevelopersChallenge from './developers-challenge'; import DevelopersChallenge from './developers-challenge';
import DevelopersMenu from './developers-menu'; import DevelopersMenu from './developers-menu';
const Developers: React.FC = () => { const Developers: React.FC = () => {
const isDeveloper = useAppSelector((state) => getSettings(state).get('isDeveloper')); const { isDeveloper } = useSettingsStore().settings;
return isDeveloper ? <DevelopersMenu /> : <DevelopersChallenge />; return isDeveloper ? <DevelopersMenu /> : <DevelopersChallenge />;
}; };

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; 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 List, { ListItem } from 'pl-fe/components/list';
import { import {
CardHeader, CardHeader,
@ -14,7 +14,8 @@ import {
Textarea, Textarea,
} from 'pl-fe/components/ui'; } from 'pl-fe/components/ui';
import SettingToggle from 'pl-fe/features/notifications/components/setting-toggle'; 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'; import toast from 'pl-fe/toast';
const isJSONValid = (text: any): boolean => { const isJSONValid = (text: any): boolean => {
@ -35,10 +36,9 @@ const messages = defineMessages({
const SettingsStore: React.FC = () => { const SettingsStore: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const settings = useSettings(); const { settings, userSettings, loadUserSettings } = useSettingsStore();
const settingsStore = useAppSelector(state => state.settings);
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(settingsStore, null, 2)); const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(userSettings, null, 2));
const [jsonValid, setJsonValid] = useState(true); const [jsonValid, setJsonValid] = useState(true);
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(false);
@ -57,7 +57,7 @@ const SettingsStore: React.FC = () => {
setLoading(true); setLoading(true);
dispatch(updateSettingsStore(settings)).then(() => { dispatch(updateSettingsStore(settings)).then(() => {
dispatch({ type: SETTINGS_UPDATE, settings }); loadUserSettings(settings);
setLoading(false); setLoading(false);
}).catch(error => { }).catch(error => {
toast.showAlertForError(error); toast.showAlertForError(error);
@ -66,9 +66,9 @@ const SettingsStore: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
setRawJSON(JSON.stringify(settingsStore, null, 2)); setRawJSON(JSON.stringify(userSettings, null, 2));
setJsonValid(true); setJsonValid(true);
}, [settingsStore]); }, [userSettings]);
return ( return (
<Column label={intl.formatMessage(messages.heading)} backHref='/developers'> <Column label={intl.formatMessage(messages.heading)} backHref='/developers'>

View file

@ -2,7 +2,6 @@ import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getSettings } from 'pl-fe/actions/settings';
import { useAccount } from 'pl-fe/api/hooks'; import { useAccount } from 'pl-fe/api/hooks';
import Account from 'pl-fe/components/account'; import Account from 'pl-fe/components/account';
import Badge from 'pl-fe/components/badge'; 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 { Avatar, Stack, Text } from 'pl-fe/components/ui';
import ActionButton from 'pl-fe/features/ui/components/action-button'; import ActionButton from 'pl-fe/features/ui/components/action-button';
import { useAppSelector } from 'pl-fe/hooks'; import { useAppSelector } from 'pl-fe/hooks';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { shortNumberFormat } from 'pl-fe/utils/numbers'; import { shortNumberFormat } from 'pl-fe/utils/numbers';
interface IAccountCard { interface IAccountCard {
@ -21,7 +21,7 @@ interface IAccountCard {
const AccountCard: React.FC<IAccountCard> = ({ id }) => { const AccountCard: React.FC<IAccountCard> = ({ id }) => {
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const { account } = useAccount(id); const { account } = useAccount(id);
const autoPlayGif = useAppSelector((state) => getSettings(state).get('autoPlayGif')); const { autoPlayGif } = useSettingsStore().settings;
if (!account) return null; if (!account) return null;

View file

@ -3,10 +3,10 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { setComposeToStatus } from 'pl-fe/actions/compose'; import { setComposeToStatus } from 'pl-fe/actions/compose';
import { cancelDraftStatus } from 'pl-fe/actions/draft-statuses'; import { cancelDraftStatus } from 'pl-fe/actions/draft-statuses';
import { getSettings } from 'pl-fe/actions/settings';
import { Button, HStack } from 'pl-fe/components/ui'; import { Button, HStack } from 'pl-fe/components/ui';
import { useAppDispatch } from 'pl-fe/hooks'; import { useAppDispatch } from 'pl-fe/hooks';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import type { Status as StatusEntity } from 'pl-fe/normalizers'; import type { Status as StatusEntity } from 'pl-fe/normalizers';
import type { DraftStatus } from 'pl-fe/reducers/draft-statuses'; import type { DraftStatus } from 'pl-fe/reducers/draft-statuses';
@ -26,12 +26,13 @@ const DraftStatusActionBar: React.FC<IDraftStatusActionBar> = ({ source, status
const intl = useIntl(); const intl = useIntl();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleCancelClick = () => { const handleCancelClick = () => {
dispatch((_, getState) => { dispatch((_, getState) => {
const deleteModal = getSettings(getState()).get('deleteModal'); const deleteModal = settings.deleteModal;
if (!deleteModal) { if (!deleteModal) {
dispatch(cancelDraftStatus(source.draft_id)); dispatch(cancelDraftStatus(source.draft_id));
} else { } else {

View file

@ -1,11 +1,10 @@
import { Map as ImmutableMap } from 'immutable'; import React, { useEffect, useState, useLayoutEffect, Suspense, useMemo } from 'react';
import React, { useEffect, useState, useLayoutEffect, Suspense } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { chooseEmoji } from 'pl-fe/actions/emojis'; import { chooseEmoji } from 'pl-fe/actions/emojis';
import { changeSetting } from 'pl-fe/actions/settings'; 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 { RootState } from 'pl-fe/store';
import { buildCustomEmojis } from '../../emoji'; import { buildCustomEmojis } from '../../emoji';
@ -71,15 +70,11 @@ const DEFAULTS = [
'ok_hand', 'ok_hand',
]; ];
const getFrequentlyUsedEmojis = createSelector([ const getFrequentlyUsedEmojis = (emojiCounters: Record<string, number>) => {
(state: RootState) => state.settings.get('frequentlyUsedEmojis', ImmutableMap()), let emojis = Object.keys(emojiCounters)
], (emojiCounters: ImmutableMap<string, number>) => { .toSorted((a, b) => emojiCounters[a] - emojiCounters[b])
let emojis = emojiCounters .toReversed()
.keySeq() .slice(0, perLine * lines);
.sort((a, b) => emojiCounters.get(a)! - emojiCounters.get(b)!)
.reverse()
.slice(0, perLine * lines)
.toArray();
if (emojis.length < DEFAULTS.length) { if (emojis.length < DEFAULTS.length) {
const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji)); const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
@ -87,7 +82,7 @@ const getFrequentlyUsedEmojis = createSelector([
} }
return emojis; return emojis;
}); };
const getCustomEmojis = createSelector([ const getCustomEmojis = createSelector([
(state: RootState) => state.custom_emojis, (state: RootState) => state.custom_emojis,
@ -132,7 +127,9 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
const theme = useTheme(); const theme = useTheme();
const customEmojis = useAppSelector((state) => getCustomEmojis(state)); 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) => { const handlePick = (emoji: any) => {
setVisible(false); setVisible(false);
@ -238,6 +235,5 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
export { export {
messages, messages,
type IEmojiPickerDropdown, type IEmojiPickerDropdown,
getFrequentlyUsedEmojis,
EmojiPickerDropdown as default, EmojiPickerDropdown as default,
}; };

View file

@ -4,7 +4,6 @@ import { Link, useHistory } from 'react-router-dom';
import { mentionCompose } from 'pl-fe/actions/compose'; import { mentionCompose } from 'pl-fe/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'pl-fe/actions/interactions'; 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 { toggleStatusMediaHidden } from 'pl-fe/actions/statuses';
import Icon from 'pl-fe/components/icon'; import Icon from 'pl-fe/components/icon';
import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; 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 { useAppDispatch, useAppSelector, useInstance, useLoggedIn } from 'pl-fe/hooks';
import { makeGetNotification } from 'pl-fe/selectors'; import { makeGetNotification } from 'pl-fe/selectors';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { NotificationType } from 'pl-fe/utils/notification'; import { NotificationType } from 'pl-fe/utils/notification';
import type { Notification as BaseNotification } from 'pl-api'; import type { Notification as BaseNotification } from 'pl-api';
@ -196,6 +196,7 @@ const Notification: React.FC<INotification> = (props) => {
const { me } = useLoggedIn(); const { me } = useLoggedIn();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const notification = useAppSelector((state) => getNotification(state, props.notification)); const notification = useAppSelector((state) => getNotification(state, props.notification));
const history = useHistory(); const history = useHistory();
@ -252,23 +253,21 @@ const Notification: React.FC<INotification> = (props) => {
const handleHotkeyBoost = useCallback((e?: KeyboardEvent) => { const handleHotkeyBoost = useCallback((e?: KeyboardEvent) => {
if (status && typeof status === 'object') { if (status && typeof status === 'object') {
dispatch((_, getState) => { const boostModal = settings.boostModal;
const boostModal = getSettings(getState()).get('boostModal'); if (status.reblogged) {
if (status.reblogged) { dispatch(unreblog(status));
dispatch(unreblog(status)); } else {
if (e?.shiftKey || !boostModal) {
dispatch(reblog(status));
} else { } else {
if (e?.shiftKey || !boostModal) { openModal('BOOST', {
dispatch(reblog(status)); statusId: status.id,
} else { onReblog: (status) => {
openModal('BOOST', { dispatch(reblog(status));
statusId: status.id, },
onReblog: (status) => { });
dispatch(reblog(status));
},
});
}
} }
}); }
} }
}, [status]); }, [status]);

View file

@ -2,10 +2,10 @@ import clsx from 'clsx';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { defaultSettings } from 'pl-fe/actions/settings';
import BackgroundShapes from 'pl-fe/features/ui/components/background-shapes'; import BackgroundShapes from 'pl-fe/features/ui/components/background-shapes';
import { useSystemTheme } from 'pl-fe/hooks'; import { useSystemTheme } from 'pl-fe/hooks';
import { normalizePlFeConfig } from 'pl-fe/normalizers'; import { normalizePlFeConfig } from 'pl-fe/normalizers';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { generateThemeCss } from 'pl-fe/utils/theme'; import { generateThemeCss } from 'pl-fe/utils/theme';
interface ISitePreview { interface ISitePreview {
@ -16,9 +16,9 @@ interface ISitePreview {
/** Renders a preview of the website's style with the configuration applied. */ /** Renders a preview of the website's style with the configuration applied. */
const SitePreview: React.FC<ISitePreview> = ({ plFe }) => { const SitePreview: React.FC<ISitePreview> = ({ plFe }) => {
const plFeConfig = useMemo(() => normalizePlFeConfig(plFe), [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 systemTheme = useSystemTheme();
const dark = ['dark', 'black'].includes(userTheme as string) || (userTheme === 'system' && systemTheme === 'black'); const dark = ['dark', 'black'].includes(userTheme as string) || (userTheme === 'system' && systemTheme === 'black');

View file

@ -2,10 +2,10 @@ import React from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { cancelScheduledStatus } from 'pl-fe/actions/scheduled-statuses'; import { cancelScheduledStatus } from 'pl-fe/actions/scheduled-statuses';
import { getSettings } from 'pl-fe/actions/settings';
import { Button, HStack } from 'pl-fe/components/ui'; import { Button, HStack } from 'pl-fe/components/ui';
import { useAppDispatch } from 'pl-fe/hooks'; import { useAppDispatch } from 'pl-fe/hooks';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import type { Status as StatusEntity } from 'pl-fe/normalizers'; import type { Status as StatusEntity } from 'pl-fe/normalizers';
@ -25,11 +25,12 @@ const ScheduledStatusActionBar: React.FC<IScheduledStatusActionBar> = ({ status
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const handleCancelClick = () => { const handleCancelClick = () => {
dispatch((_, getState) => { dispatch((_, getState) => {
const deleteModal = getSettings(getState()).get('deleteModal'); const deleteModal = settings.deleteModal;
if (!deleteModal) { if (!deleteModal) {
dispatch(cancelScheduledStatus(status.id)); dispatch(cancelScheduledStatus(status.id));
} else { } else {

View file

@ -8,7 +8,6 @@ import { useHistory } from 'react-router-dom';
import { type ComposeReplyAction, mentionCompose, replyCompose } from 'pl-fe/actions/compose'; import { type ComposeReplyAction, mentionCompose, replyCompose } from 'pl-fe/actions/compose';
import { reblog, toggleFavourite, unreblog } from 'pl-fe/actions/interactions'; import { reblog, toggleFavourite, unreblog } from 'pl-fe/actions/interactions';
import { getSettings } from 'pl-fe/actions/settings';
import { toggleStatusMediaHidden } from 'pl-fe/actions/statuses'; import { toggleStatusMediaHidden } from 'pl-fe/actions/statuses';
import ScrollableList from 'pl-fe/components/scrollable-list'; import ScrollableList from 'pl-fe/components/scrollable-list';
import StatusActionBar from 'pl-fe/components/status-action-bar'; 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 { useAppDispatch, useAppSelector } from 'pl-fe/hooks';
import { RootState } from 'pl-fe/store'; import { RootState } from 'pl-fe/store';
import { useModalsStore } from 'pl-fe/stores'; import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { textForScreenReader } from 'pl-fe/utils/status'; import { textForScreenReader } from 'pl-fe/utils/status';
import DetailedStatus from './detailed-status'; import DetailedStatus from './detailed-status';
@ -93,6 +93,7 @@ const Thread: React.FC<IThread> = ({
const intl = useIntl(); const intl = useIntl();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const { ancestorsIds, descendantsIds } = useAppSelector((state) => { const { ancestorsIds, descendantsIds } = useAppSelector((state) => {
let ancestorsIds = ImmutableOrderedSet<string>(); let ancestorsIds = ImmutableOrderedSet<string>();
@ -136,7 +137,7 @@ const Thread: React.FC<IThread> = ({
const handleReblogClick = (status: SelectedStatus, e?: React.MouseEvent) => { const handleReblogClick = (status: SelectedStatus, e?: React.MouseEvent) => {
dispatch((_, getState) => { dispatch((_, getState) => {
const boostModal = getSettings(getState()).get('boostModal'); const boostModal = settings.boostModal;
if (status.reblogged) { if (status.reblogged) {
dispatch(unreblog(status)); dispatch(unreblog(status));
} else { } else {

View file

@ -1,7 +1,5 @@
import { getLocale } from 'pl-fe/actions/settings'; import { getLocale } from 'pl-fe/actions/settings';
import { useAppSelector } from './useAppSelector';
/** Locales which should be presented in right-to-left. */ /** Locales which should be presented in right-to-left. */
const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he']; const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he'];
@ -12,7 +10,8 @@ interface UseLocaleResult {
/** Get valid locale from settings. */ /** Get valid locale from settings. */
const useLocale = (fallback = 'en'): UseLocaleResult => { const useLocale = (fallback = 'en'): UseLocaleResult => {
const locale = useAppSelector((state) => getLocale(state, fallback)); // TODO use useSettingsStore directly
const locale = getLocale(fallback);
const direction: 'ltr' | 'rtl' = const direction: 'ltr' | 'rtl' =
RTL_LOCALES.includes(locale) RTL_LOCALES.includes(locale)

View file

@ -1,14 +1,6 @@
import { useMemo } from 'react'; import { useSettingsStore } from 'pl-fe/stores/settings';
import { getSettings } from 'pl-fe/actions/settings';
import { settingsSchema } from 'pl-fe/schemas/pl-fe/settings';
import { useAppSelector } from './useAppSelector';
/** Get the user settings from the store */ /** Get the user settings from the store */
const useSettings = () => { const useSettings = () => useSettingsStore().settings;
const data = useAppSelector((state) => getSettings(state));
return useMemo(() => settingsSchema.parse(data.toJS()), [data]);
};
export { useSettings }; export { useSettings };

View file

@ -37,7 +37,7 @@ import push_notifications from './push-notifications';
import scheduled_statuses from './scheduled-statuses'; import scheduled_statuses from './scheduled-statuses';
import search from './search'; import search from './search';
import security from './security'; import security from './security';
import settings from './settings'; // import settings from './settings';
import status_lists from './status-lists'; import status_lists from './status-lists';
import statuses from './statuses'; import statuses from './statuses';
import suggestions from './suggestions'; import suggestions from './suggestions';
@ -81,7 +81,7 @@ const reducers = {
scheduled_statuses, scheduled_statuses,
search, search,
security, security,
settings, // settings,
status_lists, status_lists,
statuses, statuses,
suggestions, suggestions,

View file

@ -46,7 +46,7 @@ const persistPlFeConfig = (plFeConfig: ImmutableMap<string, any>, host: string)
} }
}; };
const importPlFeConfig = (state: ImmutableMap<string, any>, plFeConfig: ImmutableMap<string, any>, host: string) => { const importPlFeConfig = (plFeConfig: ImmutableMap<string, any>, host: string) => {
persistPlFeConfig(plFeConfig, host); persistPlFeConfig(plFeConfig, host);
return plFeConfig; return plFeConfig;
}; };
@ -58,7 +58,7 @@ const plfe = (state = initialState, action: Record<string, any>) => {
case PLFE_CONFIG_REMEMBER_SUCCESS: case PLFE_CONFIG_REMEMBER_SUCCESS:
return fromJS(action.plFeConfig); return fromJS(action.plFeConfig);
case PLFE_CONFIG_REQUEST_SUCCESS: case PLFE_CONFIG_REQUEST_SUCCESS:
return importPlFeConfig(state, fromJS(action.plFeConfig) as ImmutableMap<string, any>, action.host); return importPlFeConfig(fromJS(action.plFeConfig) as ImmutableMap<string, any>, action.host);
case PLFE_CONFIG_REQUEST_FAIL: case PLFE_CONFIG_REQUEST_FAIL:
return fallbackState.mergeDeep(state); return fallbackState.mergeDeep(state);
case ADMIN_CONFIG_UPDATE_SUCCESS: case ADMIN_CONFIG_UPDATE_SUCCESS:

View file

@ -1,61 +1,31 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { produce } from 'immer';
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { LANGUAGE_USE } from 'pl-fe/actions/languages'; import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings';
import { ME_FETCH_SUCCESS } from 'pl-fe/actions/me';
import { EMOJI_CHOOSE } from '../actions/emojis';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { SEARCH_FILTER_SET } from '../actions/search'; import { SEARCH_FILTER_SET } from '../actions/search';
import { import { SETTING_CHANGE } from '../actions/settings';
SETTING_CHANGE,
SETTING_SAVE,
SETTINGS_UPDATE,
FE_NAME,
} from '../actions/settings';
import type { Emoji } from 'pl-fe/features/emoji'; type State = Partial<Settings>;
import type { APIEntity } from 'pl-fe/types/entities';
type State = ImmutableMap<string, any>;
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<string, number>(), 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 // Default settings are in action/settings.js
// //
// Settings should be accessed with `getSettings(getState()).getIn(...)` // Settings should be accessed with `getSettings(getState()).getIn(...)`
// instead of directly from the state. // instead of directly from the state.
const settings = ( const settings = (
state: State = ImmutableMap<string, any>({ saved: true }), state: State = settingsSchema.partial().parse({}),
action: AnyAction, action: AnyAction,
): State => { ): State => {
switch (action.type) { switch (action.type) {
case ME_FETCH_SUCCESS:
return importSettings(state, action.me);
case NOTIFICATIONS_FILTER_SET: case NOTIFICATIONS_FILTER_SET:
case SEARCH_FILTER_SET: case SEARCH_FILTER_SET:
case SETTING_CHANGE: case SETTING_CHANGE:
return state return produce(state, draft => {
.setIn(action.path, action.value) // @ts-ignore
.set('saved', false); draft[action.path] = action.value;
case EMOJI_CHOOSE: draft.saved = false;
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<string, any>(fromJS(action.settings));
default: default:
return state; return state;
} }

View file

@ -77,9 +77,24 @@ const settingsSchema = z.object({
advanced: z.boolean().catch(false), advanced: z.boolean().catch(false),
show: z.boolean().catch(true), show: z.boolean().catch(true),
}), }),
sounds: z.record(z.boolean()).catch({}),
}), }),
autoTranslate: z.boolean().catch(false), autoTranslate: z.boolean().catch(false),
knownLanguages: z.array(z.string()).catch([]), 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<typeof settingsSchema>; type Settings = z.infer<typeof settingsSchema>;

View file

@ -1,13 +1,13 @@
import { import {
Map as ImmutableMap,
List as ImmutableList, List as ImmutableList,
OrderedSet as ImmutableOrderedSet, OrderedSet as ImmutableOrderedSet,
Record as ImmutableRecord, Record as ImmutableRecord,
} from 'immutable'; } from 'immutable';
import { createSelector } from 'reselect'; 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 { Entities } from 'pl-fe/entity-store/entities';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { getDomain } from 'pl-fe/utils/accounts'; import { getDomain } from 'pl-fe/utils/accounts';
import { validId } from 'pl-fe/utils/auth'; import { validId } from 'pl-fe/utils/auth';
import ConfigDB from 'pl-fe/utils/config-db'; import ConfigDB from 'pl-fe/utils/config-db';
@ -137,7 +137,7 @@ const makeGetStatus = () => createSelector(
getFilters, getFilters,
(state: RootState) => state.me, (state: RootState) => state.me,
(state: RootState) => state.auth.client.features, (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) => { (statusBase, statusReblog, statusQuote, statusGroup, poll, username, filters, me, features, locale) => {
@ -333,7 +333,7 @@ const makeGetRemoteInstance = () =>
type ColumnQuery = { type: string; prefix?: string }; type ColumnQuery = { type: string; prefix?: string };
const makeGetStatusIds = () => createSelector([ 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, { type }: ColumnQuery) => state.timelines.get(type)?.items || ImmutableOrderedSet(),
(state: RootState) => state.statuses, (state: RootState) => state.statuses,
], (columnSettings: any, statusIds: ImmutableOrderedSet<string>, statuses) => ], (columnSettings: any, statusIds: ImmutableOrderedSet<string>, statuses) =>

View file

@ -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: 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<State>((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 };

View file

@ -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'; import type { Status } from 'pl-fe/normalizers';
const shouldFilter = ( const shouldFilter = (
status: Pick<Status, 'in_reply_to_id' | 'visibility' | 'reblog_id'>, status: Pick<Status, 'in_reply_to_id' | 'visibility' | 'reblog_id'>,
columnSettings: Collection<any, any>, columnSettings: Settings['timelines'][''],
) => { ) => {
const shows = ImmutableMap({ const fallback = {
reblog: true,
reply: true,
direct: false,
};
const shows = {
reblog: status.reblog_id !== null, reblog: status.reblog_id !== null,
reply: status.in_reply_to_id !== null, reply: status.in_reply_to_id !== null,
direct: status.visibility === 'direct', 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 }; export { shouldFilter };