mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-30 02:41:34 +00:00
feat: improve collaboration rooms (#1338)
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Check,
|
||||
Crown,
|
||||
Hand,
|
||||
MonitorUp,
|
||||
MousePointerClick,
|
||||
UserMinus,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/badge";
|
||||
import { Button } from "@/components/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/alert-dialog";
|
||||
import type { CollabControlRequest, CollabRoomDetail } from "@/api/collab-api";
|
||||
|
||||
interface CollabMembersSidebarProps {
|
||||
detail: CollabRoomDetail;
|
||||
onClose: () => void;
|
||||
onInvite: () => void;
|
||||
onControl: (userId: string | null) => Promise<void>;
|
||||
onDismissRequest: (userId: string) => Promise<void>;
|
||||
onRemoveMember: (userId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function CollabMembersSidebar({
|
||||
detail,
|
||||
onClose,
|
||||
onInvite,
|
||||
onControl,
|
||||
onDismissRequest,
|
||||
onRemoveMember,
|
||||
}: CollabMembersSidebarProps) {
|
||||
const { t } = useTranslation();
|
||||
const [removing, setRemoving] = useState<{
|
||||
userId: string;
|
||||
username: string;
|
||||
} | null>(null);
|
||||
const [busyUserId, setBusyUserId] = useState<string | null>(null);
|
||||
const onlineIds = new Set(detail.online.map((user) => user.userId));
|
||||
const presenterUserId = detail.stage.presenterUserId;
|
||||
const canManageControl = detail.isHost || presenterUserId === detail.me;
|
||||
const canControl =
|
||||
canManageControl &&
|
||||
detail.stage.protocol === "ssh" &&
|
||||
!!detail.stage.shareId;
|
||||
|
||||
async function run(userId: string, action: () => Promise<void>) {
|
||||
setBusyUserId(userId);
|
||||
try {
|
||||
await action();
|
||||
} finally {
|
||||
setBusyUserId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function grant(request: CollabControlRequest) {
|
||||
await run(request.userId, async () => {
|
||||
await onControl(request.userId);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
id="collab-members-sidebar"
|
||||
aria-label={t("collab.membersPanel")}
|
||||
className="absolute inset-y-0 right-0 z-30 flex h-full w-[min(20rem,100%)] shrink-0 flex-col border-l border-border bg-background shadow-xl lg:static lg:z-auto lg:shadow-none"
|
||||
>
|
||||
<header className="flex h-11 items-center gap-2 border-b border-border px-3">
|
||||
<Users className="size-4 text-muted-foreground" />
|
||||
<h2 className="flex-1 text-sm font-semibold">
|
||||
{t("collab.membersWithCount", { count: detail.members.length })}
|
||||
</h2>
|
||||
{detail.isHost && (
|
||||
<Button size="sm" variant="outline" onClick={onInvite}>
|
||||
{t("collab.invite")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-label={t("common.close")}
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{canManageControl && (
|
||||
<section className="border-b border-border p-3" aria-live="polite">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Hand className="size-3.5 text-muted-foreground" />
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide">
|
||||
{t("collab.controlQueue")}
|
||||
</h3>
|
||||
{detail.controlRequests.length > 0 && (
|
||||
<Badge variant="default" className="ml-auto text-xs">
|
||||
{detail.controlRequests.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{detail.controlRequests.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("collab.noControlRequests")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{detail.controlRequests.map((request) => (
|
||||
<div
|
||||
key={request.userId}
|
||||
className="flex items-center gap-2 border border-border p-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-medium">
|
||||
{request.username}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(request.requestedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
aria-label={t("collab.grantControlTo", {
|
||||
name: request.username,
|
||||
})}
|
||||
disabled={busyUserId === request.userId}
|
||||
onClick={() => void grant(request)}
|
||||
>
|
||||
<Check className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
aria-label={t("collab.dismissControlRequest", {
|
||||
name: request.username,
|
||||
})}
|
||||
disabled={busyUserId === request.userId}
|
||||
onClick={() =>
|
||||
void run(request.userId, () =>
|
||||
onDismissRequest(request.userId),
|
||||
)
|
||||
}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
<h3 className="sr-only">{t("collab.members")}</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
{detail.members.map((member) => {
|
||||
const isOnline = onlineIds.has(member.userId);
|
||||
const isPresenter = member.userId === presenterUserId;
|
||||
const hasControl = member.userId === detail.controllerUserId;
|
||||
return (
|
||||
<div
|
||||
key={member.userId}
|
||||
className="group flex min-h-11 items-center gap-2 border border-transparent px-2 py-1.5 hover:border-border hover:bg-muted/40"
|
||||
>
|
||||
<span
|
||||
className={`size-2 shrink-0 rounded-full ${
|
||||
isOnline ? "bg-green-500" : "bg-muted-foreground/30"
|
||||
}`}
|
||||
aria-label={t(isOnline ? "collab.online" : "collab.offline")}
|
||||
role="img"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-medium">
|
||||
{member.username}
|
||||
{member.userId === detail.me && (
|
||||
<span className="ml-1 text-muted-foreground">
|
||||
{t("collab.you")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{member.roomRole === "host" && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Crown className="size-3" /> {t("collab.hostBadge")}
|
||||
</span>
|
||||
)}
|
||||
{isPresenter && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MonitorUp className="size-3" />
|
||||
{t("collab.presenterBadge")}
|
||||
</span>
|
||||
)}
|
||||
{hasControl && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MousePointerClick className="size-3" />
|
||||
{t("collab.controlBadge")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{canControl && !isPresenter && (
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant={hasControl ? "default" : "ghost"}
|
||||
aria-label={t(
|
||||
hasControl
|
||||
? "collab.revokeControl"
|
||||
: "collab.grantControlTo",
|
||||
{ name: member.username },
|
||||
)}
|
||||
disabled={busyUserId === member.userId}
|
||||
onClick={() =>
|
||||
void run(member.userId, () =>
|
||||
onControl(hasControl ? null : member.userId),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MousePointerClick className="size-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{detail.isHost && member.userId !== detail.room.ownerUserId && (
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
aria-label={t("collab.removeMember", {
|
||||
name: member.username,
|
||||
})}
|
||||
onClick={() =>
|
||||
setRemoving({
|
||||
userId: member.userId,
|
||||
username: member.username,
|
||||
})
|
||||
}
|
||||
>
|
||||
<UserMinus className="size-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(removing)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setRemoving(null);
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("collab.removeMemberTitle")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("collab.removeMemberDescription", {
|
||||
name: removing?.username,
|
||||
})}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => {
|
||||
if (removing) void onRemoveMember(removing.userId);
|
||||
}}
|
||||
>
|
||||
{t("collab.deleteMember")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -3,18 +3,15 @@ import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertCircle,
|
||||
Crown,
|
||||
Hand,
|
||||
Link2,
|
||||
Loader2,
|
||||
MonitorUp,
|
||||
MousePointerClick,
|
||||
Presentation,
|
||||
Square,
|
||||
UserPlus,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/button";
|
||||
import { Badge } from "@/components/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -33,6 +30,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/alert-dialog";
|
||||
import { Input } from "@/components/input";
|
||||
import { CollabMembersSidebar } from "./CollabMembersSidebar";
|
||||
import { Terminal } from "@/features/terminal/Terminal";
|
||||
import { CommandHistoryProvider } from "@/features/terminal/command-history/CommandHistoryContext";
|
||||
import { GuacamoleDisplay } from "@/features/guacamole/GuacamoleDisplay.tsx";
|
||||
@@ -45,11 +43,13 @@ import { isElectron } from "@/lib/electron";
|
||||
import { getErrorMessage } from "@/lib/error-message";
|
||||
import {
|
||||
endCollabRoom,
|
||||
dismissCollabControlRequest,
|
||||
getCollabRoom,
|
||||
getCollabStage,
|
||||
inviteCollabMembers,
|
||||
presentCollabStage,
|
||||
requestCollabStageControl,
|
||||
removeCollabMember,
|
||||
setCollabGuestLink,
|
||||
setCollabStageControl,
|
||||
stopCollabStage,
|
||||
@@ -108,6 +108,7 @@ export function CollabRoomTab({
|
||||
const [presentOpen, setPresentOpen] = useState(false);
|
||||
const [presentLoading, setPresentLoading] = useState(false);
|
||||
const [inviteOpen, setInviteOpen] = useState(false);
|
||||
const [membersOpen, setMembersOpen] = useState(true);
|
||||
const [endOpen, setEndOpen] = useState(false);
|
||||
const [takeoverChoice, setTakeoverChoice] = useState<PresentChoice | null>(
|
||||
null,
|
||||
@@ -224,19 +225,10 @@ export function CollabRoomTab({
|
||||
case "collab_members_changed":
|
||||
case "collab_stage_changed":
|
||||
case "collab_control_changed":
|
||||
case "collab_control_requested":
|
||||
case "collab_control_requests_changed":
|
||||
void refresh();
|
||||
break;
|
||||
case "collab_control_requested": {
|
||||
const request = msg as unknown as {
|
||||
userId: string;
|
||||
username?: string;
|
||||
};
|
||||
handleControlRequestRef.current?.(
|
||||
request.userId,
|
||||
request.username ?? "?",
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "collab_room_ended":
|
||||
setEnded(true);
|
||||
break;
|
||||
@@ -270,30 +262,49 @@ export function CollabRoomTab({
|
||||
const controllerUserId = detail?.controllerUserId ?? null;
|
||||
const presenterUserId = detail?.stage.presenterUserId ?? null;
|
||||
const iAmPresenter = !!me && presenterUserId === me;
|
||||
const onlineIds = new Set(detail?.online.map((user) => user.userId));
|
||||
const presenterName = detail?.members.find(
|
||||
(member) => member.userId === presenterUserId,
|
||||
)?.username;
|
||||
|
||||
const handleControlRequestRef = useRef<
|
||||
((userId: string, username: string) => void) | null
|
||||
>(null);
|
||||
handleControlRequestRef.current = (userId, username) => {
|
||||
if (!roomId) return;
|
||||
const mayGrant = isHost || iAmPresenter;
|
||||
if (!mayGrant || userId === me) return;
|
||||
toast(t("collab.controlRequestedBy", { name: username }), {
|
||||
action: {
|
||||
label: t("collab.grant"),
|
||||
onClick: () => void setCollabStageControl(roomId, userId),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function changeControl(targetId: string | null) {
|
||||
if (!roomId) return;
|
||||
try {
|
||||
await setCollabStageControl(roomId, targetId);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissControlRequest(targetId: string) {
|
||||
if (!roomId) return;
|
||||
try {
|
||||
await dismissCollabControlRequest(roomId, targetId);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMember(targetId: string) {
|
||||
if (!roomId) return;
|
||||
try {
|
||||
await removeCollabMember(roomId, targetId);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleOwnControlRequest() {
|
||||
if (!roomId || !me) return;
|
||||
try {
|
||||
const existing = detail?.controlRequests.some(
|
||||
(request) => request.userId === me,
|
||||
);
|
||||
if (existing) await dismissCollabControlRequest(roomId, me);
|
||||
else await requestCollabStageControl(roomId);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
@@ -507,70 +518,14 @@ export function CollabRoomTab({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* Header: roster + controls */}
|
||||
{/* Header: room identity + primary controls */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border flex-wrap">
|
||||
<Presentation className="size-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm font-semibold truncate">
|
||||
{detail?.room.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 flex-wrap flex-1 min-w-0">
|
||||
{detail?.members.map((member) => {
|
||||
const canToggleControl =
|
||||
(isHost || iAmPresenter) &&
|
||||
detail?.stage.protocol === "ssh" &&
|
||||
!!detail?.stage.shareId &&
|
||||
member.userId !== presenterUserId;
|
||||
const hasControl = member.userId === controllerUserId;
|
||||
const badge = (
|
||||
<Badge
|
||||
key={canToggleControl ? undefined : member.userId}
|
||||
variant={hasControl ? "default" : "outline"}
|
||||
className="text-xs gap-1"
|
||||
>
|
||||
<span
|
||||
className={`size-1.5 rounded-full ${onlineIds.has(member.userId) ? "bg-green-500" : "bg-muted-foreground/30"}`}
|
||||
/>
|
||||
{member.username}
|
||||
{member.roomRole === "host" && <Crown className="size-2.5" />}
|
||||
{member.userId === presenterUserId && (
|
||||
<MonitorUp className="size-2.5 text-red-500" />
|
||||
)}
|
||||
{hasControl && <MousePointerClick className="size-2.5" />}
|
||||
</Badge>
|
||||
);
|
||||
return canToggleControl ? (
|
||||
<button
|
||||
key={member.userId}
|
||||
type="button"
|
||||
title={t(
|
||||
hasControl ? "collab.revokeControl" : "collab.grantControl",
|
||||
)}
|
||||
aria-label={`${member.username}: ${t(
|
||||
hasControl ? "collab.revokeControl" : "collab.grantControl",
|
||||
)}`}
|
||||
onClick={() =>
|
||||
void changeControl(hasControl ? null : member.userId)
|
||||
}
|
||||
>
|
||||
{badge}
|
||||
</button>
|
||||
) : (
|
||||
badge
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{isHost && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 text-xs"
|
||||
onClick={() => void openInviteDialog()}
|
||||
>
|
||||
<UserPlus className="size-3.5 mr-1" />
|
||||
{t("collab.invite")}
|
||||
</Button>
|
||||
)}
|
||||
{!!detail?.stage.shareId &&
|
||||
detail.stage.protocol === "ssh" &&
|
||||
!iAmPresenter &&
|
||||
@@ -582,15 +537,17 @@ export function CollabRoomTab({
|
||||
onClick={() =>
|
||||
controllerUserId === me
|
||||
? void changeControl(null)
|
||||
: void requestCollabStageControl(roomId).catch((error) =>
|
||||
toast.error(getErrorMessage(error)),
|
||||
)
|
||||
: void toggleOwnControlRequest()
|
||||
}
|
||||
>
|
||||
<Hand className="size-3.5 mr-1" />
|
||||
{controllerUserId === me
|
||||
? t("collab.releaseControl")
|
||||
: t("collab.requestControl")}
|
||||
: detail.controlRequests.some(
|
||||
(request) => request.userId === me,
|
||||
)
|
||||
? t("collab.cancelControlRequest")
|
||||
: t("collab.requestControl")}
|
||||
</Button>
|
||||
)}
|
||||
{(iAmPresenter || draft || (isHost && presenterUserId)) && (
|
||||
@@ -604,6 +561,17 @@ export function CollabRoomTab({
|
||||
{t("collab.stopPresenting")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={membersOpen ? "default" : "outline"}
|
||||
className="h-8 text-xs"
|
||||
aria-controls="collab-members-sidebar"
|
||||
aria-expanded={membersOpen}
|
||||
onClick={() => setMembersOpen((open) => !open)}
|
||||
>
|
||||
<Users className="mr-1 size-3.5" />
|
||||
{detail?.members.length ?? 0}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
@@ -676,91 +644,103 @@ export function CollabRoomTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stage */}
|
||||
<div className="relative flex-1 min-h-0">
|
||||
{draft ? (
|
||||
draft.protocol === "ssh" ? (
|
||||
<CommandHistoryProvider>
|
||||
<Terminal
|
||||
hostConfig={{
|
||||
...draft.host,
|
||||
id: Number(draft.host.id),
|
||||
ip: draft.host.ip,
|
||||
port: draft.host.port,
|
||||
username: draft.host.username,
|
||||
instanceId: `collab-present-${roomId}`,
|
||||
}}
|
||||
isVisible={isVisible}
|
||||
disableAutoFocus={false}
|
||||
onSessionReady={(sessionId) =>
|
||||
void registerStage("ssh", sessionId, Number(draft.host.id))
|
||||
}
|
||||
/>
|
||||
</CommandHistoryProvider>
|
||||
) : (
|
||||
<GuacamoleDisplay
|
||||
connectionConfig={{
|
||||
token: draft.token,
|
||||
protocol: draft.protocol,
|
||||
type: draft.protocol,
|
||||
}}
|
||||
isVisible={isVisible}
|
||||
onConnect={() =>
|
||||
void registerStage(
|
||||
draft.protocol,
|
||||
draft.guacamoleConnectionId,
|
||||
Number(draft.host.id),
|
||||
)
|
||||
}
|
||||
onError={(err) => {
|
||||
toast.error(err);
|
||||
setDraft(null);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : stage && stage.protocol && !iAmPresenter ? (
|
||||
<>
|
||||
{presenterName && (
|
||||
<div className="absolute top-2 left-2 z-20 rounded px-2 py-0.5 text-[10px] bg-background/80 border border-border">
|
||||
{t("collab.presenterLabel", { name: presenterName })}
|
||||
</div>
|
||||
)}
|
||||
{stage.protocol === "ssh" ? (
|
||||
<div className="relative flex flex-1 min-h-0">
|
||||
{/* Stage */}
|
||||
<div className="relative flex-1 min-w-0 min-h-0">
|
||||
{draft ? (
|
||||
draft.protocol === "ssh" ? (
|
||||
<CommandHistoryProvider>
|
||||
<Terminal
|
||||
hostConfig={{
|
||||
id: stage.hostId ?? undefined,
|
||||
name: detail?.room.name ?? "stage",
|
||||
ip: "",
|
||||
port: 0,
|
||||
username: "",
|
||||
authType: "none",
|
||||
instanceId: `collab-view-${roomId}-${stage.shareId}`,
|
||||
joinShareId: stage.shareId,
|
||||
joinSharedSessionId: stage.sessionId ?? null,
|
||||
...draft.host,
|
||||
id: Number(draft.host.id),
|
||||
ip: draft.host.ip,
|
||||
port: draft.host.port,
|
||||
username: draft.host.username,
|
||||
instanceId: `collab-present-${roomId}`,
|
||||
}}
|
||||
isVisible={isVisible}
|
||||
disableAutoFocus
|
||||
disableAutoFocus={false}
|
||||
onSessionReady={(sessionId) =>
|
||||
void registerStage("ssh", sessionId, Number(draft.host.id))
|
||||
}
|
||||
/>
|
||||
</CommandHistoryProvider>
|
||||
) : stage.connectParams?.token ? (
|
||||
) : (
|
||||
<GuacamoleDisplay
|
||||
key={stage.connectParams.token}
|
||||
connectionConfig={{
|
||||
token: stage.connectParams.token,
|
||||
protocol: stage.protocol,
|
||||
type: stage.protocol,
|
||||
token: draft.token,
|
||||
protocol: draft.protocol,
|
||||
type: draft.protocol,
|
||||
}}
|
||||
isVisible={isVisible}
|
||||
onConnect={() =>
|
||||
void registerStage(
|
||||
draft.protocol,
|
||||
draft.guacamoleConnectionId,
|
||||
Number(draft.host.id),
|
||||
)
|
||||
}
|
||||
onError={(err) => {
|
||||
toast.error(err);
|
||||
setDraft(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CenteredNote text={t("collab.stageLoading")} />
|
||||
)}
|
||||
</>
|
||||
) : iAmPresenter && !draft ? (
|
||||
<CenteredNote text={t("collab.youArePresenting")} />
|
||||
) : (
|
||||
<CenteredNote text={t("collab.emptyStage")} />
|
||||
)
|
||||
) : stage && stage.protocol && !iAmPresenter ? (
|
||||
<>
|
||||
{presenterName && (
|
||||
<div className="absolute top-2 left-2 z-20 rounded px-2 py-0.5 text-[10px] bg-background/80 border border-border">
|
||||
{t("collab.presenterLabel", { name: presenterName })}
|
||||
</div>
|
||||
)}
|
||||
{stage.protocol === "ssh" ? (
|
||||
<CommandHistoryProvider>
|
||||
<Terminal
|
||||
hostConfig={{
|
||||
id: stage.hostId ?? undefined,
|
||||
name: detail?.room.name ?? "stage",
|
||||
ip: "",
|
||||
port: 0,
|
||||
username: "",
|
||||
authType: "none",
|
||||
instanceId: `collab-view-${roomId}-${stage.shareId}`,
|
||||
joinShareId: stage.shareId,
|
||||
joinSharedSessionId: stage.sessionId ?? null,
|
||||
}}
|
||||
isVisible={isVisible}
|
||||
disableAutoFocus
|
||||
/>
|
||||
</CommandHistoryProvider>
|
||||
) : stage.connectParams?.token ? (
|
||||
<GuacamoleDisplay
|
||||
key={stage.connectParams.token}
|
||||
connectionConfig={{
|
||||
token: stage.connectParams.token,
|
||||
protocol: stage.protocol,
|
||||
type: stage.protocol,
|
||||
}}
|
||||
isVisible={isVisible}
|
||||
/>
|
||||
) : (
|
||||
<CenteredNote text={t("collab.stageLoading")} />
|
||||
)}
|
||||
</>
|
||||
) : iAmPresenter && !draft ? (
|
||||
<CenteredNote text={t("collab.youArePresenting")} />
|
||||
) : (
|
||||
<CenteredNote text={t("collab.emptyStage")} />
|
||||
)}
|
||||
</div>
|
||||
{membersOpen && detail && (
|
||||
<CollabMembersSidebar
|
||||
detail={detail}
|
||||
onClose={() => setMembersOpen(false)}
|
||||
onInvite={() => void openInviteDialog()}
|
||||
onControl={changeControl}
|
||||
onDismissRequest={dismissControlRequest}
|
||||
onRemoveMember={removeMember}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user