mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix(session-sharing): show participant presence in member terminals (#1504)
This commit is contained in:
1 parent
0f6e48a95c
commit
4ed9b69aab
6 files changed
+114
-5
No files matched your search
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
ParticipantsBadge,
|
||||
type SessionParticipantInfo,
|
||||
} from "./SharedSessionView";
|
||||
|
||||
interface ParticipantsOverlayProps {
|
||||
subscribe: (
|
||||
listener: (message: { type: string; [key: string]: unknown }) => void,
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export function ParticipantsOverlay({ subscribe }: ParticipantsOverlayProps) {
|
||||
const { t } = useTranslation();
|
||||
const [participants, setParticipants] = useState<SessionParticipantInfo[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
subscribe((message) => {
|
||||
if (
|
||||
message.type === "participants" &&
|
||||
Array.isArray(message.participants)
|
||||
) {
|
||||
setParticipants(message.participants as SessionParticipantInfo[]);
|
||||
} else if (message.type === "session_closed") {
|
||||
setParticipants([]);
|
||||
}
|
||||
}),
|
||||
[subscribe],
|
||||
);
|
||||
|
||||
return (
|
||||
<ParticipantsBadge
|
||||
participants={participants}
|
||||
ownerLabel={t("sessionSharing.guestView.ownerLabel")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export interface SessionParticipantInfo {
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
function ParticipantsBadge({
|
||||
export function ParticipantsBadge({
|
||||
participants,
|
||||
ownerLabel,
|
||||
}: {
|
||||
|
||||
@@ -17,6 +17,7 @@ import CollabGuestView from "./CollabGuestView";
|
||||
import { CollabPanel } from "./CollabPanel";
|
||||
import { CollabRoomTab } from "./CollabRoomTab";
|
||||
import SharedSessionView from "./SharedSessionView";
|
||||
import { ParticipantsOverlay } from "./ParticipantsOverlay";
|
||||
import {
|
||||
ShareDialogHost,
|
||||
closeShareDialog,
|
||||
@@ -224,6 +225,15 @@ export function activate(app: TermixApp): void {
|
||||
},
|
||||
});
|
||||
|
||||
app.registerSlotContribution("terminal.overlay", {
|
||||
actionId: "session-sharing.participants",
|
||||
titleKey: "collab.members",
|
||||
kind: "component",
|
||||
component: ParticipantsOverlay as unknown as ComponentType<
|
||||
Record<string, unknown>
|
||||
>,
|
||||
});
|
||||
|
||||
app.registerSlotContribution("shell.overlay", {
|
||||
actionId: "session-sharing.shareDialog",
|
||||
titleKey: "sessionSharing.shareButton",
|
||||
|
||||
@@ -75,6 +75,52 @@ describe("session-sharing activate", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows terminal participants and clears them after the last guest leaves or the socket closes", async () => {
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
locales,
|
||||
api: stubApi(),
|
||||
});
|
||||
expect(rendered.registered.slot("terminal.overlay")).toEqual([
|
||||
"session-sharing.participants",
|
||||
]);
|
||||
let receive: (message: {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}) => void = () => {};
|
||||
const unsubscribe = vi.fn();
|
||||
rendered.renderSlot("terminal.overlay", {
|
||||
subscribe: (listener: typeof receive) => {
|
||||
receive = listener;
|
||||
return unsubscribe;
|
||||
},
|
||||
});
|
||||
const owner = { isOwner: true, permissionLevel: "read-write", label: null };
|
||||
const guest = {
|
||||
isOwner: false,
|
||||
permissionLevel: "read-only",
|
||||
label: "Guest 1",
|
||||
};
|
||||
await act(() =>
|
||||
receive({ type: "participants", participants: [owner, guest] }),
|
||||
);
|
||||
expect(
|
||||
screen.getByTitle(
|
||||
`${locales.sessionSharing.guestView.ownerLabel}, Guest 1`,
|
||||
).textContent,
|
||||
).toBe("2");
|
||||
await act(() => receive({ type: "participants", participants: [owner] }));
|
||||
expect(screen.queryByText("2")).toBeNull();
|
||||
await act(() =>
|
||||
receive({ type: "participants", participants: [owner, guest] }),
|
||||
);
|
||||
await act(() => receive({ type: "session_closed" }));
|
||||
expect(screen.queryByText("2")).toBeNull();
|
||||
await rendered.deactivate();
|
||||
expect(unsubscribe).toHaveBeenCalled();
|
||||
rendered = null;
|
||||
});
|
||||
|
||||
it("registers only the guest views on a guest page", async () => {
|
||||
rendered = await renderWithApp(plugin, {
|
||||
manifest,
|
||||
|
||||
@@ -471,7 +471,7 @@ export class TerminalSessionManager {
|
||||
* leave, and only while someone besides the owner is (or just was) in the
|
||||
* room - a solo owner never receives presence traffic.
|
||||
*/
|
||||
private broadcastParticipants(sessionId: string): void {
|
||||
private broadcastParticipants(sessionId: string, includeSolo = false): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
const participants = Array.from(session.participants.values()).map(
|
||||
@@ -481,7 +481,11 @@ export class TerminalSessionManager {
|
||||
label: participant.displayName ?? participant.guestLabel ?? null,
|
||||
}),
|
||||
);
|
||||
if (participants.every((participant) => participant.isOwner)) return;
|
||||
if (
|
||||
!includeSolo &&
|
||||
participants.every((participant) => participant.isOwner)
|
||||
)
|
||||
return;
|
||||
this.broadcast(sessionId, { type: "participants", participants });
|
||||
}
|
||||
|
||||
@@ -545,7 +549,7 @@ export class TerminalSessionManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disconnected > 0) this.broadcastParticipants(sessionId);
|
||||
if (disconnected > 0) this.broadcastParticipants(sessionId, true);
|
||||
return disconnected;
|
||||
}
|
||||
|
||||
@@ -585,7 +589,7 @@ export class TerminalSessionManager {
|
||||
for (const [id, participant] of session.participants.entries()) {
|
||||
if (participant.ws === ws && !participant.isOwner) {
|
||||
session.participants.delete(id);
|
||||
this.broadcastParticipants(sessionId);
|
||||
this.broadcastParticipants(sessionId, true);
|
||||
this.log.info("Participant left shared session", {
|
||||
operation: "session_leave_participant",
|
||||
sessionId,
|
||||
|
||||
@@ -417,6 +417,14 @@ describe("TerminalSessionManager - multiplayer participants", () => {
|
||||
expect(session?.detachTimeout).toBeNull();
|
||||
expect(session?.participants.size).toBe(1);
|
||||
expect(sessionManager.getParticipantForWs(session!, guestWs)).toBeNull();
|
||||
expect(
|
||||
JSON.parse(
|
||||
(ownerWs.send as ReturnType<typeof vi.fn>).mock.lastCall![0] as string,
|
||||
),
|
||||
).toMatchObject({
|
||||
type: "participants",
|
||||
participants: [{ isOwner: true }],
|
||||
});
|
||||
|
||||
sessionManager.destroySession(id);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user