diff --git a/web/src/App.tsx b/web/src/App.tsx index d7b24e3a..ff2cf919 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -70,6 +70,7 @@ const App = observer(() => { useEffect(() => { const currentLocale = workspaceStore.state.locale; + // This will trigger re-rendering of the whole app. i18n.changeLanguage(currentLocale); document.documentElement.setAttribute("lang", currentLocale); if (["ar", "fa"].includes(currentLocale)) { @@ -101,7 +102,7 @@ const App = observer(() => { return; } - workspaceStore.setPartial({ + workspaceStore.state.setPartial({ locale: userSetting.locale || workspaceStore.state.locale, appearance: userSetting.appearance || workspaceStore.state.appearance, }); diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index e6278c3c..2ac169d5 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,6 +1,6 @@ import { Divider, Option, Select } from "@mui/joy"; import { observer } from "mobx-react-lite"; -import { userStore, workspaceStore } from "@/store/v2"; +import { userStore } from "@/store/v2"; import { Visibility } from "@/types/proto/api/v1/memo_service"; import { UserSetting } from "@/types/proto/api/v1/user_service"; import { useTranslate } from "@/utils/i18n"; @@ -15,7 +15,6 @@ const PreferencesSection = observer(() => { const setting = userStore.state.userSetting as UserSetting; const handleLocaleSelectChange = async (locale: Locale) => { - workspaceStore.setPartial({ locale }); await userStore.updateUserSetting( { locale, @@ -25,7 +24,6 @@ const PreferencesSection = observer(() => { }; const handleAppearanceSelectChange = async (appearance: Appearance) => { - workspaceStore.setPartial({ appearance }); await userStore.updateUserSetting( { appearance, diff --git a/web/src/pages/AdminSignIn.tsx b/web/src/pages/AdminSignIn.tsx index ff44c220..086b37bb 100644 --- a/web/src/pages/AdminSignIn.tsx +++ b/web/src/pages/AdminSignIn.tsx @@ -13,11 +13,11 @@ const AdminSignIn = observer(() => { workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL).generalSetting || WorkspaceGeneralSetting.fromPartial({}); const handleLocaleSelectChange = (locale: Locale) => { - workspaceStore.setPartial({ locale }); + workspaceStore.state.setPartial({ locale }); }; const handleAppearanceSelectChange = (appearance: Appearance) => { - workspaceStore.setPartial({ appearance }); + workspaceStore.state.setPartial({ appearance }); }; return ( diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/SignIn.tsx index 7fd0ac05..14e7b052 100644 --- a/web/src/pages/SignIn.tsx +++ b/web/src/pages/SignIn.tsx @@ -43,11 +43,11 @@ const SignIn = observer(() => { }, []); const handleLocaleSelectChange = (locale: Locale) => { - workspaceStore.setPartial({ locale }); + workspaceStore.state.setPartial({ locale }); }; const handleAppearanceSelectChange = (appearance: Appearance) => { - workspaceStore.setPartial({ appearance }); + workspaceStore.state.setPartial({ appearance }); }; const handleSignInWithIdentityProvider = async (identityProvider: IdentityProvider) => { diff --git a/web/src/pages/SignUp.tsx b/web/src/pages/SignUp.tsx index 8deddc46..28b01bcc 100644 --- a/web/src/pages/SignUp.tsx +++ b/web/src/pages/SignUp.tsx @@ -38,11 +38,11 @@ const SignUp = observer(() => { }; const handleLocaleSelectChange = (locale: Locale) => { - workspaceStore.setPartial({ locale }); + workspaceStore.state.setPartial({ locale }); }; const handleAppearanceSelectChange = (appearance: Appearance) => { - workspaceStore.setPartial({ appearance }); + workspaceStore.state.setPartial({ appearance }); }; const handleFormSubmit = (e: React.FormEvent) => { diff --git a/web/src/store/v2/dialog.ts b/web/src/store/v2/dialog.ts index bdab673b..8cc33b89 100644 --- a/web/src/store/v2/dialog.ts +++ b/web/src/store/v2/dialog.ts @@ -1,21 +1,29 @@ import { last } from "lodash-es"; -import { makeAutoObservable } from "mobx"; +import { makeAutoObservable, runInAction } from "mobx"; + +class LocalState { + stack: string[] = []; + + constructor() { + makeAutoObservable(this); + } + + setPartial(partial: Partial) { + Object.assign(this, partial); + } +} const dialogStore = (() => { - const state = makeAutoObservable<{ - stack: string[]; - }>({ - stack: [], - }); + const state = new LocalState(); const pushDialog = (name: string) => { - state.stack.push(name); + runInAction(() => state.stack.push(name)); }; - const popDialog = () => state.stack.pop(); + const popDialog = () => runInAction(() => state.stack.pop()); const removeDialog = (name: string) => { - state.stack = state.stack.filter((n) => n !== name); + runInAction(() => (state.stack = state.stack.filter((n) => n !== name))); }; const topDialog = last(state.stack); diff --git a/web/src/store/v2/user.ts b/web/src/store/v2/user.ts index c1b41def..f04ff01d 100644 --- a/web/src/store/v2/user.ts +++ b/web/src/store/v2/user.ts @@ -2,27 +2,26 @@ import { makeAutoObservable } from "mobx"; import { authServiceClient, inboxServiceClient, userServiceClient } from "@/grpcweb"; import { Inbox } from "@/types/proto/api/v1/inbox_service"; import { Shortcut, User, UserSetting } from "@/types/proto/api/v1/user_service"; +import workspaceStore from "./workspace"; -interface LocalState { - // The name of current user. Format: `users/${uid}` +class LocalState { currentUser?: string; - // userSetting is the setting of the current user. userSetting?: UserSetting; - // shortcuts is the list of shortcuts of the current user. - shortcuts: Shortcut[]; - // inboxes is the list of inboxes of the current user. - inboxes: Inbox[]; - // userMapByName is used to cache user information. - // Key is the `user.name` and value is the `User` object. - userMapByName: Record; + shortcuts: Shortcut[] = []; + inboxes: Inbox[] = []; + userMapByName: Record = {}; + + constructor() { + makeAutoObservable(this); + } + + setPartial(partial: Partial) { + Object.assign(this, partial); + } } const userStore = (() => { - const state = makeAutoObservable({ - shortcuts: [], - inboxes: [], - userMapByName: {}, - }); + const state = new LocalState(); const getOrFetchUserByName = async (name: string) => { const userMap = state.userMapByName; @@ -32,8 +31,12 @@ const userStore = (() => { const user = await userServiceClient.getUser({ name: name, }); - userMap[name] = user; - state.userMapByName = userMap; + state.setPartial({ + userMapByName: { + ...userMap, + [name]: user, + }, + }); return user; }; @@ -42,10 +45,12 @@ const userStore = (() => { user, updateMask, }); - state.userMapByName = { - ...state.userMapByName, - [updatedUser.name]: updatedUser, - }; + state.setPartial({ + userMapByName: { + ...state.userMapByName, + [updatedUser.name]: updatedUser, + }, + }); }; const updateUserSetting = async (userSetting: Partial, updateMask: string[]) => { @@ -53,7 +58,12 @@ const userStore = (() => { setting: userSetting, updateMask: updateMask, }); - state.userSetting = UserSetting.fromPartial(updatedUserSetting); + state.setPartial({ + userSetting: UserSetting.fromPartial({ + ...state.userSetting, + ...updatedUserSetting, + }), + }); }; const fetchShortcuts = async () => { @@ -62,12 +72,16 @@ const userStore = (() => { } const { shortcuts } = await userServiceClient.listShortcuts({ parent: state.currentUser }); - state.shortcuts = shortcuts; + state.setPartial({ + shortcuts, + }); }; const fetchInboxes = async () => { const { inboxes } = await inboxServiceClient.listInboxes({}); - state.inboxes = inboxes; + state.setPartial({ + inboxes, + }); }; const updateInbox = async (inbox: Partial, updateMask: string[]) => { @@ -75,7 +89,14 @@ const userStore = (() => { inbox, updateMask, }); - state.inboxes = state.inboxes.map((i) => (i.name === updatedInbox.name ? updatedInbox : i)); + state.setPartial({ + inboxes: state.inboxes.map((i) => { + if (i.name === updatedInbox.name) { + return updatedInbox; + } + return i; + }), + }); return updatedInbox; }; @@ -94,7 +115,7 @@ export const initialUserStore = async () => { try { const currentUser = await authServiceClient.getAuthStatus({}); const userSetting = await userServiceClient.getUserSetting({}); - Object.assign(userStore.state, { + userStore.state.setPartial({ currentUser: currentUser.name, userSetting: UserSetting.fromPartial({ ...userSetting, @@ -103,6 +124,10 @@ export const initialUserStore = async () => { [currentUser.name]: currentUser, }, }); + workspaceStore.state.setPartial({ + locale: userSetting.locale, + appearance: userSetting.appearance, + }); } catch { // Do nothing. } diff --git a/web/src/store/v2/workspace.ts b/web/src/store/v2/workspace.ts index a6c08f57..93e9470f 100644 --- a/web/src/store/v2/workspace.ts +++ b/web/src/store/v2/workspace.ts @@ -1,3 +1,4 @@ +import { uniqBy } from "lodash-es"; import { makeAutoObservable } from "mobx"; import { workspaceServiceClient, workspaceSettingServiceClient } from "@/grpcweb"; import { WorkspaceProfile } from "@/types/proto/api/v1/workspace_service"; @@ -6,38 +7,48 @@ import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; import { isValidateLocale } from "@/utils/i18n"; import { workspaceSettingNamePrefix } from "../v1"; -interface LocalState { - locale: string; - appearance: string; - profile: WorkspaceProfile; - settings: WorkspaceSetting[]; +class LocalState { + locale: string = "en"; + appearance: string = "system"; + profile: WorkspaceProfile = WorkspaceProfile.fromPartial({}); + settings: WorkspaceSetting[] = []; + + constructor() { + makeAutoObservable(this); + } + + setPartial(partial: Partial) { + const finalState = { + ...this, + ...partial, + }; + if (!isValidateLocale(finalState.locale)) { + finalState.locale = "en"; + } + if (!["system", "light", "dark"].includes(finalState.appearance)) { + finalState.appearance = "system"; + } + Object.assign(this, finalState); + } } const workspaceStore = (() => { - const state = makeAutoObservable({ - locale: "en", - appearance: "system", - profile: WorkspaceProfile.fromPartial({}), - settings: [], - }); + const state = new LocalState(); const generalSetting = state.settings.find((setting) => setting.name === `${workspaceSettingNamePrefix}${WorkspaceSettingKey.GENERAL}`)?.generalSetting || WorkspaceGeneralSetting.fromPartial({}); - const setPartial = (partial: Partial) => { - Object.assign(state, partial); - }; - const fetchWorkspaceSetting = async (settingKey: WorkspaceSettingKey) => { const setting = await workspaceSettingServiceClient.getWorkspaceSetting({ name: `${workspaceSettingNamePrefix}${settingKey}` }); - state.settings.push(setting); + state.setPartial({ + settings: uniqBy([setting, ...state.settings], "name"), + }); }; return { state, generalSetting, - setPartial, fetchWorkspaceSetting, }; })(); @@ -50,17 +61,9 @@ export const initialWorkspaceStore = async () => { } const workspaceGeneralSetting = workspaceStore.generalSetting; - let locale = workspaceGeneralSetting.customProfile?.locale; - if (!isValidateLocale(locale)) { - locale = "en"; - } - let appearance = workspaceGeneralSetting.customProfile?.appearance; - if (!appearance || !["system", "light", "dark"].includes(appearance)) { - appearance = "system"; - } - workspaceStore.setPartial({ - locale: locale, - appearance: appearance, + workspaceStore.state.setPartial({ + locale: workspaceGeneralSetting.customProfile?.locale, + appearance: workspaceGeneralSetting.customProfile?.appearance, profile: workspaceProfile, }); };