mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
fix: show meeting guests to members and hide the roster from guests (#1353)
This commit is contained in:
1 parent
ab43563247
commit
f896e121e2
14 files changed
+254
-19
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [stage, setStage] = useState<CollabGuestStage | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [participantCount, setParticipantCount] = useState(0);
|
||||
const stageShareIdRef = useRef<string | null>(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")}
|
||||
</span>
|
||||
<div className="flex items-center gap-3 shrink-0 ml-auto text-xs text-muted-foreground">
|
||||
{participantCount >= 2 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="size-3.5" />
|
||||
{participantCount}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Eye className="size-3.5" />
|
||||
{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 ? (
|
||||
<RemoteDisplay
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
Check,
|
||||
Crown,
|
||||
Eye,
|
||||
Hand,
|
||||
MonitorUp,
|
||||
MousePointerClick,
|
||||
@@ -31,6 +32,7 @@ interface CollabMembersSidebarProps {
|
||||
onControl: (userId: string | null) => Promise<void>;
|
||||
onDismissRequest: (userId: string) => Promise<void>;
|
||||
onRemoveMember: (userId: string) => Promise<void>;
|
||||
onRemoveGuests: () => Promise<void>;
|
||||
}
|
||||
|
||||
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<string | null>(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({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{guests.length > 0 && (
|
||||
<div className="mt-3 border-t border-border pt-2">
|
||||
<div className="flex items-center gap-2 px-2 pb-1">
|
||||
<Eye className="size-3.5 text-muted-foreground" />
|
||||
<h3 className="flex-1 text-xs font-semibold uppercase tracking-wide">
|
||||
{t("collab.guestsWithCount", { count: guests.length })}
|
||||
</h3>
|
||||
{detail.isHost && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 text-xs"
|
||||
disabled={busyUserId === "guests"}
|
||||
onClick={() => void run("guests", onRemoveGuests)}
|
||||
>
|
||||
{t("collab.removeGuests")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{guests.map((guest, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex min-h-9 items-center gap-2 px-2 py-1 text-xs"
|
||||
>
|
||||
<span className="size-2 shrink-0 rounded-full bg-green-500" />
|
||||
<span className="truncate">
|
||||
{guest.label || t("collab.anonymousGuest")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<AlertDialog
|
||||
|
||||
@@ -103,6 +103,7 @@ export function CollabRoomTab({
|
||||
presentCollabStage,
|
||||
requestCollabStageControl,
|
||||
removeCollabMember,
|
||||
removeCollabGuests,
|
||||
setCollabGuestLink,
|
||||
setCollabStageControl,
|
||||
stopCollabStage,
|
||||
@@ -310,6 +311,16 @@ export function CollabRoomTab({
|
||||
}
|
||||
}
|
||||
|
||||
async function removeGuests() {
|
||||
if (!roomId) return;
|
||||
try {
|
||||
await removeCollabGuests(roomId);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMember(targetId: string) {
|
||||
if (!roomId) return;
|
||||
try {
|
||||
@@ -607,7 +618,7 @@ export function CollabRoomTab({
|
||||
onClick={() => setMembersOpen((open) => !open)}
|
||||
>
|
||||
<Users className="mr-1 size-3.5" />
|
||||
{detail?.members.length ?? 0}
|
||||
{(detail?.members.length ?? 0) + (detail?.guests?.length ?? 0)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -779,6 +790,7 @@ export function CollabRoomTab({
|
||||
onControl={changeControl}
|
||||
onDismissRequest={dismissControlRequest}
|
||||
onRemoveMember={removeMember}
|
||||
onRemoveGuests={removeGuests}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -259,6 +259,8 @@ export interface CollabRoomDetail {
|
||||
members: CollabRoomMember[];
|
||||
online: CollabOnlineUser[];
|
||||
stage: CollabStage;
|
||||
/** Anonymous guests watching the stage through the guest link. */
|
||||
guests?: { label: string | null }[];
|
||||
controllerUserId: string | null;
|
||||
controlRequests: CollabControlRequest[];
|
||||
/** Where live room events are served, as /plugin-ws/<id>/<path>. */
|
||||
|
||||
@@ -168,6 +168,13 @@ export function createMeetingApi(
|
||||
);
|
||||
}
|
||||
|
||||
async function removeCollabGuests(roomId: string): Promise<void> {
|
||||
await call(
|
||||
() => api().post(room(roomId, "/guests/remove")),
|
||||
"Failed to remove guests",
|
||||
);
|
||||
}
|
||||
|
||||
function getDirectory() {
|
||||
return call(
|
||||
() =>
|
||||
@@ -202,6 +209,7 @@ export function createMeetingApi(
|
||||
endCollabRoom,
|
||||
deleteCollabRoom,
|
||||
setCollabGuestLink,
|
||||
removeCollabGuests,
|
||||
getDirectory,
|
||||
resolveHost,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user