refactor(frontend): retire redux

pull/4384/head
Johnny 3 weeks ago
parent 13f6fa7b37
commit 5a2f18da69

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

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

@ -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: {}

@ -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 <Outlet />;
};
});
export default App;

@ -78,7 +78,7 @@ const ActivityCalendar = (props: Props) => {
return (
<div
key={`${date}-${index}`}
className={cn("w-6 h-6 text-xs flex justify-center items-center cursor-default", "opacity-60 text-gray-400")}
className={cn("w-6 h-6 text-xs lg:text-[13px] flex justify-center items-center cursor-default", "opacity-60 text-gray-400")}
>
{item.day}
</div>
@ -101,7 +101,7 @@ const ActivityCalendar = (props: Props) => {
<Tooltip className="shrink-0" key={`${date}-${index}`} title={tooltipText} placement="top" arrow>
<div
className={cn(
"w-6 h-6 text-xs flex justify-center items-center cursor-default",
"w-6 h-6 text-xs lg:text-[13px] flex justify-center items-center cursor-default",
"rounded-lg border-2 text-gray-400",
item.isCurrentMonth && getCellAdditionalStyles(count, maxCount),
item.isCurrentMonth && isToday && "border-zinc-400",

@ -2,7 +2,7 @@ import { Button, Input } from "@usememos/mui";
import { XIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useUserStore } from "@/store/v1";
import { userStore } from "@/store/v2";
import { User } from "@/types/proto/api/v1/user_service";
import { useTranslate } from "@/utils/i18n";
import { generateDialog } from "./Dialog";
@ -14,7 +14,6 @@ interface Props extends DialogProps {
const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
const { user, destroy } = props;
const t = useTranslate();
const userStore = useUserStore();
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");

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

@ -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: Props) => {
const BaseDialog = observer((props: Props) => {
const { children, className, clickSpaceDestroy, dialogName, destroy } = props;
const dialogStore = useDialogStore();
const dialogContainerRef = useRef<HTMLDivElement>(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: Props) => {
</div>
</div>
);
};
});
export function generateDialog<T extends DialogProps>(
config: DialogConfig,
@ -87,19 +84,15 @@ export function generateDialog<T extends DialogProps>(
destroy: cbs.destroy,
} as T;
const Fragment = (
<Provider store={store}>
<CssVarsProvider theme={theme}>
<CommonContextProvider>
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
<DialogComponent {...dialogProps} />
</BaseDialog>
</CommonContextProvider>
</CssVarsProvider>
</Provider>
);
const Fragment = observer(() => (
<CssVarsProvider theme={theme}>
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
<DialogComponent {...dialogProps} />
</BaseDialog>
</CssVarsProvider>
));
dialog.render(Fragment);
dialog.render(<Fragment />);
return cbs;
}

@ -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 = () => {
</div>
</div>
);
};
});
export default ShortcutsSection;

@ -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<Memo | undefined>(undefined);
const [sender, setSender] = useState<User | undefined>(undefined);
const [initialized, setInitialized] = useState<boolean>(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,

@ -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<Activity | undefined>(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,

@ -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<Date | undefined>();
const [hasContent, setHasContent] = useState<boolean>(false);
const editorRef = useRef<EditorRefActions>(null);
const userSetting = userStore.userSetting as UserSetting;
const userSetting = userStore.state.userSetting as UserSetting;
const contentCacheKey = `${currentUser.name}-${cacheKey || ""}`;
const [contentCache, setContentCache] = useLocalStorage<string>(contentCacheKey, "");
const referenceRelations = memoName
@ -521,6 +522,6 @@ const MemoEditor = (props: Props) => {
</div>
</MemoEditorContext.Provider>
);
};
});
export default MemoEditor;

@ -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) => {
</div>
</header>
);
};
});
export default Navigation;

@ -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<HTMLInputElement>) => {
const text = e.target.value as string;
@ -117,6 +117,6 @@ const PasswordSignInForm = () => {
</div>
</form>
);
};
});
export default PasswordSignInForm;

@ -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 = () => {
<WebhookSection />
</div>
);
};
});
export default PreferencesSection;

@ -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={
<span className="cursor-pointer hover:text-gray-600 dark:hover:text-gray-300">
<span className="cursor-pointer text-base md:text-lg hover:text-gray-600 dark:hover:text-gray-300">
{dayjs(visibleMonthString).toDate().toLocaleString(i18n.language, { year: "numeric", month: "long" })}
</span>
}
@ -73,13 +73,13 @@ const StatisticsView = () => {
className="cursor-pointer hover:opacity-80"
onClick={() => setVisibleMonthString(dayjs(visibleMonthString).subtract(1, "month").format("YYYY-MM"))}
>
<ChevronUpIcon className="w-5 h-auto shrink-0 opacity-40" />
<ChevronLeftIcon className="w-5 h-auto shrink-0 opacity-40" />
</span>
<span
className="cursor-pointer hover:opacity-80"
onClick={() => setVisibleMonthString(dayjs(visibleMonthString).add(1, "month").format("YYYY-MM"))}
>
<ChevronDownIcon className="w-5 h-auto shrink-0 opacity-40" />
<ChevronRightIcon className="w-5 h-auto shrink-0 opacity-40" />
</span>
</div>
</div>

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

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

@ -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<Context>({
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<Pick<Context, "locale" | "appearance" | "profile">>({
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 (
<CommonContext.Provider
value={{
...commonContext,
setLocale: (locale: string) => setCommonContext({ ...commonContext, locale }),
setAppearance: (appearance: string) => setCommonContext({ ...commonContext, appearance }),
}}
>
{!initialized ? null : <>{children}</>}
</CommonContext.Provider>
);
};
export const useCommonContext = () => {
return useContext(CommonContext);
};
export default CommonContextProvider;

@ -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(() => (
<CssVarsProvider theme={theme}>
<RouterProvider router={router} />
<Toaster position="top-right" toastOptions={{ className: "dark:bg-zinc-700 dark:text-gray-300" }} />
</CssVarsProvider>
));
(async () => {
await initialWorkspaceStore();
await initialUserStore();
const container = document.getElementById("root");
const root = createRoot(container as HTMLElement);
root.render(
<Provider store={store}>
<CssVarsProvider theme={theme}>
<CommonContextProvider>
<RouterProvider router={router} />
</CommonContextProvider>
<Toaster position="top-right" toastOptions={{ className: "dark:bg-zinc-700 dark:text-gray-300" }} />
</CssVarsProvider>
</Provider>,
);
root.render(<Main />);
})();

@ -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 = () => {
<PasswordSignInForm />
</div>
<div className="mt-4 flex flex-row items-center justify-center w-full gap-2">
<LocaleSelect value={commonContext.locale} onChange={handleLocaleSelectChange} />
<AppearanceSelect value={commonContext.appearance as Appearance} onChange={handleAppearanceSelectChange} />
<LocaleSelect value={workspaceStore.state.locale} onChange={handleLocaleSelectChange} />
<AppearanceSelect value={workspaceStore.state.appearance as Appearance} onChange={handleAppearanceSelectChange} />
</div>
</div>
);
};
});
export default AdminSignIn;

@ -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 = () => {
</div>
</section>
);
};
});
export default Home;

@ -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 = () => {
</div>
</section>
);
};
});
export default Inboxes;

@ -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<SettingSection, LucideIcon> = {
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<State>({
@ -115,7 +115,7 @@ const Setting = () => {
/>
))}
<span className="px-3 mt-2 opacity-70 text-sm">
{t("setting.version")}: v{commonContext.profile.version}
{t("setting.version")}: v{workspaceStore.state.profile.version}
</span>
</div>
</>
@ -151,6 +151,6 @@ const Setting = () => {
</div>
</section>
);
};
});
export default Setting;

@ -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<IdentityProvider[]>([]);
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 = () => {
)}
</div>
<div className="mt-4 flex flex-row items-center justify-center w-full gap-2">
<LocaleSelect value={commonContext.locale} onChange={handleLocaleSelectChange} />
<AppearanceSelect value={commonContext.appearance as Appearance} onChange={handleAppearanceSelectChange} />
<LocaleSelect value={workspaceStore.state.locale} onChange={handleLocaleSelectChange} />
<AppearanceSelect value={workspaceStore.state.appearance as Appearance} onChange={handleAppearanceSelectChange} />
</div>
</div>
);
};
});
export default SignIn;

@ -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<HTMLFormElement>) => {
@ -136,7 +136,7 @@ const SignUp = () => {
) : (
<p className="w-full text-2xl mt-2 dark:text-gray-500">Sign up is not allowed.</p>
)}
{!commonContext.profile.owner ? (
{!workspaceStore.state.profile.owner ? (
<p className="w-full mt-4 text-sm font-medium dark:text-gray-500">{t("auth.host-tip")}</p>
) : (
<p className="w-full mt-4 text-sm">
@ -148,11 +148,11 @@ const SignUp = () => {
)}
</div>
<div className="mt-4 flex flex-row items-center justify-center w-full gap-2">
<LocaleSelect value={commonContext.locale} onChange={handleLocaleSelectChange} />
<AppearanceSelect value={commonContext.appearance as Appearance} onChange={handleAppearanceSelectChange} />
<LocaleSelect value={workspaceStore.state.locale} onChange={handleLocaleSelectChange} />
<AppearanceSelect value={workspaceStore.state.appearance as Appearance} onChange={handleAppearanceSelectChange} />
</div>
</div>
);
};
});
export default SignUp;

@ -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<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
export const useAppDispatch: () => AppDispatch = useDispatch;
export default store;

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

@ -1 +0,0 @@
export * from "./dialog";

@ -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<string>) => {
return {
...state,
dialogStack: [...state.dialogStack, action.payload],
};
},
popDialogStack: (state) => {
return {
...state,
dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1),
};
},
removeDialog: (state, action: PayloadAction<string>) => {
const filterDialogStack = state.dialogStack.filter((dialogName) => dialogName !== action.payload);
return {
...state,
dialogStack: filterDialogStack,
};
},
},
});
export const { pushDialogStack, popDialogStack, removeDialog } = dialogSlice.actions;
export default dialogSlice.reducer;

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

@ -1,6 +1,5 @@
export * from "./user";
export * from "./memo";
export * from "./inbox";
export * from "./resourceName";
export * from "./resource";
export * from "./workspaceSetting";

@ -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<string, User>;
// 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 });
},
})),
);

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

@ -0,0 +1,4 @@
import userStore from "./user";
import workspaceStore from "./workspace";
export { workspaceStore, userStore };

@ -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<string, User>;
}
const userStore = (() => {
const state = makeAutoObservable<LocalState>({
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<User>, updateMask: string[]) => {
const updatedUser = await userServiceClient.updateUser({
user,
updateMask,
});
state.userMapByName = {
...state.userMapByName,
[updatedUser.name]: updatedUser,
};
};
const updateUserSetting = async (userSetting: Partial<UserSetting>, 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<Inbox>, 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;

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

@ -46,3 +46,8 @@ export const useTranslate = (): TypedT => {
const { t } = useTranslation<Translations>();
return t;
};
export const isValidateLocale = (locale: string | undefined | null): boolean => {
if (!locale) return false;
return locales.includes(locale);
};

Loading…
Cancel
Save