diff --git a/.commitlintrc.js b/.commitlintrc.js index a8f5bb0db2d..975a9e1090b 100644 --- a/.commitlintrc.js +++ b/.commitlintrc.js @@ -56,6 +56,7 @@ module.exports = { "github", "style", "room", + "classroom", ], ], "scope-case": [2, "always", ["lower-case", "kebab-case"]], diff --git a/desktop/renderer-app/src/api-middleware/flatServer/index.ts b/desktop/renderer-app/src/api-middleware/flatServer/index.ts index ef21ac4e252..9ed5e2feed3 100644 --- a/desktop/renderer-app/src/api-middleware/flatServer/index.ts +++ b/desktop/renderer-app/src/api-middleware/flatServer/index.ts @@ -101,6 +101,7 @@ export interface JoinRoomResult { uid: number; token: string; }; + showGuide: boolean; } export function joinRoom(uuid: string): Promise { diff --git a/desktop/renderer-app/src/api-middleware/rtm.ts b/desktop/renderer-app/src/api-middleware/rtm.ts index 5d2d562a3c9..333d1e35eb7 100644 --- a/desktop/renderer-app/src/api-middleware/rtm.ts +++ b/desktop/renderer-app/src/api-middleware/rtm.ts @@ -82,6 +82,8 @@ export enum RTMessageType { ChannelStatus = "ChannelStatus", /** user login on other device */ REMOTE_LOGIN = "REMOTE_LOGIN", + /** display a user guide message info when first create the classroom */ + UserGuide = "UserGuide", } export type RTMEvents = { @@ -123,6 +125,7 @@ export type RTMEvents = { }; }; [RTMessageType.REMOTE_LOGIN]: void; + [RTMessageType.UserGuide]: string; }; export interface RTMessage { diff --git a/desktop/renderer-app/src/components/ChatPanel/index.tsx b/desktop/renderer-app/src/components/ChatPanel/index.tsx index ac5ea9765e8..cb68534f72b 100644 --- a/desktop/renderer-app/src/components/ChatPanel/index.tsx +++ b/desktop/renderer-app/src/components/ChatPanel/index.tsx @@ -52,6 +52,7 @@ export const ChatPanel = observer(function ChatPanel({ disableHandRaising={disableHandRaising} isRaiseHand={classRoomStore.users.currentUser?.isRaiseHand} unreadCount={classRoomStore.users.handRaisingJoiners.length || null} + openCloudStorage={() => classRoomStore.toggleCloudStoragePanel(true)} /> ); }); 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 c89b1135caa..2ed07e7c49a 100644 --- a/desktop/renderer-app/src/pages/utils/join-room-handler.ts +++ b/desktop/renderer-app/src/pages/utils/join-room-handler.ts @@ -2,6 +2,7 @@ import { RouteNameType, usePushHistory } from "../../utils/routes"; import { roomStore } from "../../stores/room-store"; import { RoomType } from "../../api-middleware/flatServer/constants"; import { errorTips } from "../../components/Tips/ErrorTips"; +import { globalStore } from "../../stores/global-store"; export const joinRoomHandler = async ( roomUUID: string, @@ -10,6 +11,7 @@ export const joinRoomHandler = async ( try { const formatRoomUUID = roomUUID.replace(/\s+/g, ""); const data = await roomStore.joinRoom(formatRoomUUID); + globalStore.updateShowGuide(data.showGuide); // @TODO make roomType a param switch (data.roomType) { case RoomType.BigClass: { diff --git a/desktop/renderer-app/src/stores/class-room-store.ts b/desktop/renderer-app/src/stores/class-room-store.ts index 5aeddf90582..fa5a1368d83 100644 --- a/desktop/renderer-app/src/stores/class-room-store.ts +++ b/desktop/renderer-app/src/stores/class-room-store.ts @@ -41,7 +41,10 @@ import { i18n } from "i18next"; export type { User } from "./user-store"; export type RTMChannelMessage = RTMessage< - RTMessageType.ChannelMessage | RTMessageType.Notice | RTMessageType.BanText + | RTMessageType.ChannelMessage + | RTMessageType.Notice + | RTMessageType.BanText + | RTMessageType.UserGuide >; export type RecordingConfig = Required< @@ -257,6 +260,10 @@ export class ClassRoomStore { console.error(e); this.updateCalling(false); } + + if (globalStore.isShowGuide) { + this.onUserGuide(); + } }; public toggleCloudStoragePanel = (visible: boolean): void => { @@ -443,6 +450,27 @@ export class ClassRoomStore { this.addMessage(RTMessageType.ChannelMessage, text, this.userUUID); }; + public onUserGuide = (): void => { + // this callback is triggered immediately after joinRTC + // network may be offline status, user rejoin or refresh classroom page + // then this callback will trigger again that push the guide message + // the user guide message always at the end + // so that for avoid multiple send message of the user guide + if ( + this.messages.length > 0 && + this.messages[this.messages.length - 1].type === RTMessageType.UserGuide + ) { + return; + } + this.messages.push({ + type: RTMessageType.UserGuide, + uuid: uuidv4(), + timestamp: Date.now(), + value: false, + userUUID: this.userUUID, + }); + }; + public onCancelAllHandRaising = (): void => { if (this.isCreator) { this.cancelAllHandRaising(); diff --git a/desktop/renderer-app/src/stores/global-store.ts b/desktop/renderer-app/src/stores/global-store.ts index bae28aa2c1f..9e07572bafa 100644 --- a/desktop/renderer-app/src/stores/global-store.ts +++ b/desktop/renderer-app/src/stores/global-store.ts @@ -17,6 +17,7 @@ export class GlobalStore { */ public checkNewVersionDate: number = new Date().getTime(); public isShowRecordHintTips = true; + public isShowGuide = false; public userInfo: UserInfo | null = null; public whiteboardRoomUUID: string | null = null; public whiteboardRoomToken: string | null = null; @@ -92,6 +93,10 @@ export class GlobalStore { public isShareScreenUID = (uid: number): boolean => { return this.rtcShareScreen?.uid === uid; }; + + public updateShowGuide = (showGuide: boolean): void => { + this.isShowGuide = showGuide; + }; } export const globalStore = new GlobalStore(); diff --git a/packages/flat-components/src/components/ChatPanel/ChatMessage/index.tsx b/packages/flat-components/src/components/ChatPanel/ChatMessage/index.tsx index 577faf599f4..2b085f232c1 100644 --- a/packages/flat-components/src/components/ChatPanel/ChatMessage/index.tsx +++ b/packages/flat-components/src/components/ChatPanel/ChatMessage/index.tsx @@ -11,6 +11,7 @@ export interface ChatMessageProps { messageUser?: { name: string }; message: ChatMsg; onMount: () => void; + openCloudStorage?: () => void; } export const ChatMessage = observer(function ChatMessage({ @@ -18,6 +19,7 @@ export const ChatMessage = observer(function ChatMessage({ messageUser, message, onMount, + openCloudStorage, }) { const { t } = useTranslation(); useEffect(() => { @@ -43,6 +45,23 @@ export const ChatMessage = observer(function ChatMessage({ ); } + case ChatMsgType.UserGuide: { + return ( +
+
+
+                            {t("user-guide-text")}
+                            
+                                {t("user-guide-button")}
+                            
+                        
+
+
+ ); + } default: { break; } diff --git a/packages/flat-components/src/components/ChatPanel/ChatMessage/style.less b/packages/flat-components/src/components/ChatPanel/ChatMessage/style.less index be4201c046a..0ba53971ca1 100644 --- a/packages/flat-components/src/components/ChatPanel/ChatMessage/style.less +++ b/packages/flat-components/src/components/ChatPanel/ChatMessage/style.less @@ -53,3 +53,30 @@ user-select: none; } } + +.chat-message-user-guide-bubble { + display: inline-block; + padding: 4px 12px; + border-radius: 4px; + font-size: 12px; + user-select: text; + background: #eef0f6; + color: #7a7b7c; + + & > pre { + margin: 0; + padding: 0; + overflow: visible; + text-align: initial; + font-family: inherit; + white-space: normal; + word-break: break-all; + text-align: justify; + user-select: auto; + } +} + +.chat-message-user-guide-btn { + color: #3381ff; + cursor: pointer; +} diff --git a/packages/flat-components/src/components/ChatPanel/ChatMessageList/index.tsx b/packages/flat-components/src/components/ChatPanel/ChatMessageList/index.tsx index 04d8bab3b86..0c5c967703b 100644 --- a/packages/flat-components/src/components/ChatPanel/ChatMessageList/index.tsx +++ b/packages/flat-components/src/components/ChatPanel/ChatMessageList/index.tsx @@ -22,6 +22,7 @@ export interface ChatMessageListProps { messages: ChatMsg[]; getUserByUUID: (uuid: string) => User | undefined; loadMoreRows: InfiniteLoaderProps["loadMoreRows"]; + openCloudStorage: () => void; } export const ChatMessageList = observer(function ChatMessageList({ @@ -30,6 +31,7 @@ export const ChatMessageList = observer(function ChatMessa messages, getUserByUUID, loadMoreRows, + openCloudStorage, }) { const forceUpdate = useUpdate(); @@ -124,6 +126,7 @@ export const ChatMessageList = observer(function ChatMessa {() => ( { diff --git a/web/flat-web/src/api-middleware/flatServer/index.ts b/web/flat-web/src/api-middleware/flatServer/index.ts index 2c437bc04fe..7162665beda 100644 --- a/web/flat-web/src/api-middleware/flatServer/index.ts +++ b/web/flat-web/src/api-middleware/flatServer/index.ts @@ -100,6 +100,7 @@ export interface JoinRoomResult { token: string; }; rtmToken: string; + showGuide: boolean; } export function joinRoom(uuid: string): Promise { diff --git a/web/flat-web/src/components/ChatPanel/index.tsx b/web/flat-web/src/components/ChatPanel/index.tsx index ac5ea9765e8..cb68534f72b 100644 --- a/web/flat-web/src/components/ChatPanel/index.tsx +++ b/web/flat-web/src/components/ChatPanel/index.tsx @@ -52,6 +52,7 @@ export const ChatPanel = observer(function ChatPanel({ disableHandRaising={disableHandRaising} isRaiseHand={classRoomStore.users.currentUser?.isRaiseHand} unreadCount={classRoomStore.users.handRaisingJoiners.length || null} + openCloudStorage={() => classRoomStore.toggleCloudStoragePanel(true)} /> ); }); diff --git a/web/flat-web/src/pages/utils/join-room-handler.ts b/web/flat-web/src/pages/utils/join-room-handler.ts index 7156d1bddd6..e77d6c16674 100644 --- a/web/flat-web/src/pages/utils/join-room-handler.ts +++ b/web/flat-web/src/pages/utils/join-room-handler.ts @@ -2,6 +2,7 @@ import { generateRoutePath, RouteNameType, usePushHistory } from "../../utils/ro import { roomStore } from "../../stores/room-store"; import { RoomType } from "../../api-middleware/flatServer/constants"; import { errorTips } from "../../components/Tips/ErrorTips"; +import { globalStore } from "../../stores/GlobalStore"; export const joinRoomHandler = async ( roomUUID: string, @@ -10,6 +11,7 @@ export const joinRoomHandler = async ( try { const formatRoomUUID = roomUUID.replace(/\s+/g, ""); const data = await roomStore.joinRoom(formatRoomUUID); + globalStore.updateShowGuide(data.showGuide); // try to work around chrome does not show permission popup after // soft navigating. here we do a "hard" navigating instead. // @TODO make roomType a param diff --git a/web/flat-web/src/stores/GlobalStore.ts b/web/flat-web/src/stores/GlobalStore.ts index 075ff5e5d03..b289ee9d185 100644 --- a/web/flat-web/src/stores/GlobalStore.ts +++ b/web/flat-web/src/stores/GlobalStore.ts @@ -17,6 +17,7 @@ export class GlobalStore { * Hide it permanently if user close the tooltip. */ public isShowRecordHintTips = true; + public isShowGuide = false; public isTurnOffDeviceTest = false; public userInfo: UserInfo | null = null; public whiteboardRoomUUID: string | null = null; @@ -93,6 +94,10 @@ export class GlobalStore { public isShareScreenUID = (uid: UID): boolean => { return this.rtcShareScreen?.uid === Number(uid); }; + + public updateShowGuide = (showGuide: boolean): void => { + this.isShowGuide = showGuide; + }; } export const globalStore = new GlobalStore(); diff --git a/web/flat-web/src/stores/class-room-store.ts b/web/flat-web/src/stores/class-room-store.ts index ed7fcc7ddf4..497a1c973bb 100644 --- a/web/flat-web/src/stores/class-room-store.ts +++ b/web/flat-web/src/stores/class-room-store.ts @@ -40,7 +40,10 @@ import { WhiteboardStore } from "./whiteboard-store"; export type { User } from "./user-store"; export type RTMChannelMessage = RTMessage< - RTMessageType.ChannelMessage | RTMessageType.Notice | RTMessageType.BanText + | RTMessageType.ChannelMessage + | RTMessageType.Notice + | RTMessageType.BanText + | RTMessageType.UserGuide >; export type RecordingConfig = Required< @@ -256,6 +259,10 @@ export class ClassRoomStore { console.error(e); this.updateCalling(false); } + + if (globalStore.isShowGuide) { + this.onUserGuide(); + } }; public leaveRTC = (): void => { @@ -435,6 +442,27 @@ export class ClassRoomStore { this.addMessage(RTMessageType.ChannelMessage, text, this.userUUID); }; + public onUserGuide = (): void => { + // this callback is triggered immediately after joinRTC + // network may be offline status, user rejoin or refresh classroom page + // then this callback will trigger again that push the guide message + // the user guide message always at the end + // so that for avoid multiple send message of the user guide + if ( + this.messages.length > 0 && + this.messages[this.messages.length - 1].type === RTMessageType.UserGuide + ) { + return; + } + this.messages.push({ + type: RTMessageType.UserGuide, + uuid: uuidv4(), + timestamp: Date.now(), + value: false, + userUUID: this.userUUID, + }); + }; + public onCancelAllHandRaising = (): void => { if (this.isCreator) { this.cancelAllHandRaising(); @@ -1037,7 +1065,14 @@ export function useClassRoomStore({ i18n, }: ClassRoomStoreConfig): ClassRoomStore { const [classRoomStore] = useState( - () => new ClassRoomStore({ roomUUID, ownerUUID, recordingConfig, classMode, i18n }), + () => + new ClassRoomStore({ + roomUUID, + ownerUUID, + recordingConfig, + classMode, + i18n, + }), ); const pushHistory = usePushHistory();