From f896e121e2222d344f448c90495338906380363d Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 30 Sep 2026 12:09:05 -0500 Subject: [PATCH] fix: show meeting guests to members and hide the roster from guests (#1353) --- plugins/session-sharing/locales/en.json | 3 + plugins/session-sharing/src/backend/live.ts | 22 ++++++ .../src/backend/room-routes.ts | 70 +++++++++++++++++++ .../src/frontend/CollabGuestView.tsx | 18 +---- .../src/frontend/CollabMembersSidebar.tsx | 38 ++++++++++ .../src/frontend/CollabRoomTab.tsx | 14 +++- plugins/session-sharing/src/frontend/api.ts | 2 + .../src/frontend/meeting-api.ts | 8 +++ .../session-sharing/tests/backend/helpers.ts | 1 + .../tests/backend/rooms.test.ts | 27 +++++++ plugins/ssh-terminal/src/backend/index.ts | 2 + plugins/ssh-terminal/src/backend/services.ts | 5 ++ .../src/backend/session-manager.ts | 30 +++++++- .../tests/backend/session-manager.test.ts | 33 +++++++++ 14 files changed, 254 insertions(+), 19 deletions(-) diff --git a/plugins/session-sharing/locales/en.json b/plugins/session-sharing/locales/en.json index d1b826ee0..110b1dd38 100644 --- a/plugins/session-sharing/locales/en.json +++ b/plugins/session-sharing/locales/en.json @@ -71,6 +71,9 @@ "members": "Members", "membersPanel": "Room members and control requests", "membersWithCount": "Members ({{count}})", + "guestsWithCount": "Guests ({{count}})", + "removeGuests": "Remove guests", + "anonymousGuest": "Guest", "online": "online", "offline": "offline", "you": "(you)", diff --git a/plugins/session-sharing/src/backend/live.ts b/plugins/session-sharing/src/backend/live.ts index 904b036d3..bf6eb8417 100644 --- a/plugins/session-sharing/src/backend/live.ts +++ b/plugins/session-sharing/src/backend/live.ts @@ -34,6 +34,10 @@ export interface LiveSessionProvider { shareId: string, controllerUserId: string | null, ) => void; + listGuests?: ( + sessionId: string, + shareId: string, + ) => { label: string | null }[]; createViewerToken?: (sessionId: string, readOnly: boolean) => string; } @@ -104,6 +108,24 @@ export function createLiveSessions(ctx: PluginContext) { return !!session && session.isConnected && session.userId === userId; }, + /** Anonymous link guests on one share; empty when the type cannot tell. */ + async listGuests( + protocol: string, + sessionId: string, + shareId: string, + asUser?: string, + ): Promise<{ label: string | null }[]> { + const live = provider(protocol); + if (!live || !("listGuests" in live)) return []; + try { + return ( + (await run(asUser, () => live.listGuests(sessionId, shareId))) ?? [] + ); + } catch { + return []; + } + }, + /** Ends the session for every guest. Best effort, like before. */ async ownerEndSession( protocol: string, diff --git a/plugins/session-sharing/src/backend/room-routes.ts b/plugins/session-sharing/src/backend/room-routes.ts index fe6c6f54b..25b0ca2dd 100644 --- a/plugins/session-sharing/src/backend/room-routes.ts +++ b/plugins/session-sharing/src/backend/room-routes.ts @@ -364,6 +364,16 @@ export function registerRoomRoutes( } const members = await rooms.listMembers(roomId); + const stageShare = await liveStageShare(access.room); + const guests = + stageShare && access.room.stageProtocol + ? await live.listGuests( + access.room.stageProtocol, + stageShare.sessionId, + stageShare.id, + stageShare.ownerUserId, + ) + : []; const mayReview = access.isHost || access.room.presenterUserId === userId; const controlRequests = await store.listRequests(roomId); res.json({ @@ -373,6 +383,7 @@ export function registerRoomRoutes( members, online: await hub.onlineUsers(roomId), stage: stagePayload(access.room), + guests, controllerUserId: await store.getController(roomId), controlRequests: mayReview ? controlRequests @@ -1124,6 +1135,65 @@ export function registerRoomRoutes( }, ); + /** + * @openapi + * /plugin-api/session-sharing/rooms/{id}/guests/remove: + * post: + * summary: Disconnect every anonymous guest watching the stage (host only) + * description: The guest link stays valid, so a guest may open it again. + * tags: + * - Collab + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Guests disconnected + * 403: + * description: Not the host + * 404: + * description: Not a member, or no such room + */ + router.post( + "/rooms/:id/guests/remove", + requireUse, + async (req: Request, res: Response) => { + const userId = actorOf(ctx); + const roomId = String(req.params.id); + try { + const access = await requireRoomMember(roomId, userId); + if (!access) return res.status(404).json({ error: "Room not found" }); + if (!access.isHost) { + return res + .status(403) + .json({ error: "Only the host can remove guests" }); + } + const share = await liveStageShare(access.room); + if (share && access.room.stageProtocol) { + await live.disconnectParticipants( + access.room.stageProtocol, + share.sessionId, + share.id, + { userId: null, reason: "The host removed the guests" }, + share.ownerUserId, + ); + } + res.json({ success: true }); + } catch (error) { + fail( + res, + "collab_guests_remove_error", + "Failed to remove guests", + )(error); + } + }, + ); + /** * @openapi * /plugin-api/session-sharing/rooms/{id}/guest-link: diff --git a/plugins/session-sharing/src/frontend/CollabGuestView.tsx b/plugins/session-sharing/src/frontend/CollabGuestView.tsx index 03c628876..af8f2f642 100644 --- a/plugins/session-sharing/src/frontend/CollabGuestView.tsx +++ b/plugins/session-sharing/src/frontend/CollabGuestView.tsx @@ -1,11 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { AlertCircle, Eye, Presentation, Users } from "lucide-react"; +import { AlertCircle, Eye, Presentation } from "lucide-react"; import { RemoteDisplay } from "./shared"; -import { - GuestTerminalView, - type SessionParticipantInfo, -} from "./SharedSessionView"; +import { GuestTerminalView } from "./SharedSessionView"; import { resolveCollabGuestStage, type CollabGuestStage } from "./api"; const POLL_MS = 5000; @@ -21,7 +18,6 @@ export default function CollabGuestView() { const [roomName, setRoomName] = useState(null); const [stage, setStage] = useState(null); const [error, setError] = useState(null); - const [participantCount, setParticipantCount] = useState(0); const stageShareIdRef = useRef(null); useEffect(() => { @@ -41,7 +37,6 @@ export default function CollabGuestView() { if (nextShareId !== stageShareIdRef.current) { stageShareIdRef.current = nextShareId; setStage(result.stage); - setParticipantCount(0); } } catch { if (!cancelled) setError(t("collab.guest.linkInvalid")); @@ -66,12 +61,6 @@ export default function CollabGuestView() { {roomName ?? t("collab.guest.title")}
- {participantCount >= 2 && ( - - - {participantCount} - - )} {t("sessionSharing.guestView.readOnlyBadge")} @@ -92,9 +81,6 @@ export default function CollabGuestView() { share={{ permissionLevel: "read-only" }} wsPath={stage.wsPath ?? ""} hideBadges - onParticipantsChange={(participants: SessionParticipantInfo[]) => - setParticipantCount(participants.length) - } /> ) : stage.connectParams?.token ? ( Promise; onDismissRequest: (userId: string) => Promise; onRemoveMember: (userId: string) => Promise; + onRemoveGuests: () => Promise; } export function CollabMembersSidebar({ @@ -40,6 +42,7 @@ export function CollabMembersSidebar({ onControl, onDismissRequest, onRemoveMember, + onRemoveGuests, }: CollabMembersSidebarProps) { const { t } = useTranslation(); const [removing, setRemoving] = useState<{ @@ -47,6 +50,7 @@ export function CollabMembersSidebar({ username: string; } | null>(null); const [busyUserId, setBusyUserId] = useState(null); + const guests = detail.guests ?? []; const onlineIds = new Set(detail.online.map((user) => user.userId)); const presenterUserId = detail.stage.presenterUserId; const canManageControl = detail.isHost || presenterUserId === detail.me; @@ -252,6 +256,40 @@ export function CollabMembersSidebar({ ); })}
+ {guests.length > 0 && ( +
+
+ +

+ {t("collab.guestsWithCount", { count: guests.length })} +

+ {detail.isHost && ( + + )} +
+
+ {guests.map((guest, index) => ( +
+ + + {guest.label || t("collab.anonymousGuest")} + +
+ ))} +
+
+ )} setMembersOpen((open) => !open)} > - {detail?.members.length ?? 0} + {(detail?.members.length ?? 0) + (detail?.guests?.length ?? 0)}