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