From 9093915f701d571e210f666a5c0f48a8bafe1d6f Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Tue, 5 May 2026 09:47:07 +0200 Subject: [PATCH 1/9] ver 1 --- features/session/use-live-session.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/features/session/use-live-session.ts b/features/session/use-live-session.ts index ffdb8da..701d205 100644 --- a/features/session/use-live-session.ts +++ b/features/session/use-live-session.ts @@ -44,6 +44,7 @@ const CHAT_STORAGE_PREFIX = "eduverse:session-chat:v1" const MAX_CHAT_MESSAGES = 200 type MediaDeviceKind = "microphone" | "camera" | "screen" +type SessionTokenUser = Pick type LiveSessionError = | Error | string @@ -799,7 +800,7 @@ function mapParticipant( } satisfies SessionParticipant } -async function fetchSessionToken(classId: string, user: User) { +async function fetchSessionToken(classId: string, user: SessionTokenUser) { const response = await fetch("/api/livekit/token", { method: "POST", headers: { @@ -852,6 +853,10 @@ export function useLiveSession({ const [whiteboardMessages, setWhiteboardMessages] = useState< LiveSessionWhiteboardMessage[] >([]) + const currentUserId = currentUser.id + const currentUserName = currentUser.name + const currentUserAvatar = currentUser.avatar + const currentUserRole = currentUser.role const upsertNotice = useCallback((notice: LiveSessionNotice) => { setNotices((prev) => @@ -1080,7 +1085,12 @@ export function useLiveSession({ try { const { participantToken, serverUrl } = await fetchSessionToken( classId, - currentUser, + { + id: currentUserId, + name: currentUserName, + avatar: currentUserAvatar, + role: currentUserRole, + }, ) if (isCancelled) { @@ -1102,13 +1112,13 @@ export function useLiveSession({ if (roomSid) { const nextChatStorageKey = getSessionChatStorageKey({ classId, - userId: currentUser.id, + userId: currentUserId, roomSid, }) pruneStoredSessionChats({ classId, - userId: currentUser.id, + userId: currentUserId, activeStorageKey: nextChatStorageKey, }) setChatStorageKey(nextChatStorageKey) @@ -1167,7 +1177,10 @@ export function useLiveSession({ } }, [ classId, - currentUser, + currentUserAvatar, + currentUserId, + currentUserName, + currentUserRole, enabled, syncParticipants, updateMediaDevice, From 9bf1ce41d6d10385fc5f6f032f8d5ccbb8bd452a Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Tue, 5 May 2026 10:02:52 +0200 Subject: [PATCH 2/9] ver 2 --- features/session/live-session-provider.tsx | 4 +++ features/session/live-session-types.ts | 6 ++++ features/session/session-screen.tsx | 2 +- features/session/use-live-session.ts | 16 ++++++++++ features/session/use-whiteboard.ts | 36 ++++++++++++++++++---- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/features/session/live-session-provider.tsx b/features/session/live-session-provider.tsx index 3b9588d..502b643 100644 --- a/features/session/live-session-provider.tsx +++ b/features/session/live-session-provider.tsx @@ -154,6 +154,10 @@ export function LiveSessionProvider({ children }: { children: ReactNode }) { return } + if (isTeacher) { + await liveSession.clearWhiteboards().catch(() => false) + } + liveSession.disconnect() setSessionActive(false) setSessionScope(null) diff --git a/features/session/live-session-types.ts b/features/session/live-session-types.ts index ceba989..e30f1b1 100644 --- a/features/session/live-session-types.ts +++ b/features/session/live-session-types.ts @@ -206,6 +206,11 @@ export type LiveSessionWhiteboardMessage = version: number operations: WhiteboardOperation[] } + | { + id: string + senderId: string + type: "session:clear" + } export interface LiveSessionChatMessage { id: string @@ -239,6 +244,7 @@ export interface LiveSessionState { message: LiveSessionWhiteboardMessage, options?: { reliable?: boolean }, ) => Promise + clearWhiteboards: () => Promise sendChatMessage: (content: string) => Promise dismissNotice: (noticeId: string) => void disconnect: () => void diff --git a/features/session/session-screen.tsx b/features/session/session-screen.tsx index c8fe309..4a11e40 100644 --- a/features/session/session-screen.tsx +++ b/features/session/session-screen.tsx @@ -159,7 +159,7 @@ export function SessionScreen({ cls }: { cls: Class }) { height: number } | null>(null) const presentationSessionId = liveSession.presentation - ? `presentation:${liveSession.presentation.publication.trackSid}` + ? `presentation:${cls.id}` : null const presentationDimensions = liveSession.presentation?.publication.dimensions diff --git a/features/session/use-live-session.ts b/features/session/use-live-session.ts index 701d205..93037ba 100644 --- a/features/session/use-live-session.ts +++ b/features/session/use-live-session.ts @@ -434,6 +434,10 @@ function isWhiteboardMessage( return true } + if (value.type === "session:clear") { + return value.boardId === undefined + } + if ( value.type === "state:sync" && typeof value.version === "number" && @@ -1222,6 +1226,17 @@ export function useLiveSession({ [upsertNotice], ) + const clearWhiteboards = useCallback(() => { + return sendWhiteboardMessage( + { + id: createChatMessageId(currentUserId), + senderId: currentUserId, + type: "session:clear", + }, + { reliable: true }, + ) + }, [currentUserId, sendWhiteboardMessage]) + const sendChatMessage = useCallback( async (content: string) => { const trimmed = content.trim() @@ -1482,6 +1497,7 @@ export function useLiveSession({ toggleCamera, toggleScreenShare, sendWhiteboardMessage, + clearWhiteboards, sendChatMessage, dismissNotice, disconnect, diff --git a/features/session/use-whiteboard.ts b/features/session/use-whiteboard.ts index 3c06401..df509f2 100644 --- a/features/session/use-whiteboard.ts +++ b/features/session/use-whiteboard.ts @@ -135,6 +135,10 @@ function createEmptyBoardState(): BoardState { } function getMessageBoardId(message: LiveSessionWhiteboardMessage) { + if (message.type === "session:clear") { + return REGULAR_WHITEBOARD_BOARD_ID + } + return message.boardId ?? REGULAR_WHITEBOARD_BOARD_ID } @@ -1158,6 +1162,20 @@ export function useWhiteboard({ pendingStrokePoints.current = [] }, []) + const clearAllBoards = useCallback(() => { + boardStates.current.clear() + operations.current = [] + redoOperations.current = [] + boardVersion.current = 0 + selectedOperationIds.current = new Set() + remoteStrokes.current.clear() + stateRequested.current.clear() + setHasSelection(false) + setRedoCount(0) + resetDrawingState() + resetBoard() + }, [resetBoard, resetDrawingState]) + const handlePointerDown = (event: ReactPointerEvent) => { if ( !isTeacher || @@ -1689,11 +1707,11 @@ export function useWhiteboard({ return } - if (!isTeacher && !stateRequested.current.has(boardId)) { + if (!stateRequested.current.has(boardId)) { stateRequested.current.add(boardId) sendWhiteboardMessage({ type: "state:request" }, { reliable: true }) } - }, [boardId, isTeacher, sendWhiteboardMessage, syncEnabled]) + }, [boardId, sendWhiteboardMessage, syncEnabled]) useEffect(() => { if (!isTeacher || !syncEnabled) { @@ -1723,14 +1741,19 @@ export function useWhiteboard({ continue } - if (getMessageBoardId(message) !== boardId) { + processedMessageIds.current.add(message.id) + + if (message.type === "session:clear") { + clearAllBoards() continue } - processedMessageIds.current.add(message.id) + if (getMessageBoardId(message) !== boardId) { + continue + } - if (isTeacher) { - if (message.type === "state:request") { + if (message.type === "state:request") { + if (operations.current.length > 0 || boardVersion.current > 0) { sendStateSync() } continue @@ -1881,6 +1904,7 @@ export function useWhiteboard({ } }, [ boardId, + clearAllBoards, currentUserId, getContext, incomingMessages, From fceca9c1c06b4227d8a7ebef728514a5a512e6a9 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev Date: Tue, 5 May 2026 10:08:48 +0200 Subject: [PATCH 3/9] ver 3 --- features/session/live-session-provider.tsx | 8 ++++++++ features/session/live-session-types.ts | 6 ++++++ features/session/session-screen.tsx | 12 +++++++++-- features/session/use-live-session.ts | 24 +++++++++++++++++++++- features/session/use-whiteboard.ts | 4 ++-- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/features/session/live-session-provider.tsx b/features/session/live-session-provider.tsx index 502b643..a670636 100644 --- a/features/session/live-session-provider.tsx +++ b/features/session/live-session-provider.tsx @@ -105,12 +105,19 @@ export function LiveSessionProvider({ children }: { children: ReactNode }) { const disconnectRef = useRef<() => void>(() => {}) const isTeacher = currentUser.role === "teacher" const currentSessionScope = `${activeOrganization?.id ?? ""}:${currentUser.id}:${currentUser.role}` + const handleRemoteSessionEnded = useCallback(() => { + setSessionActive(false) + setSessionScope(null) + setHasJoinedSession(true) + void refreshClassLiveSessions({ force: true }).catch(() => {}) + }, [refreshClassLiveSessions]) const liveSession = useLiveSession({ classId: activeClass?.id ?? "", currentUser, enabled: Boolean( activeClass && sessionActive && sessionScope === currentSessionScope, ), + onSessionEnded: handleRemoteSessionEnded, }) const connected = liveSession.connectionState === ConnectionState.Connected @@ -156,6 +163,7 @@ export function LiveSessionProvider({ children }: { children: ReactNode }) { if (isTeacher) { await liveSession.clearWhiteboards().catch(() => false) + await liveSession.endSessionForEveryone().catch(() => false) } liveSession.disconnect() diff --git a/features/session/live-session-types.ts b/features/session/live-session-types.ts index e30f1b1..b614006 100644 --- a/features/session/live-session-types.ts +++ b/features/session/live-session-types.ts @@ -211,6 +211,11 @@ export type LiveSessionWhiteboardMessage = senderId: string type: "session:clear" } + | { + id: string + senderId: string + type: "session:end" + } export interface LiveSessionChatMessage { id: string @@ -245,6 +250,7 @@ export interface LiveSessionState { options?: { reliable?: boolean }, ) => Promise clearWhiteboards: () => Promise + endSessionForEveryone: () => Promise sendChatMessage: (content: string) => Promise dismissNotice: (noticeId: string) => void disconnect: () => void diff --git a/features/session/session-screen.tsx b/features/session/session-screen.tsx index 4a11e40..c2acd45 100644 --- a/features/session/session-screen.tsx +++ b/features/session/session-screen.tsx @@ -135,7 +135,7 @@ function SessionNoticeStack({ } export function SessionScreen({ cls }: { cls: Class }) { - const { currentUser } = useApp() + const { classLiveSessions, currentUser } = useApp() const { activeClass, endSession, @@ -150,6 +150,11 @@ export function SessionScreen({ cls }: { cls: Class }) { ) const isTeacher = currentUser.role === "teacher" + const canStartSession = currentUser.role !== "student" + const classHasLiveSession = classLiveSessions.some( + (session) => session.class_id === cls.id, + ) + const canJoinSession = canStartSession || classHasLiveSession const isThisClassSession = activeClass?.id === cls.id && sessionActive const hasJoinedThisClass = activeClass?.id === cls.id && hasJoinedSession const presentationStageRef = useRef(null) @@ -274,7 +279,9 @@ export function SessionScreen({ cls }: { cls: Class }) { const title = hasJoinedThisClass ? "Session ended" : "Ready to join" const description = hasJoinedThisClass ? `You left the live session for ${cls.name}.` - : `Join the live session for ${cls.name} when you are ready.` + : canJoinSession + ? `Join the live session for ${cls.name} when you are ready.` + : `The live session for ${cls.name} has not started yet.` const buttonLabel = hasJoinedThisClass ? "Rejoin" : "Join session" return ( @@ -284,6 +291,7 @@ export function SessionScreen({ cls }: { cls: Class }) {

{description}

+
+
+ + +
+

{title}

+

{description}

+
+ +
) } @@ -355,16 +375,27 @@ export function SessionScreen({ cls }: { cls: Class }) { /> ) : null} - - {isTeacher ? ( + {isTeacherPreviewSession ? ( + + ) : ( + + )} + {isTeacher && !isTeacherPreviewSession ? (