diff --git a/.github/workflows/deploy-web-dev-cn.yml b/.github/workflows/deploy-web-dev-cn.yml index 8a98c8c9d71..99b3991ecfd 100644 --- a/.github/workflows/deploy-web-dev-cn.yml +++ b/.github/workflows/deploy-web-dev-cn.yml @@ -3,6 +3,7 @@ on: push: branches: - "main" + - "big-classroom-hand-raise" paths: - "config/**" - "web/flat-web/**" diff --git a/.github/workflows/deploy-web-dev-us.yml b/.github/workflows/deploy-web-dev-us.yml index 3b9efbd0216..7e39dd82178 100644 --- a/.github/workflows/deploy-web-dev-us.yml +++ b/.github/workflows/deploy-web-dev-us.yml @@ -3,6 +3,7 @@ on: push: branches: - "main" + - "big-classroom-hand-raise" paths: - "config/**" - "web/flat-web/**" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f24fb605e2..d219c045ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.8.3](https://github.com/netless-io/flat/compare/v1.8.2...v1.8.3) (2022-07-29) + + +### Bug Fixes + +* **classroom:** disable camera and mic on down stage ([1fc82c3](https://github.com/netless-io/flat/commit/1fc82c3b362de9452e936d2ff6122889eb2caea1)) +* **whiteboard:** don't set state when not writable ([#1607](https://github.com/netless-io/flat/issues/1607)) ([#1616](https://github.com/netless-io/flat/issues/1616)) ([ef012df](https://github.com/netless-io/flat/commit/ef012df48d70f80c9f3f0ddc7df28083aa73c04c)) + + + ## [1.8.2](https://github.com/netless-io/flat/compare/v1.8.1...v1.8.2) (2022-06-23) diff --git a/desktop/main-app/package.json b/desktop/main-app/package.json index 54c7cb4e732..38acd01e455 100644 --- a/desktop/main-app/package.json +++ b/desktop/main-app/package.json @@ -1,7 +1,7 @@ { "name": "flat", "productName": "Flat", - "version": "1.8.2", + "version": "1.8.3", "private": true, "description": "", "homepage": "https://github.com/netless-io/flat", diff --git a/desktop/renderer-app/src/api-middleware/rtm.ts b/desktop/renderer-app/src/api-middleware/rtm.ts index 3ac2d05e86f..75825524d80 100644 --- a/desktop/renderer-app/src/api-middleware/rtm.ts +++ b/desktop/renderer-app/src/api-middleware/rtm.ts @@ -63,6 +63,8 @@ export enum RTMessageType { AcceptRaiseHand = "AcceptRaiseHand", /** creator cancel all hand raising */ CancelAllHandRaising = "CancelHandRaising", + /** Cancel all hand raising and whiteboard writable */ + AllOffStage = "AllOffStage", /** creator ban all rtm */ BanText = "BanText", /** creator allows a joiner or joiners allows themselves to speak */ @@ -94,6 +96,7 @@ export type RTMEvents = { [RTMessageType.RaiseHand]: boolean; [RTMessageType.AcceptRaiseHand]: { userUUID: string; accept: boolean }; [RTMessageType.CancelAllHandRaising]: boolean; + [RTMessageType.AllOffStage]: boolean; [RTMessageType.BanText]: boolean; [RTMessageType.Speak]: Array<{ userUUID: string; speak: boolean }>; [RTMessageType.DeviceState]: { userUUID: string; camera: boolean; mic: boolean }; diff --git a/desktop/renderer-app/src/components/ChatPanel/index.tsx b/desktop/renderer-app/src/components/ChatPanel/index.tsx index 4a774a3d0f5..781c45d2173 100644 --- a/desktop/renderer-app/src/components/ChatPanel/index.tsx +++ b/desktop/renderer-app/src/components/ChatPanel/index.tsx @@ -6,11 +6,13 @@ import { generateAvatar } from "../../utils/generate-avatar"; export interface ChatPanelProps { classRoomStore: ClassRoomStore; + isShowAllOfStage?: boolean; disableMultipleSpeakers?: boolean; } export const ChatPanel = observer(function ChatPanel({ classRoomStore, + isShowAllOfStage, disableMultipleSpeakers, }) { const users = useComputed(() => { @@ -28,6 +30,7 @@ export const ChatPanel = observer(function ChatPanel({ hasSpeaking={classRoomStore.users.speakingJoiners.length > 0} isBan={classRoomStore.isBan} isCreator={classRoomStore.isCreator} + isShowAllOfStage={isShowAllOfStage} loadMoreRows={classRoomStore.updateHistory} messages={classRoomStore.messages} openCloudStorage={() => classRoomStore.toggleCloudStoragePanel(true)} @@ -42,8 +45,8 @@ export const ChatPanel = observer(function ChatPanel({ } classRoomStore.acceptRaiseHand(userUUID); }} + onAllOffStage={classRoomStore.onAllOffStage} onBanChange={classRoomStore.onToggleBan} - onCancelAllHandRaising={classRoomStore.onCancelAllHandRaising} onEndSpeaking={userUUID => { void classRoomStore.onSpeak([{ userUUID, speak: false }]); }} diff --git a/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx b/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx index cf87c5f1f2d..5352458c5db 100644 --- a/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx +++ b/desktop/renderer-app/src/components/MainPageLayoutContainer/index.tsx @@ -79,7 +79,10 @@ export const MainPageLayoutContainer = observer( key: "feedback", icon: (): React.ReactNode => , title: t("feedback"), - route: "https://github.com/netless-io/flat/issues", + route: + process.env.FLAT_REGION === "CN" + ? "https://www.yuque.com/leooel/ec1kmm/vmsolg" + : "https://join.slack.com/t/agoraflat/shared_invite/zt-vdb09pf6-mD4hB7sDA4LXN2O5dhmEPQ", }, { key: "logout", diff --git a/desktop/renderer-app/src/components/Whiteboard.tsx b/desktop/renderer-app/src/components/Whiteboard.tsx index d8d4edd0bfe..550052567fd 100644 --- a/desktop/renderer-app/src/components/Whiteboard.tsx +++ b/desktop/renderer-app/src/components/Whiteboard.tsx @@ -224,15 +224,17 @@ export const Whiteboard = observer(function Whiteboard({ onDragOver={onDragOver} onDrop={onDrop} > - {!whiteboardStore.isCreator && !whiteboardStore.isWritable && ( -
- -
- )} + {!whiteboardStore.isCreator && + !whiteboardStore.isWritable && + !classRoomStore.isBan && ( +
+ +
+ )}
(function BigClassPage() } isShow={isRealtimeSideOpen} diff --git a/desktop/renderer-app/src/stores/class-room-store.ts b/desktop/renderer-app/src/stores/class-room-store.ts index 3c13f020c03..0a884389362 100644 --- a/desktop/renderer-app/src/stores/class-room-store.ts +++ b/desktop/renderer-app/src/stores/class-room-store.ts @@ -562,6 +562,18 @@ export class ClassRoomStore { } }; + public onAllOffStage = (): void => { + if (this.isCreator) { + this.allOffStage(); + void message.info(i18next.t("all-off-stage-toast")); + void this.rtm.sendCommand({ + type: RTMessageType.AllOffStage, + value: true, + keepHistory: true, + }); + } + }; + /** When current user (who is a joiner) raises hand */ public onToggleHandRaising = (): void => { if (this.isCreator || this.users.currentUser?.isSpeak) { @@ -808,6 +820,34 @@ export class ClassRoomStore { }); } + private allOffStage(): void { + if (!this.isCreator) { + this.whiteboardStore.updateWritable(false); + // guard code + if (this.whiteboardStore.room) { + if (this.whiteboardStore.isWritable !== this.whiteboardStore.room.isWritable) { + this.whiteboardStore.room.setWritable(false); + } + } + } + this.users.updateUsers(user => { + if (user.userUUID !== this.ownerUUID) { + if (user.isRaiseHand) { + user.isRaiseHand = false; + } + if (user.isSpeak) { + user.isSpeak = false; + } + if (user.camera) { + user.camera = false; + } + if (user.mic) { + user.mic = false; + } + } + }); + } + private startListenCommands = (): void => { this.rtm.on(RTMessageType.ChannelMessage, (text, senderId) => { if (!this.isBan || senderId === this.ownerUUID) { @@ -825,6 +865,12 @@ export class ClassRoomStore { } }); + this.rtm.on(RTMessageType.AllOffStage, (_value, senderId) => { + if (senderId === this.ownerUUID && !this.isCreator) { + this.allOffStage(); + } + }); + this.rtm.on(RTMessageType.RaiseHand, (isRaiseHand, senderId) => { this.users.updateUsers(user => { if (user.userUUID === senderId && (!isRaiseHand || !user.isSpeak)) { @@ -1077,6 +1123,9 @@ export class ClassRoomStore { private updateBanStatus = (isBan: boolean): void => { this.isBan = isBan; + if (isBan) { + this.allOffStage(); + } }; private updateRoomStatusLoading = (loading: RoomStatusLoadingType): void => { diff --git a/desktop/renderer-app/src/stores/whiteboard-store.ts b/desktop/renderer-app/src/stores/whiteboard-store.ts index 13daa77c5cd..ab35ba691cd 100644 --- a/desktop/renderer-app/src/stores/whiteboard-store.ts +++ b/desktop/renderer-app/src/stores/whiteboard-store.ts @@ -102,6 +102,11 @@ export class WhiteboardStore { public updatePhase = (phase: RoomPhase): void => { this.phase = phase; + if (phase === RoomPhase.Connected) { + if (this.room && this.room.isWritable !== this.isWritable) { + this.room.setWritable(this.isWritable); + } + } }; public updateViewMode = (viewMode: ViewMode): void => { diff --git a/docs/releases/v1.8.3/en.md b/docs/releases/v1.8.3/en.md new file mode 100644 index 00000000000..5832934220a --- /dev/null +++ b/docs/releases/v1.8.3/en.md @@ -0,0 +1,8 @@ +## Improved + +1. User rights management for large classes +2. User feedback process + +## Fixed + +1. The room created by the mobile side cannot be joined by the PC side diff --git a/docs/releases/v1.8.3/zh.md b/docs/releases/v1.8.3/zh.md new file mode 100644 index 00000000000..0e1569314fc --- /dev/null +++ b/docs/releases/v1.8.3/zh.md @@ -0,0 +1,8 @@ +## 优化 + +1. 大班课用户权限管理 +2. 用户反馈流程 + +## 修复 + +1. 卓面端无法加入移动端创建的房间 diff --git a/packages/flat-components/src/components/ChatPanel/ChatUsers/index.tsx b/packages/flat-components/src/components/ChatPanel/ChatUsers/index.tsx index bd2ad95c374..fc4e494b54c 100644 --- a/packages/flat-components/src/components/ChatPanel/ChatUsers/index.tsx +++ b/packages/flat-components/src/components/ChatPanel/ChatUsers/index.tsx @@ -11,17 +11,17 @@ import { User } from "../../../types/user"; export type ChatUsersProps = { isCreator: boolean; hasHandRaising: boolean; + isShowAllOfStage?: boolean; hasSpeaking: boolean; users: User[]; - onCancelAllHandRaising: () => void; + onAllOffStage: () => void; } & Omit; export const ChatUsers = observer(function ChatUsers({ - isCreator, - hasHandRaising, + isShowAllOfStage, hasSpeaking, users, - onCancelAllHandRaising, + onAllOffStage, ...restProps }) { const { t } = useTranslation(); @@ -48,20 +48,18 @@ export const ChatUsers = observer(function ChatUsers({ ); }; - const isShowCancelAllHandRaising = isCreator && hasHandRaising; - return (
- {isShowCancelAllHandRaising && ( + {isShowAllOfStage && (
-
)}
{renderList} diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index 9532c1eba43..620860ed6ca 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -15,7 +15,7 @@ "upload-success": "Successful", "agree": "agree", "all-staff-are-under-ban": "All members are mute", - "ban": "Mute", + "ban": "Mute All", "banned": "Muted", "cancel-hand-raising": "Cancel raise of hand", "during-the-presentation": "(Speaking)", @@ -468,5 +468,7 @@ "unbind-success": "Unbind success", "delete-account": "Delete Account", "confirm-delete-account": "Are you sure to delete your account?", - "quit-all-rooms-before-delete-account": "Please quit all rooms before deleting account." + "quit-all-rooms-before-delete-account": "Please quit all rooms before deleting account.", + "all-off-stage": "Down Stage All", + "all-off-stage-toast": "All users are down stage" } diff --git a/packages/flat-i18n/locales/zh-CN.json b/packages/flat-i18n/locales/zh-CN.json index 5679ae5fc19..e7bd24ef17f 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -13,10 +13,10 @@ "upload-exception": "上传异常", "upload-fail": "上传失败", "upload-success": "上传成功", - "banned": "已禁言", - "unban": "已解除禁言", + "banned": "已禁止学生聊天和举手", + "unban": "允许学生聊天和举手", "say-something": "说点什么…", - "ban": "禁言", + "ban": "全体禁言", "raise-your-hand": "举手", "all-staff-are-under-ban": "全员禁言中", "send": "发送", @@ -468,5 +468,7 @@ "unbind-success": "解绑成功", "delete-account": "账号注销", "confirm-delete-account": "确定注销账号吗?", - "quit-all-rooms-before-delete-account": "请先退出所有房间" + "quit-all-rooms-before-delete-account": "请先退出所有房间", + "all-off-stage": "全体下台", + "all-off-stage-toast": "全体学生已下台" } diff --git a/web/flat-web/src/api-middleware/Rtm.ts b/web/flat-web/src/api-middleware/Rtm.ts index bebf9104ec2..b9fb9bef74a 100644 --- a/web/flat-web/src/api-middleware/Rtm.ts +++ b/web/flat-web/src/api-middleware/Rtm.ts @@ -63,6 +63,8 @@ export enum RTMessageType { AcceptRaiseHand = "AcceptRaiseHand", /** creator cancel all hand raising */ CancelAllHandRaising = "CancelHandRaising", + /** Cancel all hand raising and whiteboard writable */ + AllOffStage = "AllOffStage", /** creator ban all rtm */ BanText = "BanText", /** creator allows a joiner or joiners allows themselves to speak */ @@ -94,6 +96,7 @@ export type RTMEvents = { [RTMessageType.RaiseHand]: boolean; [RTMessageType.AcceptRaiseHand]: { userUUID: string; accept: boolean }; [RTMessageType.CancelAllHandRaising]: boolean; + [RTMessageType.AllOffStage]: boolean; [RTMessageType.BanText]: boolean; [RTMessageType.Speak]: Array<{ userUUID: string; speak: boolean }>; [RTMessageType.DeviceState]: { userUUID: string; camera: boolean; mic: boolean }; diff --git a/web/flat-web/src/components/ChatPanel/index.tsx b/web/flat-web/src/components/ChatPanel/index.tsx index 4a774a3d0f5..ab62671a8e4 100644 --- a/web/flat-web/src/components/ChatPanel/index.tsx +++ b/web/flat-web/src/components/ChatPanel/index.tsx @@ -6,12 +6,14 @@ import { generateAvatar } from "../../utils/generate-avatar"; export interface ChatPanelProps { classRoomStore: ClassRoomStore; + isShowAllOfStage?: boolean; disableMultipleSpeakers?: boolean; } export const ChatPanel = observer(function ChatPanel({ classRoomStore, disableMultipleSpeakers, + isShowAllOfStage, }) { const users = useComputed(() => { const { creator, speakingJoiners, handRaisingJoiners, otherJoiners } = classRoomStore.users; @@ -28,6 +30,7 @@ export const ChatPanel = observer(function ChatPanel({ hasSpeaking={classRoomStore.users.speakingJoiners.length > 0} isBan={classRoomStore.isBan} isCreator={classRoomStore.isCreator} + isShowAllOfStage={isShowAllOfStage} loadMoreRows={classRoomStore.updateHistory} messages={classRoomStore.messages} openCloudStorage={() => classRoomStore.toggleCloudStoragePanel(true)} @@ -42,8 +45,8 @@ export const ChatPanel = observer(function ChatPanel({ } classRoomStore.acceptRaiseHand(userUUID); }} + onAllOffStage={classRoomStore.onAllOffStage} onBanChange={classRoomStore.onToggleBan} - onCancelAllHandRaising={classRoomStore.onCancelAllHandRaising} onEndSpeaking={userUUID => { void classRoomStore.onSpeak([{ userUUID, speak: false }]); }} diff --git a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx index dd30539c5db..a58ab2b531b 100644 --- a/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx +++ b/web/flat-web/src/components/MainPageLayoutHorizontalContainer/index.tsx @@ -85,7 +85,10 @@ export const MainPageLayoutHorizontalContainer = observer , title: t("feedback"), - route: "https://github.com/netless-io/flat/issues", + route: + process.env.FLAT_REGION === "CN" + ? "https://www.yuque.com/leooel/ec1kmm/vmsolg" + : "https://join.slack.com/t/agoraflat/shared_invite/zt-vdb09pf6-mD4hB7sDA4LXN2O5dhmEPQ", }, { key: "logout", diff --git a/web/flat-web/src/components/RTCAvatar/AvatarCanvas.tsx b/web/flat-web/src/components/RTCAvatar/AvatarCanvas.tsx index 4e6035ceb60..16c55fd19d5 100644 --- a/web/flat-web/src/components/RTCAvatar/AvatarCanvas.tsx +++ b/web/flat-web/src/components/RTCAvatar/AvatarCanvas.tsx @@ -35,6 +35,17 @@ export const AvatarCanvas = observer< } }, [canvasEl, rtcAvatar]); + useEffect( + () => () => { + if (rtcAvatar) { + rtcAvatar.setElement(null); + rtcAvatar.enableCamera(false); + rtcAvatar.enableMic(false); + } + }, + [rtcAvatar], + ); + useEffect(() => { if (rtcAvatar) { rtcAvatar.enableCamera(Boolean(camera)); diff --git a/web/flat-web/src/components/Whiteboard.tsx b/web/flat-web/src/components/Whiteboard.tsx index 2e0fe2853ad..a9e05540329 100644 --- a/web/flat-web/src/components/Whiteboard.tsx +++ b/web/flat-web/src/components/Whiteboard.tsx @@ -225,15 +225,17 @@ export const Whiteboard = observer(function Whiteboard({ onDragOver={onDragOver} onDrop={onDrop} > - {!whiteboardStore.isCreator && !whiteboardStore.isWritable && ( -
- -
- )} + {!whiteboardStore.isCreator && + !whiteboardStore.isWritable && + !classRoomStore.isBan && ( +
+ +
+ )}
(function BigClassPage() } isShow={isRealtimeSideOpen} diff --git a/web/flat-web/src/stores/class-room-store.ts b/web/flat-web/src/stores/class-room-store.ts index ae410544d5f..787cf9e9c3f 100644 --- a/web/flat-web/src/stores/class-room-store.ts +++ b/web/flat-web/src/stores/class-room-store.ts @@ -497,6 +497,18 @@ export class ClassRoomStore { } }; + public onAllOffStage = (): void => { + if (this.isCreator) { + this.allOffStage(); + void message.info(i18next.t("all-off-stage-toast")); + void this.rtm.sendCommand({ + type: RTMessageType.AllOffStage, + value: true, + keepHistory: true, + }); + } + }; + /** When current user (who is a joiner) raises hand */ public onToggleHandRaising = (): void => { if (this.isCreator || this.users.currentUser?.isSpeak) { @@ -770,6 +782,34 @@ export class ClassRoomStore { }); } + private allOffStage(): void { + if (!this.isCreator) { + this.whiteboardStore.updateWritable(false); + // guard code + if (this.whiteboardStore.room) { + if (this.whiteboardStore.isWritable !== this.whiteboardStore.room.isWritable) { + this.whiteboardStore.room.setWritable(false); + } + } + } + this.users.updateUsers(user => { + if (user.userUUID !== this.ownerUUID) { + if (user.isRaiseHand) { + user.isRaiseHand = false; + } + if (user.isSpeak) { + user.isSpeak = false; + } + if (user.camera) { + user.camera = false; + } + if (user.mic) { + user.mic = false; + } + } + }); + } + private startListenCommands = (): void => { this.rtm.on(RTMessageType.ChannelMessage, (text, senderId) => { if (!this.isBan || senderId === this.ownerUUID) { @@ -787,6 +827,12 @@ export class ClassRoomStore { } }); + this.rtm.on(RTMessageType.AllOffStage, (_value, senderId) => { + if (senderId === this.ownerUUID && !this.isCreator) { + this.allOffStage(); + } + }); + this.rtm.on(RTMessageType.RaiseHand, (isRaiseHand, senderId) => { this.users.updateUsers(user => { if (user.userUUID === senderId && (!isRaiseHand || !user.isSpeak)) { @@ -1039,6 +1085,9 @@ export class ClassRoomStore { private updateBanStatus = (isBan: boolean): void => { this.isBan = isBan; + if (isBan) { + this.allOffStage(); + } }; private updateRoomStatusLoading = (loading: RoomStatusLoadingType): void => { diff --git a/web/flat-web/src/stores/whiteboard-store.ts b/web/flat-web/src/stores/whiteboard-store.ts index 465c6ca17c0..0f9061a2e08 100644 --- a/web/flat-web/src/stores/whiteboard-store.ts +++ b/web/flat-web/src/stores/whiteboard-store.ts @@ -102,6 +102,11 @@ export class WhiteboardStore { public updatePhase = (phase: RoomPhase): void => { this.phase = phase; + if (phase === RoomPhase.Connected) { + if (this.room && this.room.isWritable !== this.isWritable) { + this.room.setWritable(this.isWritable); + } + } }; public updateViewMode = (viewMode: ViewMode): void => {