From f2359e7827f32417ff0752769b6573a808493361 Mon Sep 17 00:00:00 2001 From: boojack Date: Sat, 13 Aug 2022 13:38:17 +0800 Subject: [PATCH 1/4] feat: add `user_setting` model --- api/user.go | 11 +- api/user_setting.go | 34 +++++ .../docker-compose.yaml | 2 +- server/user.go | 69 +++++++--- store/db/migration/dev/LATEST__SCHEMA.sql | 11 ++ .../migration/prod/0.4/00__user_setting.sql | 9 ++ store/user_setting.go | 122 ++++++++++++++++++ 7 files changed, 236 insertions(+), 22 deletions(-) create mode 100644 api/user_setting.go rename docker-compose.yaml => quickstart/docker-compose.yaml (82%) create mode 100644 store/db/migration/prod/0.4/00__user_setting.sql create mode 100644 store/user_setting.go diff --git a/api/user.go b/api/user.go index 13625623424ef..5d1634d6cbc8c 100644 --- a/api/user.go +++ b/api/user.go @@ -29,11 +29,12 @@ type User struct { UpdatedTs int64 `json:"updatedTs"` // Domain specific fields - Email string `json:"email"` - Role Role `json:"role"` - Name string `json:"name"` - PasswordHash string `json:"-"` - OpenID string `json:"openId"` + Email string `json:"email"` + Role Role `json:"role"` + Name string `json:"name"` + PasswordHash string `json:"-"` + OpenID string `json:"openId"` + UserSettingList []*UserSetting `json:"userSettingList"` } type UserCreate struct { diff --git a/api/user_setting.go b/api/user_setting.go new file mode 100644 index 0000000000000..ff5b0bb7dffa5 --- /dev/null +++ b/api/user_setting.go @@ -0,0 +1,34 @@ +package api + +type UserSettingKey string + +const ( + // UserSettingLocaleKey is the key type for user locale + UserSettingLocaleKey UserSettingKey = "locale" +) + +// String returns the string format of UserSettingKey type. +func (key UserSettingKey) String() string { + switch key { + case UserSettingLocaleKey: + return "locale" + } + return "" +} + +type UserSetting struct { + UserID int + Key UserSettingKey `json:"key"` + // Value is a JSON string with basic value + Value string `json:"value"` +} + +type UserSettingUpsert struct { + UserID int + Key UserSettingKey `json:"key"` + Value string `json:"value"` +} + +type UserSettingFind struct { + UserID int +} diff --git a/docker-compose.yaml b/quickstart/docker-compose.yaml similarity index 82% rename from docker-compose.yaml rename to quickstart/docker-compose.yaml index c880a71f87597..3360f1e8c8f24 100644 --- a/docker-compose.yaml +++ b/quickstart/docker-compose.yaml @@ -7,4 +7,4 @@ services: - "5230:5230" volumes: - ~/.memos/:/var/opt/memos - command: --mode=prod --port=5230 \ No newline at end of file + command: --mode=prod --port=5230 diff --git a/server/user.go b/server/user.go index e6dfb63b3840d..8210e91e9441b 100644 --- a/server/user.go +++ b/server/user.go @@ -58,24 +58,29 @@ func (s *Server) registerUserRoutes(g *echo.Group) { return nil }) - g.GET("/user/:id", func(c echo.Context) error { + // GET /api/user/me is used to check if the user is logged in. + g.GET("/user/me", func(c echo.Context) error { ctx := c.Request().Context() - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err) + userID, ok := c.Get(getUserIDContextKey()).(int) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") } - user, err := s.Store.FindUser(ctx, &api.UserFind{ - ID: &id, - }) + userFind := &api.UserFind{ + ID: &userID, + } + user, err := s.Store.FindUser(ctx, userFind) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) } - if user != nil { - // data desensitize - user.OpenID = "" + userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{ + UserID: userID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) } + user.UserSettingList = userSettingList c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil { @@ -84,22 +89,54 @@ func (s *Server) registerUserRoutes(g *echo.Group) { return nil }) - // GET /api/user/me is used to check if the user is logged in. - g.GET("/user/me", func(c echo.Context) error { + g.POST("/user/setting", func(c echo.Context) error { ctx := c.Request().Context() userID, ok := c.Get(getUserIDContextKey()).(int) if !ok { return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") } - userFind := &api.UserFind{ - ID: &userID, + userSettingUpsert := &api.UserSettingUpsert{} + if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err) } - user, err := s.Store.FindUser(ctx, userFind) + + if userSettingUpsert.Key.String() == "" { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting key") + } + + userSettingUpsert.UserID = userID + userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err) + } + + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) + if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userSetting)); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user setting response").SetInternal(err) + } + return nil + }) + + g.GET("/user/:id", func(c echo.Context) error { + ctx := c.Request().Context() + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err) + } + + user, err := s.Store.FindUser(ctx, &api.UserFind{ + ID: &id, + }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err) } + if user != nil { + // data desensitize + user.OpenID = "" + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err) diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index ca0bb1480b17c..5c1bc47e97089 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -3,6 +3,7 @@ DROP TABLE IF EXISTS `memo_organizer`; DROP TABLE IF EXISTS `memo`; DROP TABLE IF EXISTS `shortcut`; DROP TABLE IF EXISTS `resource`; +DROP TABLE IF EXISTS `user_setting`; DROP TABLE IF EXISTS `user`; -- user @@ -139,3 +140,13 @@ SET WHERE rowid = old.rowid; END; + +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id); diff --git a/store/db/migration/prod/0.4/00__user_setting.sql b/store/db/migration/prod/0.4/00__user_setting.sql new file mode 100644 index 0000000000000..be65387f3c5c9 --- /dev/null +++ b/store/db/migration/prod/0.4/00__user_setting.sql @@ -0,0 +1,9 @@ +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id); diff --git a/store/user_setting.go b/store/user_setting.go new file mode 100644 index 0000000000000..2f32a1f1af913 --- /dev/null +++ b/store/user_setting.go @@ -0,0 +1,122 @@ +package store + +import ( + "context" + "database/sql" + + "github.com/usememos/memos/api" +) + +type userSettingRaw struct { + UserID int + Key api.UserSettingKey + Value string +} + +func (raw *userSettingRaw) toUserSetting() *api.UserSetting { + return &api.UserSetting{ + UserID: raw.UserID, + Key: raw.Key, + Value: raw.Value, + } +} + +func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + userSettingRaw, err := upsertUserSetting(ctx, tx, upsert) + if err != nil { + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + userSetting := userSettingRaw.toUserSetting() + + return userSetting, nil +} + +func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + userSettingRawList, err := findUserSettingList(ctx, tx, find) + if err != nil { + return nil, err + } + + list := []*api.UserSetting{} + for _, raw := range userSettingRawList { + list = append(list, raw.toUserSetting()) + } + + return list, nil +} + +func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) { + query := ` + INSERT INTO user_setting ( + user_id, key, value + ) + VALUES (?, ?, ?) + ON CONFLICT(user_id, key) DO UPDATE + SET + value = EXCLUDED.value + RETURNING user_id, key, value + ` + var userSettingRaw userSettingRaw + if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan( + &userSettingRaw.UserID, + &userSettingRaw.Key, + &userSettingRaw.Value, + ); err != nil { + return nil, FormatError(err) + } + + return &userSettingRaw, nil +} + +func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) { + query := ` + SELECT + user_id, + key, + value + FROM user_setting + WHERE user_id = ? + ` + rows, err := tx.QueryContext(ctx, query, find.UserID) + if err != nil { + return nil, FormatError(err) + } + defer rows.Close() + + userSettingRawList := make([]*userSettingRaw, 0) + for rows.Next() { + var userSettingRaw userSettingRaw + if err := rows.Scan( + &userSettingRaw.UserID, + &userSettingRaw.Key, + &userSettingRaw.Value, + ); err != nil { + return nil, FormatError(err) + } + + userSettingRawList = append(userSettingRawList, &userSettingRaw) + } + + if err := rows.Err(); err != nil { + return nil, FormatError(err) + } + + return userSettingRawList, nil +} From 13a86975d04397bbb04a004d0cb776a11113cbbb Mon Sep 17 00:00:00 2001 From: boojack Date: Sat, 13 Aug 2022 13:38:35 +0800 Subject: [PATCH 2/4] chore: add global store --- web/src/components/AboutSiteDialog.tsx | 6 +-- .../Settings/PreferencesSection.tsx | 48 +++++++++++++++---- web/src/components/common/Selector.tsx | 6 +-- web/src/helpers/api.ts | 4 ++ web/src/pages/Home.tsx | 8 ++++ web/src/services/globalService.ts | 39 +++++++++++++++ web/src/services/userService.ts | 25 +++++++++- web/src/store/index.ts | 2 + web/src/store/modules/global.ts | 22 +++++++++ web/src/types/i18n.d.ts | 1 + web/src/types/modules/setting.d.ts | 15 ++++++ web/src/types/modules/user.d.ts | 3 ++ 12 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 web/src/services/globalService.ts create mode 100644 web/src/store/modules/global.ts create mode 100644 web/src/types/i18n.d.ts create mode 100644 web/src/types/modules/setting.d.ts diff --git a/web/src/components/AboutSiteDialog.tsx b/web/src/components/AboutSiteDialog.tsx index fa4ed6ed9bbbe..109e66ca94269 100644 --- a/web/src/components/AboutSiteDialog.tsx +++ b/web/src/components/AboutSiteDialog.tsx @@ -10,8 +10,8 @@ import "../less/about-site-dialog.less"; interface Props extends DialogProps {} const AboutSiteDialog: React.FC = ({ destroy }: Props) => { + const { t } = useI18n(); const [profile, setProfile] = useState(); - const { t, setLocale } = useI18n(); useEffect(() => { try { @@ -27,10 +27,6 @@ const AboutSiteDialog: React.FC = ({ destroy }: Props) => { version: "0.0.0", }); } - - setTimeout(() => { - setLocale("zh"); - }, 2333); }, []); const handleCloseBtnClick = () => { diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 4aaff607d300a..b8162dfca4c19 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,11 +1,28 @@ -import { memoService } from "../../services"; +import { useEffect, useState } from "react"; +import { memoService, userService } from "../../services"; import * as utils from "../../helpers/utils"; +import { useAppSelector } from "../../store"; +import Only from "../common/OnlyWhen"; import toastHelper from "../Toast"; import "../../less/settings/preferences-section.less"; +import Selector from "../common/Selector"; interface Props {} +const localeSelectorItems = [ + { + text: "English", + value: "en", + }, + { + text: "中文", + value: "zh", + }, +]; + const PreferencesSection: React.FC = () => { + const { setting } = useAppSelector((state) => state.user.user as User); + const handleExportBtnClick = async () => { const formatedMemos = memoService.getState().memos.map((m) => { return { @@ -64,17 +81,28 @@ const PreferencesSection: React.FC = () => { fileInputEl.click(); }; + const handleLocaleChanged = async (value: string) => { + await userService.upsertUserSetting("locale", value); + }; + return (
-

Others

-
- - -
+ {/* Hide export/import buttons */} + + +

Others

+
+ + +
+
); }; diff --git a/web/src/components/common/Selector.tsx b/web/src/components/common/Selector.tsx index d1bf80bef1a36..4224845d8a32e 100644 --- a/web/src/components/common/Selector.tsx +++ b/web/src/components/common/Selector.tsx @@ -3,7 +3,7 @@ import useToggle from "../../hooks/useToggle"; import Icon from "../Icon"; import "../../less/common/selector.less"; -interface TVObject { +interface SelectorItem { text: string; value: string; } @@ -11,7 +11,7 @@ interface TVObject { interface Props { className?: string; value: string; - dataSource: TVObject[]; + dataSource: SelectorItem[]; handleValueChanged?: (value: string) => void; } @@ -48,7 +48,7 @@ const Selector: React.FC = (props: Props) => { } }, [showSelector]); - const handleItemClick = (item: TVObject) => { + const handleItemClick = (item: SelectorItem) => { if (handleValueChanged) { handleValueChanged(item.value); } diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 5e47b9c869187..8869037012c43 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -46,6 +46,10 @@ export function getUserById(id: number) { return axios.get>(`/api/user/${id}`); } +export function upsertUserSetting(upsert: UserSettingUpsert) { + return axios.post>(`/api/user/setting`, upsert); +} + export function patchUser(userPatch: UserPatch) { return axios.patch>(`/api/user/${userPatch.id}`, userPatch); } diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index b0406c2eabd41..3163a334e9376 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,6 +1,7 @@ import { useEffect } from "react"; import { locationService, userService } from "../services"; import { useAppSelector } from "../store"; +import useI18n from "../hooks/useI18n"; import useLoading from "../hooks/useLoading"; import Only from "../components/common/OnlyWhen"; import Sidebar from "../components/Sidebar"; @@ -12,6 +13,7 @@ import toastHelper from "../components/Toast"; import "../less/home.less"; function Home() { + const { setLocale } = useI18n(); const user = useAppSelector((state) => state.user.user); const location = useAppSelector((state) => state.location); const loadingState = useLoading(); @@ -40,6 +42,12 @@ function Home() { }); }, [location]); + useEffect(() => { + if (user?.setting) { + setLocale(user.setting.locale); + } + }, [user?.setting]); + return (
{loadingState.isLoading ? null : ( diff --git a/web/src/services/globalService.ts b/web/src/services/globalService.ts new file mode 100644 index 0000000000000..8d5b12cb5e7a6 --- /dev/null +++ b/web/src/services/globalService.ts @@ -0,0 +1,39 @@ +import store from "../store"; +import * as api from "../helpers/api"; +import { setLocale } from "../store/modules/global"; +import { setHost, setOwner, setUser } from "../store/modules/user"; +import userService, { convertResponseModelUser } from "./userService"; + +const globalService = { + getState: () => { + return store.getState().global; + }, + + initialState: async () => { + const { + data: { host }, + } = (await api.getSystemStatus()).data; + if (host) { + store.dispatch(setHost(convertResponseModelUser(host))); + } + + const ownerUserId = userService.getUserIdFromPath(); + if (ownerUserId) { + const { data: owner } = (await api.getUserById(ownerUserId)).data; + if (owner) { + store.dispatch(setOwner(convertResponseModelUser(owner))); + } + } + + const { data: user } = (await api.getMyselfUser()).data; + if (user) { + store.dispatch(setUser(convertResponseModelUser(user))); + } + }, + + setLocale: (locale: Locale) => { + store.dispatch(setLocale(locale)); + }, +}; + +export default globalService; diff --git a/web/src/services/userService.ts b/web/src/services/userService.ts index 9a745d24a038a..706b6ffa871ef 100644 --- a/web/src/services/userService.ts +++ b/web/src/services/userService.ts @@ -4,9 +4,24 @@ import * as api from "../helpers/api"; import store from "../store"; import { setUser, patchUser, setHost, setOwner } from "../store/modules/user"; -const convertResponseModelUser = (user: User): User => { +const defauleSetting: Setting = { + locale: "en", +}; + +export const convertResponseModelUser = (user: User): User => { + const setting: Setting = { + ...defauleSetting, + }; + + if (user.userSettingList) { + for (const userSetting of user.userSettingList) { + setting[userSetting.key] = JSON.parse(userSetting.value); + } + } + return { ...user, + setting, createdTs: user.createdTs * 1000, updatedTs: user.updatedTs * 1000, }; @@ -76,6 +91,14 @@ const userService = { } }, + upsertUserSetting: async (key: string, value: any) => { + await api.upsertUserSetting({ + key: key as any, + value: JSON.stringify(value), + }); + await userService.doSignIn(); + }, + patchUser: async (userPatch: UserPatch): Promise => { const { data } = (await api.patchUser(userPatch)).data; if (userPatch.id === store.getState().user.user?.id) { diff --git a/web/src/store/index.ts b/web/src/store/index.ts index bce86eedaa9e0..61b8b61d1de7d 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -1,5 +1,6 @@ import { configureStore } from "@reduxjs/toolkit"; import { TypedUseSelectorHook, useSelector } from "react-redux"; +import globalReducer from "./modules/global"; import userReducer from "./modules/user"; import memoReducer from "./modules/memo"; import editorReducer from "./modules/editor"; @@ -8,6 +9,7 @@ import locationReducer from "./modules/location"; const store = configureStore({ reducer: { + global: globalReducer, user: userReducer, memo: memoReducer, editor: editorReducer, diff --git a/web/src/store/modules/global.ts b/web/src/store/modules/global.ts new file mode 100644 index 0000000000000..e31711f332883 --- /dev/null +++ b/web/src/store/modules/global.ts @@ -0,0 +1,22 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + locale: Locale; +} + +const globalSlice = createSlice({ + name: "global", + initialState: {} as State, + reducers: { + setLocale: (state, action: PayloadAction) => { + return { + ...state, + locale: action.payload, + }; + }, + }, +}); + +export const { setLocale } = globalSlice.actions; + +export default globalSlice.reducer; diff --git a/web/src/types/i18n.d.ts b/web/src/types/i18n.d.ts new file mode 100644 index 0000000000000..32c3be6e0e740 --- /dev/null +++ b/web/src/types/i18n.d.ts @@ -0,0 +1 @@ +type Locale = "en" | "zh"; diff --git a/web/src/types/modules/setting.d.ts b/web/src/types/modules/setting.d.ts new file mode 100644 index 0000000000000..b90e62083a32b --- /dev/null +++ b/web/src/types/modules/setting.d.ts @@ -0,0 +1,15 @@ +interface Setting { + locale: "en" | "zh"; +} + +interface UserLocaleSetting { + key: "locale"; + value: "en" | "zh"; +} + +type UserSetting = UserLocaleSetting; + +interface UserSettingUpsert { + key: keyof Setting; + value: string; +} diff --git a/web/src/types/modules/user.d.ts b/web/src/types/modules/user.d.ts index ef55bc4cb0365..88d458c4369a4 100644 --- a/web/src/types/modules/user.d.ts +++ b/web/src/types/modules/user.d.ts @@ -12,6 +12,9 @@ interface User { email: string; name: string; openId: string; + userSettingList: UserSetting[]; + + setting: Setting; } interface UserCreate { From dbcca202e86e2f7340ff38a09906a6f1c43d334b Mon Sep 17 00:00:00 2001 From: boojack Date: Sat, 13 Aug 2022 14:11:20 +0800 Subject: [PATCH 3/4] chore: update settings in web --- web/src/App.tsx | 17 ++++++++- .../Settings/PreferencesSection.tsx | 6 +-- web/src/helpers/storage.ts | 5 +-- web/src/pages/Home.tsx | 8 ---- web/src/services/globalService.ts | 38 ++++++++++--------- web/src/services/index.ts | 3 +- web/src/store/modules/global.ts | 5 ++- 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 19bd2a616b096..e67a278a77e7a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,9 +1,13 @@ import { useEffect, useState } from "react"; +import useI18n from "./hooks/useI18n"; import { appRouterSwitch } from "./routers"; -import { locationService } from "./services"; +import { globalService, locationService } from "./services"; import { useAppSelector } from "./store"; +import * as storage from "./helpers/storage"; function App() { + const { setLocale } = useI18n(); + const global = useAppSelector((state) => state.global); const pathname = useAppSelector((state) => state.location.pathname); const [isLoading, setLoading] = useState(true); @@ -12,9 +16,18 @@ function App() { window.onpopstate = () => { locationService.updateStateWithLocation(); }; - setLoading(false); + globalService.initialState().then(() => { + setLoading(false); + }); }, []); + useEffect(() => { + setLocale(global.locale); + storage.set({ + locale: global.locale, + }); + }, [global]); + return <>{isLoading ? null : appRouterSwitch(pathname)}; } diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index b8162dfca4c19..0916d8c85ba15 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,11 +1,10 @@ -import { useEffect, useState } from "react"; -import { memoService, userService } from "../../services"; +import { globalService, memoService, userService } from "../../services"; import * as utils from "../../helpers/utils"; import { useAppSelector } from "../../store"; import Only from "../common/OnlyWhen"; import toastHelper from "../Toast"; -import "../../less/settings/preferences-section.less"; import Selector from "../common/Selector"; +import "../../less/settings/preferences-section.less"; interface Props {} @@ -82,6 +81,7 @@ const PreferencesSection: React.FC = () => { }; const handleLocaleChanged = async (value: string) => { + globalService.setLocale(value as Locale); await userService.upsertUserSetting("locale", value); }; diff --git a/web/src/helpers/storage.ts b/web/src/helpers/storage.ts index 391539aa67fa4..e767ecfe81f89 100644 --- a/web/src/helpers/storage.ts +++ b/web/src/helpers/storage.ts @@ -4,9 +4,8 @@ interface StorageData { // Editor content cache editorContentCache: string; - shouldSplitMemoWord: boolean; - shouldHideImageUrl: boolean; - shouldUseMarkdownParser: boolean; + // locale + locale: Locale; } type StorageKey = keyof StorageData; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 3163a334e9376..b0406c2eabd41 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; import { locationService, userService } from "../services"; import { useAppSelector } from "../store"; -import useI18n from "../hooks/useI18n"; import useLoading from "../hooks/useLoading"; import Only from "../components/common/OnlyWhen"; import Sidebar from "../components/Sidebar"; @@ -13,7 +12,6 @@ import toastHelper from "../components/Toast"; import "../less/home.less"; function Home() { - const { setLocale } = useI18n(); const user = useAppSelector((state) => state.user.user); const location = useAppSelector((state) => state.location); const loadingState = useLoading(); @@ -42,12 +40,6 @@ function Home() { }); }, [location]); - useEffect(() => { - if (user?.setting) { - setLocale(user.setting.locale); - } - }, [user?.setting]); - return (
{loadingState.isLoading ? null : ( diff --git a/web/src/services/globalService.ts b/web/src/services/globalService.ts index 8d5b12cb5e7a6..eb53f246d382f 100644 --- a/web/src/services/globalService.ts +++ b/web/src/services/globalService.ts @@ -1,8 +1,8 @@ import store from "../store"; import * as api from "../helpers/api"; -import { setLocale } from "../store/modules/global"; -import { setHost, setOwner, setUser } from "../store/modules/user"; -import userService, { convertResponseModelUser } from "./userService"; +import * as storage from "../helpers/storage"; +import { setGlobalState, setLocale } from "../store/modules/global"; +import { convertResponseModelUser } from "./userService"; const globalService = { getState: () => { @@ -10,25 +10,27 @@ const globalService = { }, initialState: async () => { - const { - data: { host }, - } = (await api.getSystemStatus()).data; - if (host) { - store.dispatch(setHost(convertResponseModelUser(host))); - } + const defaultGlobalState = { + locale: "en" as Locale, + }; - const ownerUserId = userService.getUserIdFromPath(); - if (ownerUserId) { - const { data: owner } = (await api.getUserById(ownerUserId)).data; - if (owner) { - store.dispatch(setOwner(convertResponseModelUser(owner))); + const { locale: storageLocale } = storage.get(["locale"]); + if (storageLocale) { + defaultGlobalState.locale = storageLocale; + } + try { + const { data } = (await api.getMyselfUser()).data; + if (data) { + const user = convertResponseModelUser(data); + if (user.setting.locale) { + defaultGlobalState.locale = user.setting.locale; + } } + } catch (error) { + // do nth } - const { data: user } = (await api.getMyselfUser()).data; - if (user) { - store.dispatch(setUser(convertResponseModelUser(user))); - } + store.dispatch(setGlobalState(defaultGlobalState)); }, setLocale: (locale: Locale) => { diff --git a/web/src/services/index.ts b/web/src/services/index.ts index 4ecd0580d07f4..2ddfd65f8c6c4 100644 --- a/web/src/services/index.ts +++ b/web/src/services/index.ts @@ -1,3 +1,4 @@ +import globalService from "./globalService"; import editorStateService from "./editorStateService"; import locationService from "./locationService"; import memoService from "./memoService"; @@ -5,4 +6,4 @@ import shortcutService from "./shortcutService"; import userService from "./userService"; import resourceService from "./resourceService"; -export { editorStateService, locationService, memoService, shortcutService, userService, resourceService }; +export { globalService, editorStateService, locationService, memoService, shortcutService, userService, resourceService }; diff --git a/web/src/store/modules/global.ts b/web/src/store/modules/global.ts index e31711f332883..d9eceacee2e1e 100644 --- a/web/src/store/modules/global.ts +++ b/web/src/store/modules/global.ts @@ -8,6 +8,9 @@ const globalSlice = createSlice({ name: "global", initialState: {} as State, reducers: { + setGlobalState: (_, action: PayloadAction) => { + return action.payload; + }, setLocale: (state, action: PayloadAction) => { return { ...state, @@ -17,6 +20,6 @@ const globalSlice = createSlice({ }, }); -export const { setLocale } = globalSlice.actions; +export const { setGlobalState, setLocale } = globalSlice.actions; export default globalSlice.reducer; From d4c14d19e5a58ad5134e205beaf4fe2a6a057144 Mon Sep 17 00:00:00 2001 From: boojack Date: Sat, 13 Aug 2022 14:34:06 +0800 Subject: [PATCH 4/4] chore: update `i18n` example --- web/src/components/AboutSiteDialog.tsx | 2 +- web/src/labs/i18n/useI18n.ts | 15 ++++++++++++--- web/src/locales/en.json | 7 ++++++- web/src/locales/zh.json | 7 ++++++- web/src/pages/Signin.tsx | 8 +++++--- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/web/src/components/AboutSiteDialog.tsx b/web/src/components/AboutSiteDialog.tsx index 109e66ca94269..869baa0b7ef8e 100644 --- a/web/src/components/AboutSiteDialog.tsx +++ b/web/src/components/AboutSiteDialog.tsx @@ -38,7 +38,7 @@ const AboutSiteDialog: React.FC = ({ destroy }: Props) => {

🤠 - {t("about")} Memos + {t("common.about")} Memos

- Email + {t("common.email")}
- Password + {t("common.password")}
@@ -141,7 +143,7 @@ const Signin: React.FC = () => { className={`btn signin-btn ${actionBtnLoadingState.isLoading ? "requesting" : ""}`} onClick={() => handleSigninBtnsClick()} > - Sign in + {t("common.sign-in")} ) : (