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';
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));
};

View file

@ -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;

View file

@ -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,

View file

@ -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<B
const updateNotifications = (notification: BaseNotification) =>
(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<string, any> = {}, done: () => an
const state = getState();
const features = state.auth.client.features;
const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as FilterType;
const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType;
const notifications = state.notifications;
if (notifications.isLoading) {
@ -291,8 +293,8 @@ const scrollTopNotifications = (top: boolean) =>
};
const setFilter = (filterType: FilterType, abort?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const activeFilter = getSettings(getState()).getIn(['notifications', 'quickFilter', 'active']);
(dispatch: AppDispatch) => {
const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType;
dispatch({
type: NOTIFICATIONS_FILTER_SET,

View file

@ -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,

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';
const getPinnedHosts = (state: RootState) => {
const settings = getSettings(state);
return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableList<string> | ImmutableOrderedSet<string>;
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 {

View file

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

View file

@ -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;

View file

@ -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<string, any>
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<string, any> = {}, 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<strin
const params: GroupTimelineParams = { only_media, limit };
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;
@ -282,7 +283,7 @@ const fetchHashtagTimeline = (hashtag: string, { tags }: Record<string, any> = {
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);

View file

@ -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);
}

View file

@ -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<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 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 && (
<SidebarLink
to='/developers'
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 { initReport, ReportableEntities } from 'pl-fe/actions/reports';
import { setSearchAccount } from 'pl-fe/actions/search';
import { getSettings } from 'pl-fe/actions/settings';
import { useFollow } from 'pl-fe/api/hooks';
import Badge from 'pl-fe/components/badge';
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 { queryClient } from 'pl-fe/queries/client';
import { useModalsStore } from 'pl-fe/stores';
import { useSettingsStore } from 'pl-fe/stores/settings';
import toast from 'pl-fe/toast';
import { isDefaultHeader } from 'pl-fe/utils/accounts';
import copy from 'pl-fe/utils/copy';
@ -90,6 +90,7 @@ const Header: React.FC<IHeader> = ({ 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<IHeader> = ({ account }) => {
};
const onRemoveFromFollowers = () => {
dispatch((_, getState) => {
const unfollowModal = getSettings(getState()).get('unfollowModal');
if (unfollowModal) {
openModal('CONFIRM', {
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> }} />,
confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
onConfirm: () => dispatch(removeFromFollowers(account.id)),
});
} else {
dispatch(removeFromFollowers(account.id));
}
});
const unfollowModal = settings.unfollowModal;
if (unfollowModal) {
openModal('CONFIRM', {
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> }} />,
confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
onConfirm: () => dispatch(removeFromFollowers(account.id)),
});
} else {
dispatch(removeFromFollowers(account.id));
}
};
const onSearch = () => {

View file

@ -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<Language, number>) => (
languageCounters.keySeq()
.sort((a, b) => languageCounters.get(a, 0) - languageCounters.get(b, 0))
.reverse()
.toArray()
));
const getFrequentlyUsedLanguages = (languageCounters: Record<string, number>) => (
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<ILanguageDropdown> =>
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<HTMLDivElement>(null);
const focusedItem = useRef<HTMLButtonElement>(null);

View file

@ -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 ? <DevelopersMenu /> : <DevelopersChallenge />;
};

View file

@ -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<string>(JSON.stringify(settingsStore, null, 2));
const [rawJSON, setRawJSON] = useState<string>(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 (
<Column label={intl.formatMessage(messages.heading)} backHref='/developers'>

View file

@ -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<IAccountCard> = ({ 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;

View file

@ -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<IDraftStatusActionBar> = ({ 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 {

View file

@ -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<string, number>) => {
let emojis = emojiCounters
.keySeq()
.sort((a, b) => emojiCounters.get(a)! - emojiCounters.get(b)!)
.reverse()
.slice(0, perLine * lines)
.toArray();
const getFrequentlyUsedEmojis = (emojiCounters: Record<string, number>) => {
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<IEmojiPickerDropdown> = ({
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<IEmojiPickerDropdown> = ({
export {
messages,
type IEmojiPickerDropdown,
getFrequentlyUsedEmojis,
EmojiPickerDropdown as default,
};

View file

@ -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<INotification> = (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<INotification> = (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]);

View file

@ -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<ISitePreview> = ({ 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');

View file

@ -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<IScheduledStatusActionBar> = ({ 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 {

View file

@ -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<IThread> = ({
const intl = useIntl();
const { openModal } = useModalsStore();
const { settings } = useSettingsStore();
const { ancestorsIds, descendantsIds } = useAppSelector((state) => {
let ancestorsIds = ImmutableOrderedSet<string>();
@ -136,7 +137,7 @@ const Thread: React.FC<IThread> = ({
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 {

View file

@ -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)

View file

@ -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 };

View file

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

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);
return plFeConfig;
};
@ -58,7 +58,7 @@ const plfe = (state = initialState, action: Record<string, any>) => {
case PLFE_CONFIG_REMEMBER_SUCCESS:
return fromJS(action.plFeConfig);
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:
return fallbackState.mergeDeep(state);
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 { LANGUAGE_USE } from 'pl-fe/actions/languages';
import { ME_FETCH_SUCCESS } from 'pl-fe/actions/me';
import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings';
import { EMOJI_CHOOSE } from '../actions/emojis';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { SEARCH_FILTER_SET } from '../actions/search';
import {
SETTING_CHANGE,
SETTING_SAVE,
SETTINGS_UPDATE,
FE_NAME,
} from '../actions/settings';
import { SETTING_CHANGE } from '../actions/settings';
import type { Emoji } from 'pl-fe/features/emoji';
import type { APIEntity } from 'pl-fe/types/entities';
type State = ImmutableMap<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;
};
type State = Partial<Settings>;
// 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<string, any>({ saved: true }),
state: State = settingsSchema.partial().parse({}),
action: AnyAction,
): State => {
switch (action.type) {
case ME_FETCH_SUCCESS:
return importSettings(state, action.me);
case NOTIFICATIONS_FILTER_SET:
case SEARCH_FILTER_SET:
case SETTING_CHANGE:
return state
.setIn(action.path, action.value)
.set('saved', false);
case EMOJI_CHOOSE:
return updateFrequentEmojis(state, action.emoji);
case LANGUAGE_USE:
return updateFrequentLanguages(state, action.language);
case SETTING_SAVE:
return state.set('saved', true);
case SETTINGS_UPDATE:
return ImmutableMap<string, any>(fromJS(action.settings));
return produce(state, draft => {
// @ts-ignore
draft[action.path] = action.value;
draft.saved = false;
});
default:
return state;
}

View file

@ -77,9 +77,24 @@ const settingsSchema = z.object({
advanced: z.boolean().catch(false),
show: z.boolean().catch(true),
}),
sounds: z.record(z.boolean()).catch({}),
}),
autoTranslate: z.boolean().catch(false),
knownLanguages: z.array(z.string()).catch([]),
frequentlyUsedEmojis: z.record(z.number()).catch({}),
frequentlyUsedLanguages: z.record(z.number()).catch({}),
timelines: z.record(coerceObject({
shows: coerceObject({
reblog: z.boolean().catch(true),
reply: z.boolean().catch(true),
direct: z.boolean().catch(false),
}),
other: coerceObject({
onlyMedia: z.boolean().catch(false),
}),
})).catch({}),
saved: z.boolean().catch(true),
});
type Settings = z.infer<typeof settingsSchema>;

View file

@ -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<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';
const shouldFilter = (
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,
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 };