diff --git a/desktop/renderer-app/src/pages/utils/join-room-handler.ts b/desktop/renderer-app/src/pages/utils/join-room-handler.ts index 93251b751ad..f5df2cbecbc 100644 --- a/desktop/renderer-app/src/pages/utils/join-room-handler.ts +++ b/desktop/renderer-app/src/pages/utils/join-room-handler.ts @@ -1,6 +1,7 @@ -import { RoomType } from "@netless/flat-server-api"; +import { RequestErrorCode, RoomType, isPmiRoom } from "@netless/flat-server-api"; import { roomStore, globalStore } from "@netless/flat-stores"; -import { errorTips } from "flat-components"; +import { errorTips, message } from "flat-components"; +import { FlatI18n } from "@netless/flat-i18n"; import { RouteNameType } from "../../route-config"; import { usePushHistory } from "../../utils/routes"; @@ -8,8 +9,9 @@ export const joinRoomHandler = async ( roomUUID: string, pushHistory: ReturnType, ): Promise => { + const formatRoomUUID = roomUUID.replace(/\s+/g, ""); + try { - const formatRoomUUID = roomUUID.replace(/\s+/g, ""); const roomInfo = roomStore.rooms.get(formatRoomUUID); const periodicUUID = roomInfo?.periodicUUID; const data = await roomStore.joinRoom(periodicUUID || formatRoomUUID); @@ -34,7 +36,19 @@ export const joinRoomHandler = async ( } } } catch (e) { + // if room not found and is pmi room, show wait for teacher to enter + if ( + e.message.indexOf(RequestErrorCode.RoomNotFound) > -1 && + (await checkPmiRoom(formatRoomUUID)) + ) { + void message.info(FlatI18n.t("wait-for-teacher-to-enter")); + return; + } pushHistory(RouteNameType.HomePage); errorTips(e); } }; + +async function checkPmiRoom(uuid: string): Promise { + return /^[0-9]*$/.test(uuid.replace(/\s+/g, "")) && (await isPmiRoom({ pmi: uuid }))?.result; +} diff --git a/packages/flat-components/src/components/EditRoomPage/EditRoomBody/index.tsx b/packages/flat-components/src/components/EditRoomPage/EditRoomBody/index.tsx index d73b09c7484..3b5063350a8 100644 --- a/packages/flat-components/src/components/EditRoomPage/EditRoomBody/index.tsx +++ b/packages/flat-components/src/components/EditRoomPage/EditRoomBody/index.tsx @@ -5,17 +5,19 @@ import gbSVG from "./icons/gb.svg"; import usSVG from "./icons/us.svg"; import sgSVG from "./icons/sg.svg"; -import { Button, Checkbox, Form, Input, Modal } from "antd"; -import { CheckboxChangeEvent } from "antd/lib/checkbox"; -import { addWeeks, endOfDay, getDay } from "date-fns"; +import { useLanguage, useTranslate } from "@netless/flat-i18n"; +import Checkbox, { CheckboxChangeEvent } from "antd/lib/checkbox"; import React, { useMemo, useRef, useState } from "react"; +import { addWeeks, endOfDay, getDay } from "date-fns"; +import { Button, Form, Input, Modal } from "antd"; import { useHistory } from "react-router-dom"; -import { useLanguage, useTranslate } from "@netless/flat-i18n"; + import { PeriodicEndType, RoomType, Week } from "../../../types/room"; import { renderBeginTimePicker } from "./renderBeginTimePicker"; import { renderEndTimePicker } from "./renderEndTimePicker"; import { renderPeriodicForm } from "./renderPeriodicForm"; import { ClassPicker } from "../../HomePage/ClassPicker"; +import { PmiDesc, PmiExistTip } from "../../Pmi"; export enum Region { CN_HZ = "cn-hz", @@ -54,6 +56,7 @@ export interface EditRoomFormValues { rate: number; endTime: Date; }; + pmi?: boolean; } export type EditRoomFormInitialValues = @@ -69,13 +72,21 @@ export interface EditRoomBodyProps { onSubmit: (value: EditRoomFormValues) => void; previousPeriodicRoomBeginTime?: number | null; nextPeriodicRoomEndTime?: number | null; + pmi?: string | null; + autoPmiOn?: boolean; + pmiRoomExist?: boolean; + updateAutoPmiOn?: (autoPmiOn: boolean) => void; } export const EditRoomBody: React.FC = ({ + pmi, + autoPmiOn, + pmiRoomExist, type, initialValues, loading, onSubmit, + updateAutoPmiOn, previousPeriodicRoomBeginTime, nextPeriodicRoomEndTime, }) => { @@ -84,6 +95,9 @@ export const EditRoomBody: React.FC = ({ const [isFormVetted, setIsFormVetted] = useState(true); const [isShowEditSubmitConfirm, showEditSubmitConfirm] = useState(false); + const [isPeriodic, setIsPeriodic] = useState(false); + const showPmi = useMemo(() => !!pmi && !isPeriodic, [isPeriodic, pmi]); + // @TODO: need to remove const [region] = useState(initialValues.region); @@ -158,6 +172,26 @@ export const EditRoomBody: React.FC = ({ nextPeriodicRoomEndTime, )} {renderEndTimePicker(t, form, nextPeriodicRoomEndTime)} + {showPmi && updateAutoPmiOn && ( + + updateAutoPmiOn(!autoPmiOn)} + > + + {pmiRoomExist && } + + + )} {type === "schedule" ? ( @@ -306,6 +340,8 @@ export const EditRoomBody: React.FC = ({ } function formValidateStatus(): void { + // synchronize isPeriodic when periodic field changed + setIsPeriodic(form.getFieldValue("isPeriodic")); setIsFormVetted(form.getFieldsError().every(field => field.errors.length <= 0)); } }; diff --git a/packages/flat-components/src/components/EditRoomPage/EditRoomBody/style.less b/packages/flat-components/src/components/EditRoomPage/EditRoomBody/style.less index 53893b7e4d4..a76ff06ce43 100644 --- a/packages/flat-components/src/components/EditRoomPage/EditRoomBody/style.less +++ b/packages/flat-components/src/components/EditRoomPage/EditRoomBody/style.less @@ -22,6 +22,10 @@ .ant-input-number-input { border-radius: 4px !important; } + + .edit-room-form-item.no-margin { + margin-bottom: 0; + } } .edit-room-box { diff --git a/packages/flat-components/src/components/HomePage/HomePageHeroButton/index.tsx b/packages/flat-components/src/components/HomePage/HomePageHeroButton/index.tsx index 54e909b83f5..61a3793851d 100644 --- a/packages/flat-components/src/components/HomePage/HomePageHeroButton/index.tsx +++ b/packages/flat-components/src/components/HomePage/HomePageHeroButton/index.tsx @@ -19,7 +19,11 @@ export interface HomePageHeroButtonProps { onClick?: () => void; } -export const HomePageHeroButton: React.FC = ({ type, onClick }) => { +export const HomePageHeroButton: React.FC = ({ + type, + onClick, + children, +}) => { const t = useTranslate(); return ( ); }; diff --git a/packages/flat-components/src/components/Pmi/PmiDesc.stories.tsx b/packages/flat-components/src/components/Pmi/PmiDesc.stories.tsx new file mode 100644 index 00000000000..112168148d6 --- /dev/null +++ b/packages/flat-components/src/components/Pmi/PmiDesc.stories.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import { PmiDesc, PmiDescProps } from "."; + +const storyMeta: Meta = { + title: "Pmi/PmiDesc", + component: PmiDesc, +}; + +export default storyMeta; + +export const Overview: Story = props => { + return ; +}; + +Overview.args = { + text: "使用个人房间号", + pmi: "1234 5678 900", +}; diff --git a/packages/flat-components/src/components/Pmi/PmiExistTip/PmiExistTip.stories.tsx b/packages/flat-components/src/components/Pmi/PmiExistTip/PmiExistTip.stories.tsx new file mode 100644 index 00000000000..5278d37ffec --- /dev/null +++ b/packages/flat-components/src/components/Pmi/PmiExistTip/PmiExistTip.stories.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import { PmiExistTip, PmiExistTipProps } from "."; + +const storyMeta: Meta = { + title: "Pmi/PmiExistTip", + component: PmiExistTip, +}; + +export default storyMeta; + +export const Overview: Story = props => { + return ; +}; + +Overview.args = { + title: "你的专属房间号,可使用该固定号码创建房间", +}; diff --git a/packages/flat-components/src/components/Pmi/PmiExistTip/index.tsx b/packages/flat-components/src/components/Pmi/PmiExistTip/index.tsx new file mode 100644 index 00000000000..a2d370f63c1 --- /dev/null +++ b/packages/flat-components/src/components/Pmi/PmiExistTip/index.tsx @@ -0,0 +1,22 @@ +import "./style.less"; + +import { Tooltip } from "antd"; +import React, { FC } from "react"; +import { useTranslate } from "@netless/flat-i18n"; +import { QuestionCircleOutlined } from "@ant-design/icons"; + +export interface PmiExistTipProps { + title?: string; +} + +export const PmiExistTip: FC = ({ title }) => { + const t = useTranslate(); + + return ( + + + + ); +}; + +export default PmiExistTip; diff --git a/packages/flat-components/src/components/Pmi/PmiExistTip/style.less b/packages/flat-components/src/components/Pmi/PmiExistTip/style.less new file mode 100644 index 00000000000..ed8853361c0 --- /dev/null +++ b/packages/flat-components/src/components/Pmi/PmiExistTip/style.less @@ -0,0 +1,4 @@ +.pmi-room-help { + margin-left: 4px; + cursor: help; +} \ No newline at end of file diff --git a/packages/flat-components/src/components/Pmi/index.tsx b/packages/flat-components/src/components/Pmi/index.tsx new file mode 100644 index 00000000000..3fa721d6b95 --- /dev/null +++ b/packages/flat-components/src/components/Pmi/index.tsx @@ -0,0 +1,20 @@ +import "./style.less"; + +import React, { HTMLAttributes, FC } from "react"; +export * from "./PmiExistTip"; + +export interface PmiDescProps extends HTMLAttributes { + text: string; + pmi: string; +} + +export const PmiDesc: FC = ({ text, pmi, ...restProps }) => { + return ( + + {text} + {pmi} + + ); +}; + +export default PmiDesc; diff --git a/packages/flat-components/src/components/Pmi/style.less b/packages/flat-components/src/components/Pmi/style.less new file mode 100644 index 00000000000..477fbfe019b --- /dev/null +++ b/packages/flat-components/src/components/Pmi/style.less @@ -0,0 +1,11 @@ +.pmi-id { + margin-left: 4px; + color: var(--grey-3); +} + + +.flat-color-scheme-dark { + .pmi-id { + color: var(--grey-6); + } +} \ No newline at end of file diff --git a/packages/flat-components/src/index.ts b/packages/flat-components/src/index.ts index cf3d29e2027..b849c62e644 100644 --- a/packages/flat-components/src/index.ts +++ b/packages/flat-components/src/index.ts @@ -31,6 +31,7 @@ export * from "./components/RoomStatusElement"; export * from "./components/SaveAnnotationModal"; export * from "./components/SettingPage/AppearancePicker"; export * from "./components/UsersPanel"; +export * from "./components/Pmi"; export * from "./containers/CloudStorageContainer"; export * from "./theme/antd.mod"; diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index f6de013097f..1ae90805181 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -529,6 +529,17 @@ "user-account": "Account", "user-profile": "My Profile", "username": "Name", + "turn-on-the-pmi": "Use PMI", + "copy-pmi": "Copy PMI", + "wait-for-teacher-to-enter": "The room hasn't started yet. Please wait for the teacher to enter", + "the-pmi-room-already-exists": "The PMI room already exists", + "user-pmi-drained": "User PMI used up, please contact the administrator", + "pmi-room-exist": "PMI room already exists, please cancel existing PMI room first", + "pmi-help": "Your own room number, you can use this fixed number to create a room", + "get-link": "Create room to generate link", + "get-pmi": "Generate PMI", + "personal-room-id": "Personal Room ID", + "personal-room-link": "Personal Room Link", "upload-avatar": "Upload Avatar", "upload-avatar-failed": "Upload avatar failed", "bind-wechat": "Bind WeChat", diff --git a/packages/flat-i18n/locales/zh-CN.json b/packages/flat-i18n/locales/zh-CN.json index aa5948e4b9d..ebba5874d8a 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -529,6 +529,17 @@ "user-account": "账号安全", "user-profile": "我的资料", "username": "昵称", + "turn-on-the-pmi": "使用个人房间号", + "copy-pmi": "复制个人房间信息", + "wait-for-teacher-to-enter": "房间未开始,请等待老师进入", + "the-pmi-room-already-exists": "PMI房间已存在", + "user-pmi-drained": "用户PMI已分配完,请联系管理员", + "pmi-room-exist": "个人房间已存在,请先取消已存在的房间", + "pmi-help": "你的专属房间号,可使用该固定号码创建房间", + "get-link": "创建房间获取邀请链接", + "get-pmi": "获取", + "personal-room-id": "个人房间号", + "personal-room-link": "个人房间链接", "upload-avatar": "上传头像", "upload-avatar-failed": "上传头像失败", "bind-wechat": "绑定微信", diff --git a/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.less b/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.less index 0fe090f3af9..480ee329410 100644 --- a/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.less +++ b/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.less @@ -1,16 +1,21 @@ .create-room-box-container { - > .ant-modal { + >.ant-modal { top: 50px; - > .ant-modal-content { - > .ant-modal-header { + + >.ant-modal-content { + >.ant-modal-header { padding: 16px; } - > .ant-modal-body { + >.ant-modal-body { padding: 0 16px; + + .main-room-menu-form-item.no-margin { + margin: 0 + } } - > .ant-modal-footer { + >.ant-modal-footer { border-top: none; } } @@ -21,3 +26,52 @@ margin-top: 2px; margin-right: -11px; } + + +.pmi-selector-more { + flex: 1; + text-align: right; +} + +.pmi-selector-content { + display: flex; + flex-direction: column; + padding: 8px 4px; + background: #fff; + border-radius: 8px; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + font-family: var(--font-family); + + .pmi-selector-item { + padding: 8px 12px; + color: var(--grey-6); + cursor: pointer; + } + + .pmi-selector-item:hover { + border-radius: 8px; + background: var(--blue-0); + } + + .ant-btn-link { + text-align: start; + } + + .checkbox-item-inner { + font-weight: normal; + } +} + +.flat-color-scheme-dark { + .pmi-selector-content { + background-color: var(--grey-10); + + .pmi-selector-item { + color: var(--text); + } + + .pmi-selector-item:hover { + background: var(--grey-9); + } + } +} \ No newline at end of file diff --git a/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.tsx b/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.tsx index debe0d0fa34..e85b108290a 100644 --- a/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.tsx +++ b/packages/flat-pages/src/HomePage/MainRoomMenu/CreateRoomBox.tsx @@ -1,23 +1,28 @@ import "./CreateRoomBox.less"; -import React, { useContext, useEffect, useRef, useState, KeyboardEvent } from "react"; -import { observer } from "mobx-react-lite"; -import { Button, Input, Modal, Checkbox, Form, InputRef } from "antd"; +import React, { useContext, useEffect, useRef, useState, KeyboardEvent, useCallback } from "react"; +import { ClassPicker, HomePageHeroButton, PmiDesc, PmiExistTip, Region } from "flat-components"; +import { Input, Modal, Checkbox, Form, InputRef, Dropdown, message, Button } from "antd"; +import { MoreOutlined } from "@ant-design/icons"; +import { useTranslate } from "@netless/flat-i18n"; import { RoomType } from "@netless/flat-server-api"; +import { observer } from "mobx-react-lite"; + import { PreferencesStoreContext, GlobalStoreContext } from "../../components/StoreProvider"; +import { RouteNameType, usePushHistory } from "../../utils/routes"; +import { joinRoomHandler } from "../../utils/join-room-handler"; import { useSafePromise } from "../../utils/hooks/lifecycle"; -import { ClassPicker, HomePageHeroButton, Region } from "flat-components"; -import { useTranslate } from "@netless/flat-i18n"; interface CreateRoomFormValues { roomTitle: string; roomType: RoomType; autoMicOn: boolean; autoCameraOn: boolean; + pmi?: boolean; } export interface CreateRoomBoxProps { - onCreateRoom: (title: string, type: RoomType, region: Region) => Promise; + onCreateRoom: (title: string, type: RoomType, region: Region, pmi?: boolean) => Promise; } export const CreateRoomBox = observer(function CreateRoomBox({ onCreateRoom }) { @@ -25,6 +30,8 @@ export const CreateRoomBox = observer(function CreateRoomBox const sp = useSafePromise(); const globalStore = useContext(GlobalStoreContext); const preferencesStore = useContext(PreferencesStoreContext); + const pushHistory = usePushHistory(); + const [form] = Form.useForm(); const [isLoading, setLoading] = useState(false); @@ -44,6 +51,8 @@ export const CreateRoomBox = observer(function CreateRoomBox roomType: RoomType.BigClass, autoMicOn: preferencesStore.autoMicOn, autoCameraOn: preferencesStore.autoCameraOn, + // if there exists pmi room, it will can not be selected + pmi: preferencesStore.autoPmiOn && !globalStore.pmiRoomExist, }; useEffect(() => { @@ -62,16 +71,79 @@ export const CreateRoomBox = observer(function CreateRoomBox }; }, [isShowModal]); + const handleCopy = useCallback( + (text: string) => { + navigator.clipboard.writeText(text); + void message.success(t("copy-success")); + }, + [t], + ); + + const handleCreateRoom = (): void => { + if (preferencesStore.autoPmiOn && globalStore.pmiRoomExist) { + // enter room directly + onJoinRoom(globalStore.pmiRoomUUID); + } else { + form.setFieldsValue(defaultValues); + showModal(true); + formValidateStatus(); + } + }; + + const onJoinRoom = async (roomUUID: string): Promise => { + if (globalStore.isTurnOffDeviceTest || window.isElectron) { + await joinRoomHandler(roomUUID, pushHistory); + } else { + pushHistory(RouteNameType.DevicesTestPage, { roomUUID }); + } + }; + return ( <> - { - form.setFieldsValue(defaultValues); - showModal(true); - formValidateStatus(); - }} - /> + + {!!globalStore.pmi && ( + { + e.stopPropagation(); + }} + > + + preferencesStore.updateAutoPmiOn( + !preferencesStore.autoPmiOn, + ) + } + > + + + + + } + overlayClassName="pmi-selector" + trigger={["hover"]} + > +
e.stopPropagation()}> + +
+
+ )} +
(function CreateRoomBox {t("turn-on-the-camera")} + {globalStore.pmi && ( + + + preferencesStore.updateAutoPmiOn( + !preferencesStore.autoPmiOn, + ) + } + > + + {globalStore.pmiRoomExist && } + + + )}
@@ -153,7 +249,7 @@ export const CreateRoomBox = observer(function CreateRoomBox try { const values = form.getFieldsValue(); preferencesStore.updateAutoCameraOn(values.autoCameraOn); - await sp(onCreateRoom(values.roomTitle, values.roomType, roomRegion)); + await sp(onCreateRoom(values.roomTitle, values.roomType, roomRegion, values.pmi)); setLoading(false); showModal(false); } catch (e) { diff --git a/packages/flat-pages/src/HomePage/MainRoomMenu/index.tsx b/packages/flat-pages/src/HomePage/MainRoomMenu/index.tsx index 2557c2946f6..35734ce9235 100644 --- a/packages/flat-pages/src/HomePage/MainRoomMenu/index.tsx +++ b/packages/flat-pages/src/HomePage/MainRoomMenu/index.tsx @@ -3,7 +3,7 @@ import "./MainRoomMenu.less"; import React, { FC, useContext } from "react"; import { Col, Row } from "antd"; import { Region } from "flat-components"; -import { RoomType } from "@netless/flat-server-api"; +import { RoomType, listPmi } from "@netless/flat-server-api"; import { GlobalStoreContext, RoomStoreContext } from "../../components/StoreProvider"; import { RouteNameType, usePushHistory } from "../../utils/routes"; import { CreateRoomBox } from "./CreateRoomBox"; @@ -45,6 +45,7 @@ export const MainRoomMenu: FC = () => { title: string, type: RoomType, region: Region, + pmi?: boolean, ): Promise { try { const roomUUID = await roomStore.createOrdinaryRoom({ @@ -52,7 +53,14 @@ export const MainRoomMenu: FC = () => { type, beginTime: Date.now(), region, + pmi: !!pmi, }); + + if (pmi) { + // update pmi room list + globalStore.updatePmiRoomList((await listPmi()) || []); + } + await onJoinRoom(roomUUID); } catch (e) { errorTips(e); diff --git a/packages/flat-pages/src/LoginPage/utils/state.ts b/packages/flat-pages/src/LoginPage/utils/state.ts index 3587aa6a27f..13c51f9f954 100644 --- a/packages/flat-pages/src/LoginPage/utils/state.ts +++ b/packages/flat-pages/src/LoginPage/utils/state.ts @@ -9,7 +9,7 @@ import { githubLogin } from "../githubLogin"; import { googleLogin } from "../googleLogin"; import { WindowsSystemBtnContext } from "../../components/StoreProvider"; import { loginMachine, ToggleEventsType } from "./machine"; -import { LoginProcessResult } from "@netless/flat-server-api"; +import { LoginProcessResult, createOrGetPmi, listPmi } from "@netless/flat-server-api"; import { LoginButtonProviderType } from "flat-components"; import { LoginDisposer } from "./disposer"; import { NODE_ENV } from "../../constants/process"; @@ -87,6 +87,8 @@ export function useLoginState(): LoginState { const onLoginResult = useCallback( async (authData: LoginProcessResult | null, account?: Account) => { globalStore.updateUserInfo(authData); + globalStore.updatePmi((await createOrGetPmi({ create: true }))?.pmi || null); + globalStore.updatePmiRoomList((await listPmi()) || []); if (!authData) { setLoginResult(null); diff --git a/packages/flat-pages/src/UserScheduledPage/index.tsx b/packages/flat-pages/src/UserScheduledPage/index.tsx index 3aacfd0d7a0..1eef10b8e52 100644 --- a/packages/flat-pages/src/UserScheduledPage/index.tsx +++ b/packages/flat-pages/src/UserScheduledPage/index.tsx @@ -54,14 +54,20 @@ export const UserScheduledPage = observer(function UserScheduledPage() { rate: 7, endTime: addWeeks(scheduleBeginTime, 6), }, + // if there exists pmi room, it will can not be selected + pmi: preferencesStore.autoPmiOn && !globalStore.pmiRoomExist, }; }); return ( ); @@ -95,7 +101,7 @@ export const UserScheduledPage = observer(function UserScheduledPage() { }), ); } else { - await sp(roomStore.createOrdinaryRoom(basePayload)); + await sp(roomStore.createOrdinaryRoom({ ...basePayload, pmi: !!values.pmi })); } history.goBack(); diff --git a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/EditableInput.tsx b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/EditableInput.tsx index 32605a3ed2d..9e0325d9c48 100644 --- a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/EditableInput.tsx +++ b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/EditableInput.tsx @@ -14,7 +14,7 @@ export function nicknameValidator(name: string): boolean { // input editable interface EditableInputProps { value: string; - icon: string; + icon?: string; desc: string; setValue: (event: React.ChangeEvent) => void; @@ -49,7 +49,7 @@ export function EditableInput({ onMouseEnter={() => setHovering(true)} onMouseLeave={() => !editing && setHovering(false)} > - {icon} + {icon && {icon}} {desc} {editing ? ( <> diff --git a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/icons/user.svg b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/icons/user.svg deleted file mode 100644 index fcbb75f889f..00000000000 --- a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/icons/user.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.less b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.less index 36a1bcc126f..2f9a3e43234 100644 --- a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.less +++ b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.less @@ -35,6 +35,15 @@ &-icon-desc { width: 150px; + + .ant-tooltip { + max-width: none; + } + } + + &-icon-desc-help { + margin-left: 4px; + cursor: help; } &-key { @@ -126,6 +135,7 @@ >span { display: inline-block; + max-width: 240px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.tsx b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.tsx index 22685841f1c..ad193d488c5 100644 --- a/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.tsx +++ b/packages/flat-pages/src/UserSettingPage/GeneralSettingPage/index.tsx @@ -4,43 +4,45 @@ import "./index.less"; import phoneSVG from "./icons/phone.svg"; import emailSVG from "./icons/email.svg"; -import userSVG from "./icons/user.svg"; import lockSVG from "./icons/lock.svg"; import React, { useCallback, useContext, useMemo, useState } from "react"; -import { observer } from "mobx-react-lite"; +import { FlatI18n, useLanguage, useTranslate } from "@netless/flat-i18n"; import { Button, Checkbox, message, Modal, Radio } from "antd"; +import { observer } from "mobx-react-lite"; import { FlatPrefersColorScheme, AppearancePicker, errorTips, LoginButtonProviderType, + SVGCopy, + PmiExistTip, } from "flat-components"; -import { UserSettingLayoutContainer } from "../UserSettingLayoutContainer"; -import { FlatI18n, useLanguage, useTranslate } from "@netless/flat-i18n"; - -import { PreferencesStoreContext, GlobalStoreContext } from "../../components/StoreProvider"; -import { useSafePromise } from "../../utils/hooks/lifecycle"; import { LoginPlatform, + createOrGetPmi, deleteAccount, deleteAccountValidate, loginCheck, removeBinding, rename, } from "@netless/flat-server-api"; -// import { ConfirmButtons } from "./ConfirmButtons"; + +import { PreferencesStoreContext, GlobalStoreContext } from "../../components/StoreProvider"; +import { UserSettingLayoutContainer } from "../UserSettingLayoutContainer"; +import { RouteNameType, usePushHistory } from "../../utils/routes"; +import { useSafePromise } from "../../utils/hooks/lifecycle"; import { uploadAvatar, UploadAvatar } from "./UploadAvatar"; import { UpdatePasswordModel } from "./UpdatePasswordModel"; -import { BindWeChat } from "./binding/WeChat"; -import { useBindingList } from "./binding"; -import { BindGitHub } from "./binding/GitHub"; -import { RouteNameType, usePushHistory } from "../../utils/routes"; -import { BindGoogle } from "./binding/Google"; +import { FLAT_WEB_BASE_URL } from "../../constants/process"; import { UpdateEmailModel } from "./UpdateEmailModel"; import { UpdatePhoneModel } from "./UpdatePhoneModel"; import { EditableInput } from "./EditableInput"; +import { BindWeChat } from "./binding/WeChat"; +import { BindGitHub } from "./binding/GitHub"; +import { BindGoogle } from "./binding/Google"; import { BindingField } from "./BindingField"; +import { useBindingList } from "./binding"; enum SelectLanguage { Chinese, @@ -67,6 +69,10 @@ export const GeneralSettingPage = observer(function GeneralSettingPage() { const [showPhoneModel, setShowPhoneModel] = useState(false); const hasPassword = useMemo(() => globalStore.hasPassword, [globalStore.hasPassword]); + const personalLink = useMemo( + () => globalStore.pmiRoomExist && `${FLAT_WEB_BASE_URL}/join/${globalStore.pmiRoomUUID}`, + [globalStore.pmiRoomExist, globalStore.pmiRoomUUID], + ); const loginButtons = useMemo( () => process.env.LOGIN_METHODS.split(",") as LoginButtonProviderType[], @@ -114,6 +120,8 @@ export const GeneralSettingPage = observer(function GeneralSettingPage() { try { await sp(deleteAccount()); globalStore.updateUserInfo(null); + globalStore.updatePmi(null); + globalStore.updatePmiRoomList([]); globalStore.deleteCurrentAccountFromHistory(); pushHistory(RouteNameType.LoginPage); } catch (err) { @@ -151,6 +159,27 @@ export const GeneralSettingPage = observer(function GeneralSettingPage() { [globalStore, refreshBindings, sp, t], ); + const handleCopy = useCallback( + (text: string) => { + navigator.clipboard.writeText(text); + void message.success(t("copy-success")); + }, + [t], + ); + + const handlePmi = useCallback(async () => { + try { + const { pmi = null } = await createOrGetPmi({ create: true }); + globalStore.updatePmi(pmi); + } catch (err) { + errorTips(err); + } + }, [globalStore]); + + const generatePersonalLink = useCallback(() => { + pushHistory(RouteNameType.HomePage); + }, [pushHistory]); + return (
@@ -158,14 +187,74 @@ export const GeneralSettingPage = observer(function GeneralSettingPage() {
{t("user-profile")} - {EditableInput({ - value: name, - icon: userSVG, - desc: t("username"), - setValue: ev => setName(ev.currentTarget.value), - updateValue: changeUserName, - cancelUpdate: () => setName(globalStore.userName || ""), - })} +
+ {EditableInput({ + value: name, + desc: t("username"), + setValue: ev => setName(ev.currentTarget.value), + updateValue: changeUserName, + cancelUpdate: () => setName(globalStore.userName || ""), + })} +
+ +
+
+ + {t("personal-room-id")} + + + + {globalStore.pmi ? ( + <> + {globalStore.pmi} + + + ) : ( + + )} +
+
+ +
+
+ + {t("personal-room-link")} + + + {personalLink ? ( + <> + {personalLink} + + + ) : ( + + )} +
+
diff --git a/packages/flat-pages/src/utils/join-room-handler.ts b/packages/flat-pages/src/utils/join-room-handler.ts index b45d20110b5..371751a28ed 100644 --- a/packages/flat-pages/src/utils/join-room-handler.ts +++ b/packages/flat-pages/src/utils/join-room-handler.ts @@ -1,14 +1,16 @@ import { RouteNameType, usePushHistory } from "../utils/routes"; import { roomStore, globalStore } from "@netless/flat-stores"; -import { RoomType } from "@netless/flat-server-api"; -import { errorTips } from "flat-components"; +import { RequestErrorCode, RoomType, isPmiRoom } from "@netless/flat-server-api"; +import { errorTips, message } from "flat-components"; +import { FlatI18n } from "@netless/flat-i18n"; export const joinRoomHandler = async ( roomUUID: string, pushHistory: ReturnType, ): Promise => { + const formatRoomUUID = roomUUID.replace(/\s+/g, ""); + try { - const formatRoomUUID = roomUUID.replace(/\s+/g, ""); const roomInfo = roomStore.rooms.get(formatRoomUUID); const periodicUUID = roomInfo?.periodicUUID; const data = await roomStore.joinRoom(periodicUUID || formatRoomUUID); @@ -33,7 +35,19 @@ export const joinRoomHandler = async ( } } } catch (e) { + // if room not found and is pmi room, show wait for teacher to enter + if ( + e.message.indexOf(RequestErrorCode.RoomNotFound) > -1 && + (await checkPmiRoom(formatRoomUUID)) + ) { + void message.info(FlatI18n.t("wait-for-teacher-to-enter")); + return; + } pushHistory(RouteNameType.HomePage); errorTips(e); } }; + +async function checkPmiRoom(uuid: string): Promise { + return /^[0-9]*$/.test(uuid.replace(/\s+/g, "")) && (await isPmiRoom({ pmi: uuid }))?.result; +} diff --git a/packages/flat-server-api/src/error.ts b/packages/flat-server-api/src/error.ts index 4fcc3a90945..ffda75dec0c 100644 --- a/packages/flat-server-api/src/error.ts +++ b/packages/flat-server-api/src/error.ts @@ -23,6 +23,7 @@ export enum RequestErrorCode { RoomNotIsRunning, RoomNotIsEnded, RoomNotIsIdle, + RoomExists, // (pmi) room already exists, cannot create new room PeriodicNotFound = 300000, PeriodicIsEnded, @@ -33,6 +34,7 @@ export enum RequestErrorCode { UserAlreadyBinding, // already bound, should unbind first UserPasswordIncorrect, // user password (for update) incorrect UserOrPasswordIncorrect, // user or password (for login) incorrect + UserPmiDrained, // user pmi drained RecordNotFound = 500000, @@ -100,6 +102,7 @@ export const RequestErrorMessage = { [RequestErrorCode.RoomNotIsRunning]: "the-room-is-not-in-progress", [RequestErrorCode.RoomNotIsEnded]: "the-room-is-not-over-yet", [RequestErrorCode.RoomNotIsIdle]: "the-room-has-not-yet-started", + [RequestErrorCode.RoomExists]: "the-pmi-room-already-exists", [RequestErrorCode.PeriodicNotFound]: "periodic-rooms-do-not-exist", [RequestErrorCode.PeriodicIsEnded]: "periodic-rooms-have-ended", @@ -110,6 +113,7 @@ export const RequestErrorMessage = { [RequestErrorCode.UserAlreadyBinding]: "user-already-binding", [RequestErrorCode.UserPasswordIncorrect]: "user-password-incorrect", [RequestErrorCode.UserOrPasswordIncorrect]: "user-account-or-password-incorrect", + [RequestErrorCode.UserPmiDrained]: "user-pmi-drained", [RequestErrorCode.RecordNotFound]: "replay-does-not-exist", diff --git a/packages/flat-server-api/src/index.ts b/packages/flat-server-api/src/index.ts index 0df8de00752..2f383d01576 100644 --- a/packages/flat-server-api/src/index.ts +++ b/packages/flat-server-api/src/index.ts @@ -9,3 +9,4 @@ export * from "./room"; export * from "./storage"; export * from "./utils"; export * from "./config"; +export * from "./pmi"; diff --git a/packages/flat-server-api/src/pmi.ts b/packages/flat-server-api/src/pmi.ts new file mode 100644 index 00000000000..0719e01245c --- /dev/null +++ b/packages/flat-server-api/src/pmi.ts @@ -0,0 +1,33 @@ +import { postV2 } from "./utils"; + +export interface IsPmiRoomPayload { + pmi: string; +} + +export interface IsPmiRoomResult { + result: boolean; +} + +export function isPmiRoom(payload: IsPmiRoomPayload): Promise { + return postV2("user/is-pmi", payload); +} + +export interface PmiListItem { + roomUUID: string; +} + +export function listPmi(): Promise { + return postV2("room/list/pmi", {}); +} + +export interface CreateOrGetPmiPayload { + create: boolean; +} + +export interface CreateOrGetPmiResult { + pmi: string; +} + +export function createOrGetPmi(payload: CreateOrGetPmiPayload): Promise { + return postV2("user/pmi", payload); +} diff --git a/packages/flat-server-api/src/room.ts b/packages/flat-server-api/src/room.ts index 6274bba1ea2..9d525001eb8 100644 --- a/packages/flat-server-api/src/room.ts +++ b/packages/flat-server-api/src/room.ts @@ -7,6 +7,7 @@ export interface CreateOrdinaryRoomPayload { beginTime: number; region: Region; endTime?: number; + pmi?: boolean; } export interface CreateOrdinaryRoomResult { diff --git a/packages/flat-stores/src/classroom-store/index.ts b/packages/flat-stores/src/classroom-store/index.ts index ca4e94ca3cd..c23e3047091 100644 --- a/packages/flat-stores/src/classroom-store/index.ts +++ b/packages/flat-stores/src/classroom-store/index.ts @@ -10,6 +10,7 @@ import { RoomStatus, RoomType, checkRTMCensor, + listPmi, } from "@netless/flat-server-api"; import { FlatI18n } from "@netless/flat-i18n"; import { errorTips, message } from "flat-components"; @@ -1369,6 +1370,11 @@ export class ClassroomStore { case RoomStatus.Stopped: { this.updateRoomStatusLoading(RoomStatusLoadingType.Stopping); await stopClass(this.roomUUID); + + if (globalStore.pmiRoomUUID === this.roomUUID) { + // remove pmi room id list + globalStore.updatePmiRoomList((await listPmi()) || []); + } break; } default: { diff --git a/packages/flat-stores/src/global-store.ts b/packages/flat-stores/src/global-store.ts index 01e80e333db..2bbf4b74b11 100644 --- a/packages/flat-stores/src/global-store.ts +++ b/packages/flat-stores/src/global-store.ts @@ -22,6 +22,10 @@ export type Account = { countryCode?: string | null; }; +export type PmiRoom = { + roomUUID: string; +}; + /** * Properties in Global Store are persisted and shared globally. */ @@ -35,6 +39,8 @@ export class GlobalStore { public isShowGuide = false; public isTurnOffDeviceTest = false; public userInfo: UserInfo | null = null; + public pmi: string | null = null; + public pmiRoomList: PmiRoom[] = []; // login with password public currentAccount: Account | null = null; @@ -70,6 +76,14 @@ export class GlobalStore { */ public hideAvatarsRoomUUIDs: string[] | undefined = undefined; + public get pmiRoomExist(): boolean { + return this.pmiRoomList.length > 0; + } + + public get pmiRoomUUID(): string { + return this.pmiRoomList[0]?.roomUUID; + } + public get userUUID(): string | undefined { return this.userInfo?.userUUID; } @@ -107,6 +121,14 @@ export class GlobalStore { }); } + public updatePmi = (pmi: string | null): void => { + this.pmi = pmi; + }; + + public updatePmiRoomList = (pmiRoomList: PmiRoom[]): void => { + this.pmiRoomList = pmiRoomList; + }; + public updateUserInfo = (userInfo: UserInfo | null): void => { this.userInfo = userInfo; }; diff --git a/packages/flat-stores/src/preferences-store.ts b/packages/flat-stores/src/preferences-store.ts index fa7f26cfa82..f3afeb71d93 100644 --- a/packages/flat-stores/src/preferences-store.ts +++ b/packages/flat-stores/src/preferences-store.ts @@ -29,6 +29,8 @@ export class PreferencesStore { /** selected speaker device id on devices test page */ public speakerId?: string | null = null; + public autoPmiOn = true; + public prefersColorScheme: FlatPrefersColorScheme = "light"; public background: Background = "default"; @@ -54,6 +56,10 @@ export class PreferencesStore { this.autoMicOn = isOn; }; + public updateAutoPmiOn = (isOn: boolean): void => { + this.autoPmiOn = isOn; + }; + public updateCursorNameOn = (isOn: boolean): void => { this.cursorNameOn = isOn; };