diff --git a/desktop/renderer-app/src/api-middleware/flatServer/constants.ts b/desktop/renderer-app/src/api-middleware/flatServer/constants.ts index 95158219157..cf91771d492 100644 --- a/desktop/renderer-app/src/api-middleware/flatServer/constants.ts +++ b/desktop/renderer-app/src/api-middleware/flatServer/constants.ts @@ -43,11 +43,6 @@ export enum Status { AuthFailed, } -export enum Sex { - Man = "Man", - Woman = "Woman", -} - export enum FileConvertStep { None = "None", Converting = "Converting", diff --git a/desktop/renderer-app/src/api-middleware/flatServer/index.ts b/desktop/renderer-app/src/api-middleware/flatServer/index.ts index 5fbc7d9b801..4dcb05feb7a 100644 --- a/desktop/renderer-app/src/api-middleware/flatServer/index.ts +++ b/desktop/renderer-app/src/api-middleware/flatServer/index.ts @@ -1,5 +1,5 @@ import { Region } from "flat-components"; -import { RoomStatus, RoomType, Sex, Week } from "./constants"; +import { RoomStatus, RoomType, Week } from "./constants"; import { post, postNotAuth } from "./utils"; export interface CreateOrdinaryRoomPayload { @@ -451,22 +451,18 @@ export async function updatePeriodicSubRoom(payload: UpdatePeriodicSubRoomPayloa ); } -export interface LoginCheckPayload { - type: "web" | "mobile"; -} +export interface LoginCheckPayload {} export interface LoginCheckResult { name: string; - sex: Sex; avatar: string; + token: string; userUUID: string; hasPhone: boolean; } export async function loginCheck(): Promise { - return await post("login", { - type: "web", - }); + return await post("login", {}); } export interface setAuthUUIDPayload { @@ -489,7 +485,6 @@ export interface LoginProcessPayload { export interface LoginProcessResult { name: string; - sex: Sex; avatar: string; userUUID: string; token: string; @@ -557,3 +552,15 @@ export async function bindingPhone(phone: string, code: number): Promise { + return await post("user/rename", { + name, + }); +} diff --git a/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx b/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx index 15ee24e048a..cf87c5f1f2d 100644 --- a/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx +++ b/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx @@ -21,6 +21,7 @@ import { SVGLogout, WindowsSystemBtnItem, } from "flat-components"; +import { observer } from "mobx-react-lite"; import { useTranslation } from "react-i18next"; import { routeConfig, RouteNameType } from "../../route-config"; import { GlobalStoreContext } from "../StoreProvider"; @@ -34,119 +35,116 @@ export interface MainPageLayoutContainerProps { onRouteChange?: MainPageLayoutProps["onClick"]; } -export const MainPageLayoutContainer: React.FC = ({ - subMenu, - children, - activeKeys, - onRouteChange, -}) => { - const { t } = useTranslation(); - const sideMenu = [ - { - key: routeConfig[RouteNameType.HomePage].path, - title: "home", - route: routeConfig[RouteNameType.HomePage].path, - icon: (active: boolean): React.ReactNode => { - return active ? : ; +export const MainPageLayoutContainer = observer( + function MainPageLayoutContainer({ subMenu, children, activeKeys, onRouteChange }) { + const { t } = useTranslation(); + const sideMenu = [ + { + key: routeConfig[RouteNameType.HomePage].path, + title: "home", + route: routeConfig[RouteNameType.HomePage].path, + icon: (active: boolean): React.ReactNode => { + return active ? : ; + }, }, - }, - { - key: routeConfig[RouteNameType.CloudStoragePage].path, - title: "cloudStorage", - route: routeConfig[RouteNameType.CloudStoragePage].path, - icon: (active: boolean): React.ReactNode => { - return active ? : ; + { + key: routeConfig[RouteNameType.CloudStoragePage].path, + title: "cloudStorage", + route: routeConfig[RouteNameType.CloudStoragePage].path, + icon: (active: boolean): React.ReactNode => { + return active ? : ; + }, }, - }, - ]; + ]; - const sideMenuFooter = [ - { - key: "deviceCheck", - title: "deviceCheck", - route: routeConfig[RouteNameType.SystemCheckPage].path, - icon: (active: boolean): React.ReactNode => { - return active ? : ; + const sideMenuFooter = [ + { + key: "deviceCheck", + title: "deviceCheck", + route: routeConfig[RouteNameType.SystemCheckPage].path, + icon: (active: boolean): React.ReactNode => { + return active ? : ; + }, }, - }, - ]; + ]; - const popMenu = [ - { - key: routeConfig[RouteNameType.GeneralSettingPage].path, - icon: (): React.ReactNode => , - title: t("settings"), - route: routeConfig[RouteNameType.GeneralSettingPage].path, - }, - { - key: "feedback", - icon: (): React.ReactNode => , - title: t("feedback"), - route: "https://github.com/netless-io/flat/issues", - }, - { - key: "logout", - icon: (): React.ReactNode => , - title: {t("logout")}, - route: routeConfig[RouteNameType.LoginPage].path, - }, - ]; + const popMenu = [ + { + key: routeConfig[RouteNameType.GeneralSettingPage].path, + icon: (): React.ReactNode => , + title: t("settings"), + route: routeConfig[RouteNameType.GeneralSettingPage].path, + }, + { + key: "feedback", + icon: (): React.ReactNode => , + title: t("feedback"), + route: "https://github.com/netless-io/flat/issues", + }, + { + key: "logout", + icon: (): React.ReactNode => , + title: {t("logout")}, + route: routeConfig[RouteNameType.LoginPage].path, + }, + ]; - const location = useLocation(); + const location = useLocation(); - activeKeys ??= [location.pathname]; + activeKeys ??= [location.pathname]; - const history = useHistory(); + const history = useHistory(); - const globalStore = useContext(GlobalStoreContext); + const globalStore = useContext(GlobalStoreContext); - const onMenuItemClick = (mainPageLayoutItem: MainPageLayoutItem): void => { - if (mainPageLayoutItem.key === "logout") { - globalStore.logout(); - } + const onMenuItemClick = (mainPageLayoutItem: MainPageLayoutItem): void => { + if (mainPageLayoutItem.key === "logout") { + globalStore.logout(); + } - if (mainPageLayoutItem.route.startsWith("/")) { - onRouteChange - ? onRouteChange(mainPageLayoutItem) - : history.push(mainPageLayoutItem.route); - } else { - void shell.openExternal(mainPageLayoutItem.route); - } - }; + if (mainPageLayoutItem.route.startsWith("/")) { + onRouteChange + ? onRouteChange(mainPageLayoutItem) + : history.push(mainPageLayoutItem.route); + } else { + void shell.openExternal(mainPageLayoutItem.route); + } + }; - const topBarMenu = [ - { - key: "github", - icon: , - route: "https://github.com/netless-io/flat/", - }, - ]; + const topBarMenu = [ + { + key: "github", + icon: , + route: "https://github.com/netless-io/flat/", + }, + ]; - const onClickTopBarMenu = (mainPageTopBarMenuItem: MainPageTopBarMenuItem): void => { - void shell.openExternal(mainPageTopBarMenuItem.route); - }; + const onClickTopBarMenu = (mainPageTopBarMenuItem: MainPageTopBarMenuItem): void => { + void shell.openExternal(mainPageTopBarMenuItem.route); + }; - const onClickWindowsSystemBtn = (winSystemBtn: WindowsSystemBtnItem): void => { - ipcAsyncByMainWindow("set-win-status", { windowStatus: winSystemBtn }); - }; + const onClickWindowsSystemBtn = (winSystemBtn: WindowsSystemBtnItem): void => { + ipcAsyncByMainWindow("set-win-status", { windowStatus: winSystemBtn }); + }; - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }, +); diff --git a/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/ConfirmButtons.tsx b/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/ConfirmButtons.tsx new file mode 100644 index 00000000000..c1b19bf6606 --- /dev/null +++ b/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/ConfirmButtons.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useState } from "react"; +import { Button } from "antd"; +import { useTranslation } from "react-i18next"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; + +export interface ConfirmButtonsProps { + onConfirm: () => Promise; +} + +export const ConfirmButtons: React.FC = ({ onConfirm }) => { + const sp = useSafePromise(); + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [phase, setPhase] = useState<"idle" | "confirm">("idle"); + + const confirm = useCallback(async () => { + setLoading(true); + await sp(onConfirm()); + setLoading(false); + setPhase("idle"); + }, [onConfirm, sp]); + + if (phase === "idle") { + return ( + + ); + } else { + return ( + <> + + + + ); + } +}; diff --git a/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/index.tsx b/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/index.tsx index a6cce4b4edb..a5e6fc0489b 100644 --- a/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/index.tsx +++ b/desktop/renderer-app/src/pages/UserSettingPage/GeneralSettingPage/index.tsx @@ -1,12 +1,15 @@ import "./style.less"; -import { Checkbox, Radio, RadioChangeEvent } from "antd"; import React, { useContext, useEffect, useState } from "react"; +import { Checkbox, Input, Radio, RadioChangeEvent } from "antd"; import { UserSettingLayoutContainer } from "../UserSettingLayoutContainer"; import { ipcSyncByApp, ipcAsyncByApp } from "../../../utils/ipc"; import { useTranslation } from "react-i18next"; import { AppearancePicker, FlatPrefersColorScheme } from "flat-components"; -import { ConfigStoreContext } from "../../../components/StoreProvider"; +import { ConfigStoreContext, GlobalStoreContext } from "../../../components/StoreProvider"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; +import { loginCheck, rename } from "../../../api-middleware/flatServer"; +import { ConfirmButtons } from "./ConfirmButtons"; enum SelectLanguage { Chinese, @@ -14,9 +17,26 @@ enum SelectLanguage { } export const GeneralSettingPage = (): React.ReactElement => { + const sp = useSafePromise(); const { t, i18n } = useTranslation(); const [openAtLogin, setOpenAtLogin] = useState(false); const configStore = useContext(ConfigStoreContext); + const globalStore = useContext(GlobalStoreContext); + + const [name, setName] = useState(globalStore.userName || ""); + const [isRenaming, setRenaming] = useState(false); + + async function changeUserName(): Promise { + if (name !== globalStore.userName) { + setRenaming(true); + await sp(rename(name)); + setRenaming(false); + // Refresh user info in global store. + const result = await sp(loginCheck()); + globalStore.updateUserInfo(result); + globalStore.updateLastLoginCheck(Date.now()); + } + } useEffect(() => { ipcSyncByApp("get-open-at-login") @@ -55,6 +75,17 @@ export const GeneralSettingPage = (): React.ReactElement => { +
+ {t("user-profile")} + setName(ev.currentTarget.value)} + /> + +
{t("language-settings")} span { + display: block; + padding-bottom: 4px; + } + > label { + padding-right: 16px; + color: var(--text); + } + .ant-input { + width: 320px; + margin-bottom: 12px; + } + .ant-input, + .ant-btn { + margin-right: 8px; + } +} + .general-setting-checkbox { - margin-bottom: 32px; + margin-bottom: 8px; } .general-setting-select-language { diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index f22f6509b5b..101d65f78b8 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -453,5 +453,6 @@ "math_1": "Coordinate Axis", "english_1": "Four Lines and Three Grids", "chinese_1": "Tin Word Format" - } + }, + "user-profile": "User Profile" } diff --git a/packages/flat-i18n/locales/zh-CN.json b/packages/flat-i18n/locales/zh-CN.json index 70ca14f3c98..46db3c355d8 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -453,5 +453,6 @@ "math_1": "坐标系", "english_1": "四线三格", "chinese_1": "田字格" - } + }, + "user-profile": "用户资料" } diff --git a/web/flat-web/src/api-middleware/flatServer/index.ts b/web/flat-web/src/api-middleware/flatServer/index.ts index 3aad93af130..ff9792ade3d 100644 --- a/web/flat-web/src/api-middleware/flatServer/index.ts +++ b/web/flat-web/src/api-middleware/flatServer/index.ts @@ -571,3 +571,15 @@ export async function bindingPhone(phone: string, code: number): Promise { + return await post("user/rename", { + name, + }); +} diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx index 4965f286224..dd30539c5db 100644 --- a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx @@ -1,5 +1,6 @@ /* eslint react/display-name: off */ import React, { useContext } from "react"; +import { observer } from "mobx-react-lite"; import { useHistory, useLocation } from "react-router-dom"; import { MainPageLayoutHorizontal, @@ -29,102 +30,109 @@ export interface MainPageLayoutHorizontalContainerProps { onBackPreviousPage?: () => void; } -export const MainPageLayoutHorizontalContainer: React.FC< - MainPageLayoutHorizontalContainerProps -> = ({ subMenu, children, activeKeys, onRouteChange, title, onBackPreviousPage }) => { - const { t } = useTranslation(); - const leftMenu = [ - { - key: routeConfig[RouteNameType.HomePage].path, - icon: (active: boolean): React.ReactNode => { - return active ? : ; +export const MainPageLayoutHorizontalContainer = observer( + function MainPageLayoutHorizontalContainer({ + subMenu, + children, + activeKeys, + onRouteChange, + title, + onBackPreviousPage, + }) { + const { t } = useTranslation(); + const leftMenu = [ + { + key: routeConfig[RouteNameType.HomePage].path, + icon: (active: boolean): React.ReactNode => { + return active ? : ; + }, + title: t("home"), + route: routeConfig[RouteNameType.HomePage].path, }, - title: t("home"), - route: routeConfig[RouteNameType.HomePage].path, - }, - { - key: routeConfig[RouteNameType.CloudStoragePage].path, - icon: (active: boolean): React.ReactNode => { - return active ? : ; + { + key: routeConfig[RouteNameType.CloudStoragePage].path, + icon: (active: boolean): React.ReactNode => { + return active ? : ; + }, + title: t("cloud-storage"), + route: routeConfig[RouteNameType.CloudStoragePage].path, }, - title: t("cloud-storage"), - route: routeConfig[RouteNameType.CloudStoragePage].path, - }, - ]; + ]; - const rightMenu: MainPageLayoutItem[] = [ - { - key: "download", - icon: (): React.ReactNode => , - title: <>, - route: FLAT_DOWNLOAD_URL, - }, - { - key: "getGitHubCode", - icon: (): React.ReactNode => , - title: <>, - route: "https://github.com/netless-io/flat/", - }, - { - key: routeConfig[RouteNameType.GeneralSettingPage].path, - icon: (): React.ReactNode => , - title: <>, - route: routeConfig[RouteNameType.GeneralSettingPage].path, - }, - ]; + const rightMenu: MainPageLayoutItem[] = [ + { + key: "download", + icon: (): React.ReactNode => , + title: <>, + route: FLAT_DOWNLOAD_URL, + }, + { + key: "getGitHubCode", + icon: (): React.ReactNode => , + title: <>, + route: "https://github.com/netless-io/flat/", + }, + { + key: routeConfig[RouteNameType.GeneralSettingPage].path, + icon: (): React.ReactNode => , + title: <>, + route: routeConfig[RouteNameType.GeneralSettingPage].path, + }, + ]; - const popMenu = [ - { - key: "feedback", - icon: (): React.ReactNode => , - title: t("feedback"), - route: "https://github.com/netless-io/flat/issues", - }, - { - key: "logout", - icon: (): React.ReactNode => , - title: {t("logout")}, - route: routeConfig[RouteNameType.LoginPage].path, - }, - ]; + const popMenu = [ + { + key: "feedback", + icon: (): React.ReactNode => , + title: t("feedback"), + route: "https://github.com/netless-io/flat/issues", + }, + { + key: "logout", + icon: (): React.ReactNode => , + title: {t("logout")}, + route: routeConfig[RouteNameType.LoginPage].path, + }, + ]; - const location = useLocation(); + const location = useLocation(); - activeKeys ??= [location.pathname]; + activeKeys ??= [location.pathname]; - const history = useHistory(); + const history = useHistory(); - const globalStore = useContext(GlobalStoreContext); + const globalStore = useContext(GlobalStoreContext); - const onMenuItemClick = (mainPageLayoutItem: MainPageLayoutItem): void => { - if (mainPageLayoutItem.key === "logout") { - globalStore.logout(); - } + const onMenuItemClick = (mainPageLayoutItem: MainPageLayoutItem): void => { + if (mainPageLayoutItem.key === "logout") { + globalStore.logout(); + } - if (mainPageLayoutItem.route.startsWith("/")) { - onRouteChange - ? onRouteChange(mainPageLayoutItem) - : history.push(mainPageLayoutItem.route); - } else { - void window.open(mainPageLayoutItem.route); - } - }; + if (mainPageLayoutItem.route.startsWith("/")) { + onRouteChange + ? onRouteChange(mainPageLayoutItem) + : history.push(mainPageLayoutItem.route); + } else { + void window.open(mainPageLayoutItem.route); + } + }; - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }, +); diff --git a/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/ConfirmButtons.tsx b/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/ConfirmButtons.tsx new file mode 100644 index 00000000000..c1b19bf6606 --- /dev/null +++ b/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/ConfirmButtons.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useState } from "react"; +import { Button } from "antd"; +import { useTranslation } from "react-i18next"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; + +export interface ConfirmButtonsProps { + onConfirm: () => Promise; +} + +export const ConfirmButtons: React.FC = ({ onConfirm }) => { + const sp = useSafePromise(); + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [phase, setPhase] = useState<"idle" | "confirm">("idle"); + + const confirm = useCallback(async () => { + setLoading(true); + await sp(onConfirm()); + setLoading(false); + setPhase("idle"); + }, [onConfirm, sp]); + + if (phase === "idle") { + return ( + + ); + } else { + return ( + <> + + + + ); + } +}; diff --git a/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.less b/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.less index 0d486b67605..c728f4375eb 100644 --- a/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.less +++ b/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.less @@ -5,6 +5,25 @@ } } +.general-setting-user-profile { + > span { + display: block; + padding-bottom: 4px; + } + > label { + padding-right: 16px; + color: var(--text); + } + .ant-input { + width: 320px; + margin-bottom: 12px; + } + .ant-input, + .ant-btn { + margin-right: 8px; + } +} + .general-setting-select-language { > span { display: block; diff --git a/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.tsx b/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.tsx index a22c888c2b5..e13afbde344 100644 --- a/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.tsx +++ b/web/flat-web/src/pages/UserSettingPage/GeneralSettingPage/index.tsx @@ -1,12 +1,15 @@ import "./index.less"; +import React, { useContext, useState } from "react"; +import { Checkbox, Input, Radio } from "antd"; import { FlatPrefersColorScheme, AppearancePicker } from "flat-components"; -import { Checkbox, Radio } from "antd"; -import React, { useContext } from "react"; import { UserSettingLayoutContainer } from "../UserSettingLayoutContainer"; import { useTranslation } from "react-i18next"; import type { CheckboxChangeEvent } from "antd/lib/checkbox"; import { ConfigStoreContext, GlobalStoreContext } from "../../../components/StoreProvider"; +import { useSafePromise } from "../../../utils/hooks/lifecycle"; +import { loginCheck, rename } from "../../../api-middleware/flatServer"; +import { ConfirmButtons } from "./ConfirmButtons"; enum SelectLanguage { Chinese, @@ -17,8 +20,24 @@ export const GeneralSettingPage = (): React.ReactElement => { const globalStore = useContext(GlobalStoreContext); const configStore = useContext(ConfigStoreContext); + const sp = useSafePromise(); const { t, i18n } = useTranslation(); + const [name, setName] = useState(globalStore.userName || ""); + const [isRenaming, setRenaming] = useState(false); + + async function changeUserName(): Promise { + if (name !== globalStore.userName) { + setRenaming(true); + await sp(rename(name)); + setRenaming(false); + // Refresh user info in global store. + const result = await sp(loginCheck()); + globalStore.updateUserInfo(result); + globalStore.updateLastLoginCheck(Date.now()); + } + } + function changeLanguage(event: CheckboxChangeEvent): void { const language: SelectLanguage = event.target.value; void i18n.changeLanguage(language === SelectLanguage.Chinese ? "zh-CN" : "en"); @@ -32,6 +51,17 @@ export const GeneralSettingPage = (): React.ReactElement => { return (
+
+ {t("user-profile")} + setName(ev.currentTarget.value)} + /> + +
{t("language-settings")}