From 5a2f18da69d3b004b67401d4dec8bf694bbeb459 Mon Sep 17 00:00:00 2001 From: Johnny Date: Sun, 9 Feb 2025 11:43:55 +0800 Subject: [PATCH] refactor(frontend): retire redux --- proto/gen/apidocs.swagger.yaml | 120 +++++++++++++----- web/package.json | 6 +- web/pnpm-lock.yaml | 107 +++++----------- web/src/App.tsx | 39 +++--- web/src/components/ActivityCalendar.tsx | 4 +- .../components/ChangeMemberPasswordDialog.tsx | 3 +- web/src/components/CreateShortcutDialog.tsx | 3 +- web/src/components/Dialog/BaseDialog.tsx | 37 +++--- .../HomeSidebar/ShortcutsSection.tsx | 11 +- .../components/Inbox/MemoCommentMessage.tsx | 7 +- .../components/Inbox/VersionUpdateMessage.tsx | 6 +- web/src/components/MemoEditor/index.tsx | 11 +- web/src/components/Navigation.tsx | 23 +--- web/src/components/PasswordSignInForm.tsx | 12 +- .../Settings/PreferencesSection.tsx | 16 +-- web/src/components/StatisticsView.tsx | 8 +- web/src/helpers/polyfill.ts | 15 --- web/src/hooks/useCurrentUser.ts | 5 +- web/src/layouts/CommonContextProvider.tsx | 84 ------------ web/src/main.tsx | 28 ++-- web/src/pages/AdminSignIn.tsx | 16 +-- web/src/pages/Home.tsx | 11 +- web/src/pages/Inboxes.tsx | 12 +- web/src/pages/Setting.tsx | 10 +- web/src/pages/SignIn.tsx | 16 +-- web/src/pages/SignUp.tsx | 18 +-- web/src/store/index.ts | 17 --- web/src/store/module/dialog.ts | 26 ---- web/src/store/module/index.ts | 1 - web/src/store/reducer/dialog.ts | 37 ------ web/src/store/v1/inbox.ts | 31 ----- web/src/store/v1/index.ts | 1 - web/src/store/v1/user.ts | 12 +- web/src/store/v2/dialog.ts | 32 +++++ web/src/store/v2/index.ts | 4 + web/src/store/v2/user.ts | 112 ++++++++++++++++ web/src/store/v2/workspace.ts | 68 ++++++++++ web/src/utils/i18n.ts | 5 + 38 files changed, 482 insertions(+), 492 deletions(-) delete mode 100644 web/src/helpers/polyfill.ts delete mode 100644 web/src/layouts/CommonContextProvider.tsx delete mode 100644 web/src/store/index.ts delete mode 100644 web/src/store/module/dialog.ts delete mode 100644 web/src/store/module/index.ts delete mode 100644 web/src/store/reducer/dialog.ts delete mode 100644 web/src/store/v1/inbox.ts create mode 100644 web/src/store/v2/dialog.ts create mode 100644 web/src/store/v2/index.ts create mode 100644 web/src/store/v2/user.ts create mode 100644 web/src/store/v2/workspace.ts diff --git a/proto/gen/apidocs.swagger.yaml b/proto/gen/apidocs.swagger.yaml index 812cdde3..93420686 100644 --- a/proto/gen/apidocs.swagger.yaml +++ b/proto/gen/apidocs.swagger.yaml @@ -305,7 +305,9 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: parent - description: "The parent is the owner of the memos.\r\nIf not specified or `users/-`, it will list all memos." + description: |- + The parent is the owner of the memos. + If not specified or `users/-`, it will list all memos. in: query required: false type: string @@ -316,12 +318,16 @@ paths: type: integer format: int32 - name: pageToken - description: "A page token, received from a previous `ListMemos` call.\r\nProvide this to retrieve the subsequent page." + description: |- + A page token, received from a previous `ListMemos` call. + Provide this to retrieve the subsequent page. in: query required: false type: string - name: state - description: "The state of the memos to list.\r\nDefault to `NORMAL`. Set to `ARCHIVED` to list archived memos." + description: |- + The state of the memos to list. + Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. in: query required: false type: string @@ -331,12 +337,16 @@ paths: - ARCHIVED default: STATE_UNSPECIFIED - name: sort - description: "What field to sort the results by.\r\nDefault to display_time." + description: |- + What field to sort the results by. + Default to display_time. in: query required: false type: string - name: direction - description: "The direction to sort the results by.\r\nDefault to DESC." + description: |- + The direction to sort the results by. + Default to DESC. in: query required: false type: string @@ -346,12 +356,16 @@ paths: - DESC default: DIRECTION_UNSPECIFIED - name: filter - description: "Filter is a CEL expression to filter memos.\r\nRefer to `Shortcut.filter`." + description: |- + Filter is a CEL expression to filter memos. + Refer to `Shortcut.filter`. in: query required: false type: string - name: oldFilter - description: "[Deprecated] Old filter contains some specific conditions to filter memos.\r\nFormat: \"creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']\"" + description: |- + [Deprecated] Old filter contains some specific conditions to filter memos. + Format: "creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']" in: query required: false type: string @@ -396,7 +410,9 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: id - description: "The id of the reaction.\r\nRefer to the `Reaction.id`." + description: |- + The id of the reaction. + Refer to the `Reaction.id`. in: path required: true type: integer @@ -810,13 +826,17 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: memo.name - description: "The name of the memo.\r\nFormat: memos/{memo}, memo is the user defined id or uuid." + description: |- + The name of the memo. + Format: memos/{memo}, memo is the user defined id or uuid. in: path required: true type: string pattern: memos/[^/]+ - name: memo - description: "The memo to update.\r\nThe `name` field is required." + description: |- + The memo to update. + The `name` field is required. in: body required: true schema: @@ -826,7 +846,9 @@ paths: $ref: '#/definitions/v1State' creator: type: string - title: "The name of the creator.\r\nFormat: users/{user}" + title: |- + The name of the creator. + Format: users/{user} createTime: type: string format: date-time @@ -874,7 +896,9 @@ paths: readOnly: true parent: type: string - title: "The name of the parent memo.\r\nFormat: memos/{id}" + title: |- + The name of the parent memo. + Format: memos/{id} readOnly: true snippet: type: string @@ -883,7 +907,9 @@ paths: location: $ref: '#/definitions/apiv1Location' description: The location of the memo. - title: "The memo to update.\r\nThe `name` field is required." + title: |- + The memo to update. + The `name` field is required. required: - memo tags: @@ -1440,7 +1466,9 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: parent - description: "The parent is the owner of the memos.\r\nIf not specified or `users/-`, it will list all memos." + description: |- + The parent is the owner of the memos. + If not specified or `users/-`, it will list all memos. in: path required: true type: string @@ -1452,12 +1480,16 @@ paths: type: integer format: int32 - name: pageToken - description: "A page token, received from a previous `ListMemos` call.\r\nProvide this to retrieve the subsequent page." + description: |- + A page token, received from a previous `ListMemos` call. + Provide this to retrieve the subsequent page. in: query required: false type: string - name: state - description: "The state of the memos to list.\r\nDefault to `NORMAL`. Set to `ARCHIVED` to list archived memos." + description: |- + The state of the memos to list. + Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. in: query required: false type: string @@ -1467,12 +1499,16 @@ paths: - ARCHIVED default: STATE_UNSPECIFIED - name: sort - description: "What field to sort the results by.\r\nDefault to display_time." + description: |- + What field to sort the results by. + Default to display_time. in: query required: false type: string - name: direction - description: "The direction to sort the results by.\r\nDefault to DESC." + description: |- + The direction to sort the results by. + Default to DESC. in: query required: false type: string @@ -1482,12 +1518,16 @@ paths: - DESC default: DIRECTION_UNSPECIFIED - name: filter - description: "Filter is a CEL expression to filter memos.\r\nRefer to `Shortcut.filter`." + description: |- + Filter is a CEL expression to filter memos. + Refer to `Shortcut.filter`. in: query required: false type: string - name: oldFilter - description: "[Deprecated] Old filter contains some specific conditions to filter memos.\r\nFormat: \"creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']\"" + description: |- + [Deprecated] Old filter contains some specific conditions to filter memos. + Format: "creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']" in: query required: false type: string @@ -1625,7 +1665,9 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: parent - description: "The parent, who owns the tags.\r\nFormat: memos/{id}. Use \"memos/-\" to delete all tags." + description: |- + The parent, who owns the tags. + Format: memos/{id}. Use "memos/-" to delete all tags. in: path required: true type: string @@ -1656,7 +1698,9 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: parent - description: "The parent, who owns the tags.\r\nFormat: memos/{id}. Use \"memos/-\" to rename all tags." + description: |- + The parent, who owns the tags. + Format: memos/{id}. Use "memos/-" to rename all tags. in: path required: true type: string @@ -1771,7 +1815,9 @@ paths: $ref: '#/definitions/googlerpcStatus' parameters: - name: user.name - description: "The name of the user.\r\nFormat: users/{id}, id is the system generated auto-incremented id." + description: |- + The name of the user. + Format: users/{id}, id is the system generated auto-incremented id. in: path required: true type: string @@ -2106,13 +2152,17 @@ definitions: properties: name: type: string - description: "The name of the memo.\r\nFormat: memos/{memo}, memo is the user defined id or uuid." + description: |- + The name of the memo. + Format: memos/{memo}, memo is the user defined id or uuid. readOnly: true state: $ref: '#/definitions/v1State' creator: type: string - title: "The name of the creator.\r\nFormat: users/{user}" + title: |- + The name of the creator. + Format: users/{user} createTime: type: string format: date-time @@ -2160,7 +2210,9 @@ definitions: readOnly: true parent: type: string - title: "The name of the parent memo.\r\nFormat: memos/{id}" + title: |- + The name of the parent memo. + Format: memos/{id} readOnly: true snippet: type: string @@ -2738,7 +2790,9 @@ definitions: $ref: '#/definitions/apiv1Memo' nextPageToken: type: string - description: "A token, which can be sent as `page_token` to retrieve the next page.\r\nIf this field is omitted, there are no subsequent pages." + description: |- + A token, which can be sent as `page_token` to retrieve the next page. + If this field is omitted, there are no subsequent pages. v1ListNode: type: object properties: @@ -3156,7 +3210,9 @@ definitions: properties: name: type: string - description: "The name of the user.\r\nFormat: users/{id}, id is the system generated auto-incremented id." + description: |- + The name of the user. + Format: users/{id}, id is the system generated auto-incremented id. role: $ref: '#/definitions/UserRole' username: @@ -3205,7 +3261,9 @@ definitions: items: type: string format: date-time - description: "The timestamps when the memos were displayed.\r\nWe should return raw data to the client, and let the client format the data with the user's timezone." + description: |- + The timestamps when the memos were displayed. + We should return raw data to the client, and let the client format the data with the user's timezone. memoTypeStats: $ref: '#/definitions/UserStatsMemoTypeStats' description: The stats of memo types. @@ -3214,7 +3272,9 @@ definitions: additionalProperties: type: integer format: int32 - title: "The count of tags.\r\nFormat: \"tag1\": 1, \"tag2\": 2" + title: |- + The count of tags. + Format: "tag1": 1, "tag2": 2 v1Visibility: type: string enum: diff --git a/web/package.json b/web/package.json index 18a5712b..d00f7b1c 100644 --- a/web/package.json +++ b/web/package.json @@ -18,7 +18,6 @@ "@matejmazur/react-katex": "^3.1.3", "@mui/joy": "5.0.0-beta.51", "@radix-ui/react-popover": "^1.1.5", - "@reduxjs/toolkit": "^2.5.0", "@usememos/mui": "0.0.1-alpha.26", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -32,6 +31,8 @@ "lodash-es": "^4.17.21", "lucide-react": "^0.453.0", "mermaid": "^11.4.1", + "mobx": "^6.13.6", + "mobx-react-lite": "^4.1.0", "react": "^18.3.1", "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", @@ -39,7 +40,6 @@ "react-hot-toast": "^2.5.1", "react-i18next": "^15.4.0", "react-leaflet": "^4.2.1", - "react-redux": "^9.2.0", "react-router-dom": "^7.1.1", "react-simple-pull-to-refresh": "^1.3.3", "react-use": "^17.6.0", @@ -82,4 +82,4 @@ "typescript": "^5.7.3", "vite": "^6.0.6" } -} +} \ No newline at end of file diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 79234791..31229d09 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.5 version: 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reduxjs/toolkit': - specifier: ^2.5.0 - version: 2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1) '@usememos/mui': specifier: 0.0.1-alpha.26 version: 0.0.1-alpha.26(lucide-react@0.453.0(react@18.3.1))(postcss@8.4.49)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwind-merge@2.6.0)(tailwindcss@3.4.17) @@ -77,6 +74,12 @@ importers: mermaid: specifier: ^11.4.1 version: 11.4.1 + mobx: + specifier: ^6.13.6 + version: 6.13.6 + mobx-react-lite: + specifier: ^4.1.0 + version: 4.1.0(mobx@6.13.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -98,9 +101,6 @@ importers: react-leaflet: specifier: ^4.2.1 version: 4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-redux: - specifier: ^9.2.0 - version: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) react-router-dom: specifier: ^7.1.1 version: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1095,17 +1095,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@reduxjs/toolkit@2.5.0': - resolution: {integrity: sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@rollup/rollup-android-arm-eabi@4.29.1': resolution: {integrity: sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==} cpu: [arm] @@ -1368,9 +1357,6 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -2678,6 +2664,22 @@ packages: mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mobx-react-lite@4.1.0: + resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==} + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx@6.13.6: + resolution: {integrity: sha512-r19KNV0uBN4b+ER8Z0gA4y+MzDYIQ2SvOmn3fUrqPnWXdQfakd9yfbPBDBF/p5I+bd3N5Rk1fHONIvMay+bJGA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2981,18 +2983,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -3073,14 +3063,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - reflect.getprototypeof@1.0.9: resolution: {integrity: sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==} engines: {node: '>= 0.4'} @@ -3092,9 +3074,6 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -4460,16 +4439,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@reduxjs/toolkit@2.5.0(react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': - dependencies: - immer: 10.1.1 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 18.3.1 - react-redux: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) - '@rollup/rollup-android-arm-eabi@4.29.1': optional: true @@ -4723,8 +4692,6 @@ snapshots: '@types/trusted-types@2.0.7': optional: true - '@types/use-sync-external-store@0.0.6': {} - '@types/uuid@10.0.0': {} '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': @@ -5914,7 +5881,8 @@ snapshots: image-size@0.5.5: optional: true - immer@10.1.1: {} + immer@10.1.1: + optional: true import-fresh@3.3.0: dependencies: @@ -6275,6 +6243,16 @@ snapshots: pkg-types: 1.3.0 ufo: 1.5.4 + mobx-react-lite@4.1.0(mobx@6.13.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + mobx: 6.13.6 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + mobx@6.13.6: {} + ms@2.1.3: {} mz@2.7.0: @@ -6586,15 +6564,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux@9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 18.3.1 - use-sync-external-store: 1.4.0(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.18 - redux: 5.0.1 - react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@18.3.1): @@ -6681,12 +6650,6 @@ snapshots: dependencies: picomatch: 2.3.1 - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - reflect.getprototypeof@1.0.9: dependencies: call-bind: 1.0.8 @@ -6707,8 +6670,6 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 - reselect@5.1.1: {} - resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} diff --git a/web/src/App.tsx b/web/src/App.tsx index 27166f13..d7b24e3a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,28 +1,19 @@ import { useColorScheme } from "@mui/joy"; +import { observer } from "mobx-react-lite"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Outlet } from "react-router-dom"; -import useLocalStorage from "react-use/lib/useLocalStorage"; import { getSystemColorScheme } from "./helpers/utils"; import useNavigateTo from "./hooks/useNavigateTo"; -import { useCommonContext } from "./layouts/CommonContextProvider"; -import { useUserStore, useWorkspaceSettingStore } from "./store/v1"; -import { WorkspaceGeneralSetting, WorkspaceSettingKey } from "./types/proto/store/workspace_setting"; +import { userStore, workspaceStore } from "./store/v2"; -const App = () => { +const App = observer(() => { const { i18n } = useTranslation(); const navigateTo = useNavigateTo(); const { mode, setMode } = useColorScheme(); - const workspaceSettingStore = useWorkspaceSettingStore(); - const userStore = useUserStore(); - const commonContext = useCommonContext(); - const [, setLocale] = useLocalStorage("locale", "en"); - const [, setAppearance] = useLocalStorage("appearance", "system"); - const workspaceProfile = commonContext.profile; - const userSetting = userStore.userSetting; - - const workspaceGeneralSetting = - workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL).generalSetting || WorkspaceGeneralSetting.fromPartial({}); + const workspaceProfile = workspaceStore.state.profile; + const userSetting = userStore.state.userSetting; + const workspaceGeneralSetting = workspaceStore.generalSetting; // Redirect to sign up page if no instance owner. useEffect(() => { @@ -78,7 +69,7 @@ const App = () => { }, [workspaceGeneralSetting.customProfile]); useEffect(() => { - const currentLocale = commonContext.locale; + const currentLocale = workspaceStore.state.locale; i18n.changeLanguage(currentLocale); document.documentElement.setAttribute("lang", currentLocale); if (["ar", "fa"].includes(currentLocale)) { @@ -86,17 +77,15 @@ const App = () => { } else { document.documentElement.setAttribute("dir", "ltr"); } - setLocale(currentLocale); - }, [commonContext.locale]); + }, [workspaceStore.state.locale]); useEffect(() => { - let currentAppearance = commonContext.appearance as Appearance; + let currentAppearance = workspaceStore.state.appearance as Appearance; if (currentAppearance === "system") { currentAppearance = getSystemColorScheme(); } setMode(currentAppearance); - setAppearance(currentAppearance); - }, [commonContext.appearance]); + }, [workspaceStore.state.appearance]); useEffect(() => { const root = document.documentElement; @@ -112,11 +101,13 @@ const App = () => { return; } - commonContext.setLocale(userSetting.locale); - commonContext.setAppearance(userSetting.appearance); + workspaceStore.setPartial({ + locale: userSetting.locale || workspaceStore.state.locale, + appearance: userSetting.appearance || workspaceStore.state.appearance, + }); }, [userSetting?.locale, userSetting?.appearance]); return ; -}; +}); export default App; diff --git a/web/src/components/ActivityCalendar.tsx b/web/src/components/ActivityCalendar.tsx index d435da91..ac2cdc23 100644 --- a/web/src/components/ActivityCalendar.tsx +++ b/web/src/components/ActivityCalendar.tsx @@ -78,7 +78,7 @@ const ActivityCalendar = (props: Props) => { return (
{item.day}
@@ -101,7 +101,7 @@ const ActivityCalendar = (props: Props) => {
= (props: Props) => { const { user, destroy } = props; const t = useTranslate(); - const userStore = useUserStore(); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index 0a81c2f8..dcafcfc7 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -6,7 +6,7 @@ import { toast } from "react-hot-toast"; import { userServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; import useLoading from "@/hooks/useLoading"; -import { useUserStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Shortcut } from "@/types/proto/api/v1/user_service"; import { useTranslate } from "@/utils/i18n"; import { generateUUID } from "@/utils/uuid"; @@ -20,7 +20,6 @@ const CreateShortcutDialog: React.FC = (props: Props) => { const { destroy } = props; const t = useTranslate(); const user = useCurrentUser(); - const userStore = useUserStore(); const [shortcut, setShortcut] = useState(Shortcut.fromPartial({ ...props.shortcut })); const requestState = useLoading(false); const isCreating = !props.shortcut; diff --git a/web/src/components/Dialog/BaseDialog.tsx b/web/src/components/Dialog/BaseDialog.tsx index a09d112e..5b9ede17 100644 --- a/web/src/components/Dialog/BaseDialog.tsx +++ b/web/src/components/Dialog/BaseDialog.tsx @@ -1,10 +1,8 @@ import { CssVarsProvider } from "@mui/joy"; +import { observer } from "mobx-react-lite"; import { useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; -import { Provider } from "react-redux"; -import CommonContextProvider from "@/layouts/CommonContextProvider"; -import store from "@/store"; -import { useDialogStore } from "@/store/module"; +import dialogStore from "@/store/v2/dialog"; import theme from "@/theme"; import { cn } from "@/utils"; import "@/less/base-dialog.less"; @@ -19,17 +17,16 @@ interface Props extends DialogConfig, DialogProps { children: React.ReactNode; } -const BaseDialog: React.FC = (props: Props) => { +const BaseDialog = observer((props: Props) => { const { children, className, clickSpaceDestroy, dialogName, destroy } = props; - const dialogStore = useDialogStore(); const dialogContainerRef = useRef(null); - const dialogIndex = dialogStore.state.dialogStack.findIndex((item) => item === dialogName); + const dialogIndex = dialogStore.state.stack.findIndex((item) => item === dialogName); useEffect(() => { - dialogStore.pushDialogStack(dialogName); + dialogStore.pushDialog(dialogName); const handleKeyDown = (event: KeyboardEvent) => { if (event.code === "Escape") { - if (dialogName === dialogStore.topDialogStack()) { + if (dialogName === dialogStore.topDialog) { destroy(); } } @@ -62,7 +59,7 @@ const BaseDialog: React.FC = (props: Props) => {
); -}; +}); export function generateDialog( config: DialogConfig, @@ -87,19 +84,15 @@ export function generateDialog( destroy: cbs.destroy, } as T; - const Fragment = ( - - - - - - - - - - ); + const Fragment = observer(() => ( + + + + + + )); - dialog.render(Fragment); + dialog.render(); return cbs; } diff --git a/web/src/components/HomeSidebar/ShortcutsSection.tsx b/web/src/components/HomeSidebar/ShortcutsSection.tsx index 497210ea..154f7ba7 100644 --- a/web/src/components/HomeSidebar/ShortcutsSection.tsx +++ b/web/src/components/HomeSidebar/ShortcutsSection.tsx @@ -1,20 +1,21 @@ import { Dropdown, Menu, MenuButton, MenuItem, Tooltip } from "@mui/joy"; import { Edit3Icon, MoreVerticalIcon, TrashIcon, PlusIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { userServiceClient } from "@/grpcweb"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useMemoFilterStore, useUserStore } from "@/store/v1"; +import { useMemoFilterStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Shortcut } from "@/types/proto/api/v1/user_service"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; import showCreateShortcutDialog from "../CreateShortcutDialog"; -const ShortcutsSection = () => { +const ShortcutsSection = observer(() => { const t = useTranslate(); const user = useCurrentUser(); - const userStore = useUserStore(); const memoFilterStore = useMemoFilterStore(); - const shortcuts = userStore.getState().shortcuts; + const shortcuts = userStore.state.shortcuts; useAsyncEffect(async () => { await userStore.fetchShortcuts(); @@ -71,6 +72,6 @@ const ShortcutsSection = () => { ); -}; +}); export default ShortcutsSection; diff --git a/web/src/components/Inbox/MemoCommentMessage.tsx b/web/src/components/Inbox/MemoCommentMessage.tsx index d9d3eaab..3e58d628 100644 --- a/web/src/components/Inbox/MemoCommentMessage.tsx +++ b/web/src/components/Inbox/MemoCommentMessage.tsx @@ -5,7 +5,8 @@ import toast from "react-hot-toast"; import { activityServiceClient } from "@/grpcweb"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { activityNamePrefix, useInboxStore, useMemoStore, useUserStore } from "@/store/v1"; +import { activityNamePrefix, useMemoStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Inbox, Inbox_Status } from "@/types/proto/api/v1/inbox_service"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { User } from "@/types/proto/api/v1/user_service"; @@ -19,9 +20,7 @@ interface Props { const MemoCommentMessage = ({ inbox }: Props) => { const t = useTranslate(); const navigateTo = useNavigateTo(); - const inboxStore = useInboxStore(); const memoStore = useMemoStore(); - const userStore = useUserStore(); const [relatedMemo, setRelatedMemo] = useState(undefined); const [sender, setSender] = useState(undefined); const [initialized, setInitialized] = useState(false); @@ -58,7 +57,7 @@ const MemoCommentMessage = ({ inbox }: Props) => { }; const handleArchiveMessage = async (silence = false) => { - await inboxStore.updateInbox( + await userStore.updateInbox( { name: inbox.name, status: Inbox_Status.ARCHIVED, diff --git a/web/src/components/Inbox/VersionUpdateMessage.tsx b/web/src/components/Inbox/VersionUpdateMessage.tsx index 80fb36fc..9c0c499a 100644 --- a/web/src/components/Inbox/VersionUpdateMessage.tsx +++ b/web/src/components/Inbox/VersionUpdateMessage.tsx @@ -3,7 +3,8 @@ import { ArrowUpIcon, InboxIcon } from "lucide-react"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { activityServiceClient } from "@/grpcweb"; -import { activityNamePrefix, useInboxStore } from "@/store/v1"; +import { activityNamePrefix } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Activity } from "@/types/proto/api/v1/activity_service"; import { Inbox, Inbox_Status } from "@/types/proto/api/v1/inbox_service"; import { cn } from "@/utils"; @@ -15,7 +16,6 @@ interface Props { const VersionUpdateMessage = ({ inbox }: Props) => { const t = useTranslate(); - const inboxStore = useInboxStore(); const [activity, setActivity] = useState(undefined); useEffect(() => { @@ -43,7 +43,7 @@ const VersionUpdateMessage = ({ inbox }: Props) => { }; const handleArchiveMessage = async (silence = false) => { - await inboxStore.updateInbox( + await userStore.updateInbox( { name: inbox.name, status: Inbox_Status.ARCHIVED, diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 4a507053..40683bcd 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -2,6 +2,7 @@ import { Select, Option, Divider } from "@mui/joy"; import { Button } from "@usememos/mui"; import { isEqual } from "lodash-es"; import { LoaderIcon, SendIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import React, { useEffect, useMemo, useRef, useState } from "react"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; @@ -13,7 +14,8 @@ import { TAB_SPACE_WIDTH } from "@/helpers/consts"; import { isValidUrl } from "@/helpers/utils"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1"; +import { useMemoStore, useResourceStore, useWorkspaceSettingStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_relation_service"; import { Location, Memo, Visibility } from "@/types/proto/api/v1/memo_service"; import { Resource } from "@/types/proto/api/v1/resource_service"; @@ -57,12 +59,11 @@ interface State { isComposing: boolean; } -const MemoEditor = (props: Props) => { +const MemoEditor = observer((props: Props) => { const { className, cacheKey, memoName, parentMemoName, autoFocus, onConfirm, onCancel } = props; const t = useTranslate(); const { i18n } = useTranslation(); const workspaceSettingStore = useWorkspaceSettingStore(); - const userStore = useUserStore(); const memoStore = useMemoStore(); const resourceStore = useResourceStore(); const currentUser = useCurrentUser(); @@ -78,7 +79,7 @@ const MemoEditor = (props: Props) => { const [displayTime, setDisplayTime] = useState(); const [hasContent, setHasContent] = useState(false); const editorRef = useRef(null); - const userSetting = userStore.userSetting as UserSetting; + const userSetting = userStore.state.userSetting as UserSetting; const contentCacheKey = `${currentUser.name}-${cacheKey || ""}`; const [contentCache, setContentCache] = useLocalStorage(contentCacheKey, ""); const referenceRelations = memoName @@ -521,6 +522,6 @@ const MemoEditor = (props: Props) => { ); -}; +}); export default MemoEditor; diff --git a/web/src/components/Navigation.tsx b/web/src/components/Navigation.tsx index cd87703e..744bf0fa 100644 --- a/web/src/components/Navigation.tsx +++ b/web/src/components/Navigation.tsx @@ -1,10 +1,11 @@ import { Tooltip } from "@mui/joy"; import { ArchiveIcon, BellIcon, Globe2Icon, HomeIcon, LogInIcon, PaperclipIcon, SettingsIcon, SmileIcon, User2Icon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useEffect } from "react"; import { NavLink } from "react-router-dom"; import useCurrentUser from "@/hooks/useCurrentUser"; import { Routes } from "@/router"; -import { useInboxStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Inbox_Status } from "@/types/proto/api/v1/inbox_service"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; @@ -22,30 +23,18 @@ interface Props { className?: string; } -const Navigation = (props: Props) => { +const Navigation = observer((props: Props) => { const { collapsed, className } = props; const t = useTranslate(); const user = useCurrentUser(); - const inboxStore = useInboxStore(); - const hasUnreadInbox = inboxStore.inboxes.some((inbox) => inbox.status === Inbox_Status.UNREAD); + const hasUnreadInbox = userStore.state.inboxes.some((inbox) => inbox.status === Inbox_Status.UNREAD); useEffect(() => { if (!user) { return; } - inboxStore.fetchInboxes(); - // Fetch inboxes every 5 minutes. - const timer = setInterval( - async () => { - await inboxStore.fetchInboxes(); - }, - 1000 * 60 * 5, - ); - - return () => { - clearInterval(timer); - }; + userStore.fetchInboxes(); }, []); const homeNavLink: NavLinkItem = { @@ -147,6 +136,6 @@ const Navigation = (props: Props) => { ); -}; +}); export default Navigation; diff --git a/web/src/components/PasswordSignInForm.tsx b/web/src/components/PasswordSignInForm.tsx index aba46cba..9b4d956d 100644 --- a/web/src/components/PasswordSignInForm.tsx +++ b/web/src/components/PasswordSignInForm.tsx @@ -1,19 +1,19 @@ import { Button, Checkbox, Input } from "@usememos/mui"; import { LoaderIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { ClientError } from "nice-grpc-web"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { authServiceClient } from "@/grpcweb"; import useLoading from "@/hooks/useLoading"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { useCommonContext } from "@/layouts/CommonContextProvider"; import { useUserStore } from "@/store/v1"; +import { workspaceStore } from "@/store/v2"; import { useTranslate } from "@/utils/i18n"; -const PasswordSignInForm = () => { +const PasswordSignInForm = observer(() => { const t = useTranslate(); const navigateTo = useNavigateTo(); - const commonContext = useCommonContext(); const userStore = useUserStore(); const actionBtnLoadingState = useLoading(false); const [username, setUsername] = useState(""); @@ -21,11 +21,11 @@ const PasswordSignInForm = () => { const [remember, setRemember] = useState(true); useEffect(() => { - if (commonContext.profile.mode === "demo") { + if (workspaceStore.state.profile.mode === "demo") { setUsername("yourselfhosted"); setPassword("yourselfhosted"); } - }, [commonContext.profile.mode]); + }, [workspaceStore.state.profile.mode]); const handleUsernameInputChanged = (e: React.ChangeEvent) => { const text = e.target.value as string; @@ -117,6 +117,6 @@ const PasswordSignInForm = () => { ); -}; +}); export default PasswordSignInForm; diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index c4b9519b..e6278c3c 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 { useCommonContext } from "@/layouts/CommonContextProvider"; -import { useUserStore } from "@/store/v1"; +import { observer } from "mobx-react-lite"; +import { userStore, workspaceStore } 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"; @@ -10,14 +10,12 @@ import LocaleSelect from "../LocaleSelect"; import VisibilityIcon from "../VisibilityIcon"; import WebhookSection from "./WebhookSection"; -const PreferencesSection = () => { +const PreferencesSection = observer(() => { const t = useTranslate(); - const commonContext = useCommonContext(); - const userStore = useUserStore(); - const setting = userStore.userSetting as UserSetting; + const setting = userStore.state.userSetting as UserSetting; const handleLocaleSelectChange = async (locale: Locale) => { - commonContext.setLocale(locale); + workspaceStore.setPartial({ locale }); await userStore.updateUserSetting( { locale, @@ -27,7 +25,7 @@ const PreferencesSection = () => { }; const handleAppearanceSelectChange = async (appearance: Appearance) => { - commonContext.setAppearance(appearance); + workspaceStore.setPartial({ appearance }); await userStore.updateUserSetting( { appearance, @@ -84,6 +82,6 @@ const PreferencesSection = () => { ); -}; +}); export default PreferencesSection; diff --git a/web/src/components/StatisticsView.tsx b/web/src/components/StatisticsView.tsx index 0abbdcb3..11fef056 100644 --- a/web/src/components/StatisticsView.tsx +++ b/web/src/components/StatisticsView.tsx @@ -1,7 +1,7 @@ import { Tooltip } from "@mui/joy"; import dayjs from "dayjs"; import { countBy } from "lodash-es"; -import { CheckCircleIcon, ChevronDownIcon, ChevronUpIcon, Code2Icon, LinkIcon, ListTodoIcon } from "lucide-react"; +import { CheckCircleIcon, ChevronRightIcon, ChevronLeftIcon, Code2Icon, LinkIcon, ListTodoIcon } from "lucide-react"; import { useState } from "react"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; @@ -60,7 +60,7 @@ const StatisticsView = () => { showMonthYearPicker showFullMonthYearPicker customInput={ - + {dayjs(visibleMonthString).toDate().toLocaleString(i18n.language, { year: "numeric", month: "long" })} } @@ -73,13 +73,13 @@ const StatisticsView = () => { className="cursor-pointer hover:opacity-80" onClick={() => setVisibleMonthString(dayjs(visibleMonthString).subtract(1, "month").format("YYYY-MM"))} > - + setVisibleMonthString(dayjs(visibleMonthString).add(1, "month").format("YYYY-MM"))} > - + diff --git a/web/src/helpers/polyfill.ts b/web/src/helpers/polyfill.ts deleted file mode 100644 index 1f9f53c2..00000000 --- a/web/src/helpers/polyfill.ts +++ /dev/null @@ -1,15 +0,0 @@ -(() => { - if (!String.prototype.replaceAll) { - String.prototype.replaceAll = function (str: any, newStr: any) { - // If a regex pattern - if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { - return this.replace(str, newStr); - } - - // If a string - return this.replace(new RegExp(str, "g"), newStr); - }; - } -})(); - -export default null; diff --git a/web/src/hooks/useCurrentUser.ts b/web/src/hooks/useCurrentUser.ts index 572f4977..158f9247 100644 --- a/web/src/hooks/useCurrentUser.ts +++ b/web/src/hooks/useCurrentUser.ts @@ -1,8 +1,7 @@ -import { useUserStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; const useCurrentUser = () => { - const userStore = useUserStore(); - return userStore.getUserByName(userStore.currentUser || ""); + return userStore.state.userMapByName[userStore.state.currentUser || ""]; }; export default useCurrentUser; diff --git a/web/src/layouts/CommonContextProvider.tsx b/web/src/layouts/CommonContextProvider.tsx deleted file mode 100644 index 36f7f088..00000000 --- a/web/src/layouts/CommonContextProvider.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { createContext, useContext, useEffect, useState } from "react"; -import useLocalStorage from "react-use/lib/useLocalStorage"; -import { workspaceServiceClient } from "@/grpcweb"; -import { useUserStore, useWorkspaceSettingStore } from "@/store/v1"; -import { WorkspaceProfile } from "@/types/proto/api/v1/workspace_service"; -import { WorkspaceGeneralSetting, WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; - -interface Context { - locale: string; - appearance: string; - profile: WorkspaceProfile; - setLocale: (locale: string) => void; - setAppearance: (appearance: string) => void; -} - -const CommonContext = createContext({ - locale: "en", - appearance: "system", - profile: WorkspaceProfile.fromPartial({}), - setLocale: () => {}, - setAppearance: () => {}, -}); - -const CommonContextProvider = ({ children }: { children: React.ReactNode }) => { - const workspaceSettingStore = useWorkspaceSettingStore(); - const userStore = useUserStore(); - const [initialized, setInitialized] = useState(false); - const [commonContext, setCommonContext] = useState>({ - locale: "en", - appearance: "system", - profile: WorkspaceProfile.fromPartial({}), - }); - const [locale] = useLocalStorage("locale", "en"); - const [appearance] = useLocalStorage("appearance", "system"); - - useEffect(() => { - const initialWorkspace = async () => { - const workspaceProfile = await workspaceServiceClient.getWorkspaceProfile({}); - // Initial fetch for workspace settings. - (async () => { - [WorkspaceSettingKey.GENERAL, WorkspaceSettingKey.MEMO_RELATED].forEach(async (key) => { - await workspaceSettingStore.fetchWorkspaceSetting(key); - }); - })(); - - const workspaceGeneralSetting = - workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL).generalSetting || - WorkspaceGeneralSetting.fromPartial({}); - setCommonContext({ - locale: locale || workspaceGeneralSetting.customProfile?.locale || "en", - appearance: appearance || workspaceGeneralSetting.customProfile?.appearance || "system", - profile: workspaceProfile, - }); - }; - - const initialUser = async () => { - try { - await userStore.fetchCurrentUser(); - } catch (error) { - // Do nothing. - } - }; - - Promise.all([initialWorkspace(), initialUser()]).then(() => setInitialized(true)); - }, []); - - return ( - setCommonContext({ ...commonContext, locale }), - setAppearance: (appearance: string) => setCommonContext({ ...commonContext, appearance }), - }} - > - {!initialized ? null : <>{children}} - - ); -}; - -export const useCommonContext = () => { - return useContext(CommonContext); -}; - -export default CommonContextProvider; diff --git a/web/src/main.tsx b/web/src/main.tsx index 54a67855..847c2430 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -2,30 +2,30 @@ import "@github/relative-time-element"; import { CssVarsProvider } from "@mui/joy"; import "@usememos/mui/dist/index.css"; import "leaflet/dist/leaflet.css"; +import { observer } from "mobx-react-lite"; import { createRoot } from "react-dom/client"; import { Toaster } from "react-hot-toast"; -import { Provider } from "react-redux"; import { RouterProvider } from "react-router-dom"; import "./css/tailwind.css"; -import "./helpers/polyfill"; import "./i18n"; -import CommonContextProvider from "./layouts/CommonContextProvider"; import "./less/highlight.less"; import router from "./router"; -import store from "./store"; +import { initialUserStore } from "./store/v2/user"; +import { initialWorkspaceStore } from "./store/v2/workspace"; import theme from "./theme"; +const Main = observer(() => ( + + + + +)); + (async () => { + await initialWorkspaceStore(); + await initialUserStore(); + const container = document.getElementById("root"); const root = createRoot(container as HTMLElement); - root.render( - - - - - - - - , - ); + root.render(
); })(); diff --git a/web/src/pages/AdminSignIn.tsx b/web/src/pages/AdminSignIn.tsx index 520dec25..ff44c220 100644 --- a/web/src/pages/AdminSignIn.tsx +++ b/web/src/pages/AdminSignIn.tsx @@ -1,23 +1,23 @@ +import { observer } from "mobx-react-lite"; import AppearanceSelect from "@/components/AppearanceSelect"; import LocaleSelect from "@/components/LocaleSelect"; import PasswordSignInForm from "@/components/PasswordSignInForm"; -import { useCommonContext } from "@/layouts/CommonContextProvider"; import { useWorkspaceSettingStore } from "@/store/v1"; +import { workspaceStore } from "@/store/v2"; import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_setting_service"; import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; -const AdminSignIn = () => { - const commonContext = useCommonContext(); +const AdminSignIn = observer(() => { const workspaceSettingStore = useWorkspaceSettingStore(); const workspaceGeneralSetting = workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL).generalSetting || WorkspaceGeneralSetting.fromPartial({}); const handleLocaleSelectChange = (locale: Locale) => { - commonContext.setLocale(locale); + workspaceStore.setPartial({ locale }); }; const handleAppearanceSelectChange = (appearance: Appearance) => { - commonContext.setAppearance(appearance); + workspaceStore.setPartial({ appearance }); }; return ( @@ -33,11 +33,11 @@ const AdminSignIn = () => {
- - + +
); -}; +}); export default AdminSignIn; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 7ef68802..e7ec0417 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,4 +1,5 @@ import dayjs from "dayjs"; +import { observer } from "mobx-react-lite"; import { useMemo } from "react"; import { HomeSidebar, HomeSidebarDrawer } from "@/components/HomeSidebar"; import MemoEditor from "@/components/MemoEditor"; @@ -7,17 +8,17 @@ import MobileHeader from "@/components/MobileHeader"; import PagedMemoList from "@/components/PagedMemoList"; import useCurrentUser from "@/hooks/useCurrentUser"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; -import { useMemoFilterStore, useUserStore } from "@/store/v1"; +import { useMemoFilterStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Direction, State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { cn } from "@/utils"; -const Home = () => { +const Home = observer(() => { const { md, lg } = useResponsiveWidth(); const user = useCurrentUser(); - const userStore = useUserStore(); const memoFilterStore = useMemoFilterStore(); - const selectedShortcut = userStore.shortcuts.find((shortcut) => shortcut.id === memoFilterStore.shortcut); + const selectedShortcut = userStore.state.shortcuts.find((shortcut) => shortcut.id === memoFilterStore.shortcut); const memoListFilter = useMemo(() => { const conditions = []; @@ -95,6 +96,6 @@ const Home = () => { ); -}; +}); export default Home; diff --git a/web/src/pages/Inboxes.tsx b/web/src/pages/Inboxes.tsx index f9b84b0e..2e40b161 100644 --- a/web/src/pages/Inboxes.tsx +++ b/web/src/pages/Inboxes.tsx @@ -1,17 +1,17 @@ import { BellIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useEffect } from "react"; import Empty from "@/components/Empty"; import MemoCommentMessage from "@/components/Inbox/MemoCommentMessage"; import VersionUpdateMessage from "@/components/Inbox/VersionUpdateMessage"; import MobileHeader from "@/components/MobileHeader"; -import { useInboxStore } from "@/store/v1"; +import { userStore } from "@/store/v2"; import { Inbox_Status, Inbox_Type } from "@/types/proto/api/v1/inbox_service"; import { useTranslate } from "@/utils/i18n"; -const Inboxes = () => { +const Inboxes = observer(() => { const t = useTranslate(); - const inboxStore = useInboxStore(); - const inboxes = inboxStore.inboxes.sort((a, b) => { + const inboxes = userStore.state.inboxes.sort((a, b) => { if (a.status === b.status) { return 0; } @@ -19,7 +19,7 @@ const Inboxes = () => { }); useEffect(() => { - inboxStore.fetchInboxes(); + userStore.fetchInboxes(); }, []); return ( @@ -55,6 +55,6 @@ const Inboxes = () => { ); -}; +}); export default Inboxes; diff --git a/web/src/pages/Setting.tsx b/web/src/pages/Setting.tsx index 77b07eee..976cd0b4 100644 --- a/web/src/pages/Setting.tsx +++ b/web/src/pages/Setting.tsx @@ -1,5 +1,6 @@ import { Option, Select } from "@mui/joy"; import { CogIcon, DatabaseIcon, KeyIcon, LibraryIcon, LucideIcon, Settings2Icon, UserIcon, UsersIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; import MobileHeader from "@/components/MobileHeader"; @@ -12,8 +13,8 @@ import SectionMenuItem from "@/components/Settings/SectionMenuItem"; import StorageSection from "@/components/Settings/StorageSection"; import WorkspaceSection from "@/components/Settings/WorkspaceSection"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useCommonContext } from "@/layouts/CommonContextProvider"; import { useWorkspaceSettingStore } from "@/store/v1"; +import { workspaceStore } from "@/store/v2"; import { User_Role } from "@/types/proto/api/v1/user_service"; import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; import { useTranslate } from "@/utils/i18n"; @@ -36,10 +37,9 @@ const SECTION_ICON_MAP: Record = { sso: KeyIcon, }; -const Setting = () => { +const Setting = observer(() => { const t = useTranslate(); const location = useLocation(); - const commonContext = useCommonContext(); const user = useCurrentUser(); const workspaceSettingStore = useWorkspaceSettingStore(); const [state, setState] = useState({ @@ -115,7 +115,7 @@ const Setting = () => { /> ))} - {t("setting.version")}: v{commonContext.profile.version} + {t("setting.version")}: v{workspaceStore.state.profile.version} @@ -151,6 +151,6 @@ const Setting = () => { ); -}; +}); export default Setting; diff --git a/web/src/pages/SignIn.tsx b/web/src/pages/SignIn.tsx index bb89b5f1..7fd0ac05 100644 --- a/web/src/pages/SignIn.tsx +++ b/web/src/pages/SignIn.tsx @@ -1,5 +1,6 @@ import { Divider } from "@mui/joy"; import { Button } from "@usememos/mui"; +import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { Link } from "react-router-dom"; @@ -9,18 +10,17 @@ import PasswordSignInForm from "@/components/PasswordSignInForm"; import { identityProviderServiceClient } from "@/grpcweb"; import { absolutifyLink } from "@/helpers/utils"; import useCurrentUser from "@/hooks/useCurrentUser"; -import { useCommonContext } from "@/layouts/CommonContextProvider"; import { Routes } from "@/router"; import { extractIdentityProviderIdFromName, useWorkspaceSettingStore } from "@/store/v1"; +import { workspaceStore } from "@/store/v2"; import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/idp_service"; import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_setting_service"; import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; import { useTranslate } from "@/utils/i18n"; -const SignIn = () => { +const SignIn = observer(() => { const t = useTranslate(); const currentUser = useCurrentUser(); - const commonContext = useCommonContext(); const workspaceSettingStore = useWorkspaceSettingStore(); const [identityProviderList, setIdentityProviderList] = useState([]); const workspaceGeneralSetting = @@ -43,11 +43,11 @@ const SignIn = () => { }, []); const handleLocaleSelectChange = (locale: Locale) => { - commonContext.setLocale(locale); + workspaceStore.setPartial({ locale }); }; const handleAppearanceSelectChange = (appearance: Appearance) => { - commonContext.setAppearance(appearance); + workspaceStore.setPartial({ appearance }); }; const handleSignInWithIdentityProvider = async (identityProvider: IdentityProvider) => { @@ -110,11 +110,11 @@ const SignIn = () => { )}
- - + +
); -}; +}); export default SignIn; diff --git a/web/src/pages/SignUp.tsx b/web/src/pages/SignUp.tsx index 14874213..8deddc46 100644 --- a/web/src/pages/SignUp.tsx +++ b/web/src/pages/SignUp.tsx @@ -1,5 +1,6 @@ import { Button, Input } from "@usememos/mui"; import { LoaderIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { ClientError } from "nice-grpc-web"; import { useState } from "react"; import { toast } from "react-hot-toast"; @@ -9,16 +10,15 @@ import LocaleSelect from "@/components/LocaleSelect"; import { authServiceClient } from "@/grpcweb"; import useLoading from "@/hooks/useLoading"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { useCommonContext } from "@/layouts/CommonContextProvider"; import { useUserStore, useWorkspaceSettingStore } from "@/store/v1"; +import { workspaceStore } from "@/store/v2"; import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_setting_service"; import { WorkspaceSettingKey } from "@/types/proto/store/workspace_setting"; import { useTranslate } from "@/utils/i18n"; -const SignUp = () => { +const SignUp = observer(() => { const t = useTranslate(); const navigateTo = useNavigateTo(); - const commonContext = useCommonContext(); const workspaceSettingStore = useWorkspaceSettingStore(); const userStore = useUserStore(); const actionBtnLoadingState = useLoading(false); @@ -38,11 +38,11 @@ const SignUp = () => { }; const handleLocaleSelectChange = (locale: Locale) => { - commonContext.setLocale(locale); + workspaceStore.setPartial({ locale }); }; const handleAppearanceSelectChange = (appearance: Appearance) => { - commonContext.setAppearance(appearance); + workspaceStore.setPartial({ appearance }); }; const handleFormSubmit = (e: React.FormEvent) => { @@ -136,7 +136,7 @@ const SignUp = () => { ) : (

Sign up is not allowed.

)} - {!commonContext.profile.owner ? ( + {!workspaceStore.state.profile.owner ? (

{t("auth.host-tip")}

) : (

@@ -148,11 +148,11 @@ const SignUp = () => { )}

- - + +
); -}; +}); export default SignUp; diff --git a/web/src/store/index.ts b/web/src/store/index.ts deleted file mode 100644 index 0f939145..00000000 --- a/web/src/store/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { configureStore } from "@reduxjs/toolkit"; -import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; -import dialogReducer from "./reducer/dialog"; - -const store = configureStore({ - reducer: { - dialog: dialogReducer, - }, -}); - -type AppState = ReturnType; -type AppDispatch = typeof store.dispatch; - -export const useAppSelector: TypedUseSelectorHook = useSelector; -export const useAppDispatch: () => AppDispatch = useDispatch; - -export default store; diff --git a/web/src/store/module/dialog.ts b/web/src/store/module/dialog.ts deleted file mode 100644 index 27f90508..00000000 --- a/web/src/store/module/dialog.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { last } from "lodash-es"; -import store, { useAppSelector } from ".."; -import { popDialogStack, pushDialogStack, removeDialog } from "../reducer/dialog"; - -export const useDialogStore = () => { - const state = useAppSelector((state) => state.dialog); - - return { - state, - getState: () => { - return store.getState().dialog; - }, - pushDialogStack: (dialogName: string) => { - store.dispatch(pushDialogStack(dialogName)); - }, - popDialogStack: () => { - store.dispatch(popDialogStack()); - }, - removeDialog: (dialogName: string) => { - store.dispatch(removeDialog(dialogName)); - }, - topDialogStack: () => { - return last(store.getState().dialog.dialogStack); - }, - }; -}; diff --git a/web/src/store/module/index.ts b/web/src/store/module/index.ts deleted file mode 100644 index e5c28748..00000000 --- a/web/src/store/module/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dialog"; diff --git a/web/src/store/reducer/dialog.ts b/web/src/store/reducer/dialog.ts deleted file mode 100644 index d6ffa3c0..00000000 --- a/web/src/store/reducer/dialog.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -interface State { - dialogStack: string[]; -} - -const dialogSlice = createSlice({ - name: "dialog", - initialState: { - dialogStack: [], - } as State, - reducers: { - pushDialogStack: (state, action: PayloadAction) => { - return { - ...state, - dialogStack: [...state.dialogStack, action.payload], - }; - }, - popDialogStack: (state) => { - return { - ...state, - dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1), - }; - }, - removeDialog: (state, action: PayloadAction) => { - const filterDialogStack = state.dialogStack.filter((dialogName) => dialogName !== action.payload); - return { - ...state, - dialogStack: filterDialogStack, - }; - }, - }, -}); - -export const { pushDialogStack, popDialogStack, removeDialog } = dialogSlice.actions; - -export default dialogSlice.reducer; diff --git a/web/src/store/v1/inbox.ts b/web/src/store/v1/inbox.ts deleted file mode 100644 index 67a44d04..00000000 --- a/web/src/store/v1/inbox.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { create } from "zustand"; -import { combine } from "zustand/middleware"; -import { inboxServiceClient } from "@/grpcweb"; -import { Inbox } from "@/types/proto/api/v1/inbox_service"; - -interface State { - inboxes: Inbox[]; -} - -const getDefaultState = (): State => ({ - inboxes: [], -}); - -export const useInboxStore = create( - combine(getDefaultState(), (set, get) => ({ - fetchInboxes: async () => { - const { inboxes } = await inboxServiceClient.listInboxes({}); - set({ inboxes }); - return inboxes; - }, - updateInbox: async (inbox: Partial, updateMask: string[]) => { - const updatedInbox = await inboxServiceClient.updateInbox({ - inbox, - updateMask, - }); - const inboxes = get().inboxes; - set({ inboxes: inboxes.map((i) => (i.name === updatedInbox.name ? updatedInbox : i)) }); - return updatedInbox; - }, - })), -); diff --git a/web/src/store/v1/index.ts b/web/src/store/v1/index.ts index 5ca084fb..52f3a140 100644 --- a/web/src/store/v1/index.ts +++ b/web/src/store/v1/index.ts @@ -1,6 +1,5 @@ export * from "./user"; export * from "./memo"; -export * from "./inbox"; export * from "./resourceName"; export * from "./resource"; export * from "./workspaceSetting"; diff --git a/web/src/store/v1/user.ts b/web/src/store/v1/user.ts index 7501b9dd..483a05c5 100644 --- a/web/src/store/v1/user.ts +++ b/web/src/store/v1/user.ts @@ -1,21 +1,19 @@ import { create } from "zustand"; import { combine } from "zustand/middleware"; import { authServiceClient, userServiceClient } from "@/grpcweb"; -import { Shortcut, User, UserSetting, User_Role } from "@/types/proto/api/v1/user_service"; +import { User, UserSetting, User_Role } from "@/types/proto/api/v1/user_service"; interface State { userMapByName: Record; // The name of current user. Format: `users/${uid}` currentUser?: string; userSetting?: UserSetting; - shortcuts: Shortcut[]; } const getDefaultState = (): State => ({ userMapByName: {}, currentUser: undefined, userSetting: undefined, - shortcuts: [], }); const getDefaultUserSetting = () => { @@ -131,14 +129,6 @@ export const useUserStore = create( set({ userSetting: updatedUserSetting }); return updatedUserSetting; }, - fetchShortcuts: async () => { - const { currentUser } = get(); - if (!currentUser) { - return; - } - const { shortcuts } = await userServiceClient.listShortcuts({ parent: currentUser }); - set({ shortcuts }); - }, })), ); diff --git a/web/src/store/v2/dialog.ts b/web/src/store/v2/dialog.ts new file mode 100644 index 00000000..bdab673b --- /dev/null +++ b/web/src/store/v2/dialog.ts @@ -0,0 +1,32 @@ +import { last } from "lodash-es"; +import { makeAutoObservable } from "mobx"; + +const dialogStore = (() => { + const state = makeAutoObservable<{ + stack: string[]; + }>({ + stack: [], + }); + + const pushDialog = (name: string) => { + state.stack.push(name); + }; + + const popDialog = () => state.stack.pop(); + + const removeDialog = (name: string) => { + state.stack = state.stack.filter((n) => n !== name); + }; + + const topDialog = last(state.stack); + + return { + state, + topDialog, + pushDialog, + popDialog, + removeDialog, + }; +})(); + +export default dialogStore; diff --git a/web/src/store/v2/index.ts b/web/src/store/v2/index.ts new file mode 100644 index 00000000..67fd7ede --- /dev/null +++ b/web/src/store/v2/index.ts @@ -0,0 +1,4 @@ +import userStore from "./user"; +import workspaceStore from "./workspace"; + +export { workspaceStore, userStore }; diff --git a/web/src/store/v2/user.ts b/web/src/store/v2/user.ts new file mode 100644 index 00000000..a3e7e11f --- /dev/null +++ b/web/src/store/v2/user.ts @@ -0,0 +1,112 @@ +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"; + +interface LocalState { + // The name of current user. Format: `users/${uid}` + 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; +} + +const userStore = (() => { + const state = makeAutoObservable({ + shortcuts: [], + inboxes: [], + userMapByName: {}, + }); + + const getOrFetchUserByName = async (name: string) => { + const userMap = state.userMapByName; + if (userMap[name]) { + return userMap[name] as User; + } + const user = await userServiceClient.getUser({ + name: name, + }); + userMap[name] = user; + state.userMapByName = userMap; + return user; + }; + + const updateUser = async (user: Partial, updateMask: string[]) => { + const updatedUser = await userServiceClient.updateUser({ + user, + updateMask, + }); + state.userMapByName = { + ...state.userMapByName, + [updatedUser.name]: updatedUser, + }; + }; + + const updateUserSetting = async (userSetting: Partial, updateMask: string[]) => { + const updatedUserSetting = await userServiceClient.updateUserSetting({ + setting: userSetting, + updateMask: updateMask, + }); + state.userSetting = UserSetting.fromPartial(updatedUserSetting); + }; + + const fetchShortcuts = async () => { + if (!state.currentUser) { + return; + } + + const { shortcuts } = await userServiceClient.listShortcuts({ parent: state.currentUser }); + state.shortcuts = shortcuts; + }; + + const fetchInboxes = async () => { + const { inboxes } = await inboxServiceClient.listInboxes({}); + state.inboxes = inboxes; + console.log("inboxes", inboxes); + }; + + const updateInbox = async (inbox: Partial, updateMask: string[]) => { + const updatedInbox = await inboxServiceClient.updateInbox({ + inbox, + updateMask, + }); + state.inboxes = state.inboxes.map((i) => (i.name === updatedInbox.name ? updatedInbox : i)); + return updatedInbox; + }; + + return { + state, + getOrFetchUserByName, + updateUser, + updateUserSetting, + fetchShortcuts, + fetchInboxes, + updateInbox, + }; +})(); + +export const initialUserStore = async () => { + try { + const currentUser = await authServiceClient.getAuthStatus({}); + const userSetting = await userServiceClient.getUserSetting({}); + Object.assign(userStore.state, { + currentUser: currentUser.name, + userSetting: UserSetting.fromPartial({ + ...userSetting, + }), + userMapByName: { + [currentUser.name]: currentUser, + }, + }); + } catch { + // Do nothing. + } +}; + +export default userStore; diff --git a/web/src/store/v2/workspace.ts b/web/src/store/v2/workspace.ts new file mode 100644 index 00000000..a6c08f57 --- /dev/null +++ b/web/src/store/v2/workspace.ts @@ -0,0 +1,68 @@ +import { makeAutoObservable } from "mobx"; +import { workspaceServiceClient, workspaceSettingServiceClient } from "@/grpcweb"; +import { WorkspaceProfile } from "@/types/proto/api/v1/workspace_service"; +import { WorkspaceGeneralSetting, WorkspaceSetting } from "@/types/proto/api/v1/workspace_setting_service"; +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[]; +} + +const workspaceStore = (() => { + const state = makeAutoObservable({ + locale: "en", + appearance: "system", + profile: WorkspaceProfile.fromPartial({}), + settings: [], + }); + + 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); + }; + + return { + state, + generalSetting, + setPartial, + fetchWorkspaceSetting, + }; +})(); + +export const initialWorkspaceStore = async () => { + const workspaceProfile = await workspaceServiceClient.getWorkspaceProfile({}); + // Prepare workspace settings. + for (const key of [WorkspaceSettingKey.GENERAL, WorkspaceSettingKey.MEMO_RELATED]) { + await workspaceStore.fetchWorkspaceSetting(key); + } + + 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, + profile: workspaceProfile, + }); +}; + +export default workspaceStore; diff --git a/web/src/utils/i18n.ts b/web/src/utils/i18n.ts index 9e6bf97a..7d6a33c5 100644 --- a/web/src/utils/i18n.ts +++ b/web/src/utils/i18n.ts @@ -46,3 +46,8 @@ export const useTranslate = (): TypedT => { const { t } = useTranslation(); return t; }; + +export const isValidateLocale = (locale: string | undefined | null): boolean => { + if (!locale) return false; + return locales.includes(locale); +};