diff --git a/apps/geolibre-desktop/src/components/layout/CollaborateDialog.tsx b/apps/geolibre-desktop/src/components/layout/CollaborateDialog.tsx index fb51b0be8..6fcc98069 100644 --- a/apps/geolibre-desktop/src/components/layout/CollaborateDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/CollaborateDialog.tsx @@ -15,6 +15,7 @@ import { QRCodeSVG } from "qrcode.react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import type { CollaborationApi } from "../../hooks/useCollaboration"; +import { fetchCollabCapabilities } from "../../lib/collab-client"; import { CollaborationParticipantRow } from "./CollaborationParticipantRow"; // A small fixed palette so participant colors stay distinct and legible. Each @@ -61,6 +62,11 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog const [error, setError] = useState(null); const [copied, setCopied] = useState<"code" | "link" | null>(null); const copyTimer = useRef(null); + // Whether the relay can verify a sign-in. Probed when the dialog opens + // because the "require a signed-in account" option has to be decided before + // a session exists; false until the probe answers, so the option is hidden + // rather than briefly offered on a relay that would reject it. + const [identitySupported, setIdentitySupported] = useState(false); // Seed from a prior session (or a ?collab= deep link) when the dialog opens. useEffect(() => { @@ -87,6 +93,19 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog } }, [open, collaboration.selfName, collaboration.selfColor]); + // Probe relay capabilities on open. Ignored if the dialog closes first, so a + // slow relay can't flip the option on after the user has moved away. + useEffect(() => { + if (!open) return; + let cancelled = false; + void fetchCollabCapabilities().then((caps) => { + if (!cancelled) setIdentitySupported(caps.identitySupported); + }); + return () => { + cancelled = true; + }; + }, [open]); + useEffect( () => () => { if (copyTimer.current !== null) window.clearTimeout(copyTimer.current); @@ -114,6 +133,8 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog }); }; + const [requireIdentity, setRequireIdentity] = useState(false); + const handleStart = async () => { if (!name.trim()) { setError(t("collaborate.nameRequired")); @@ -122,12 +143,15 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog setBusy(true); setError(null); try { - await api.start(name.trim(), color, mode); + await api.start(name.trim(), color, mode, requireIdentity); } catch (err) { // Show a localized message; keep the raw error in the console for - // diagnostics (collab-client throws human-readable English strings). + // diagnostics. When the relay sends a specific rejection reason + // (identity-required, forbidden, etc.) it arrives as err.message; + // fall back to the generic string for unexpected failures. console.error("[GeoLibre] Collaboration error", err); - setError(t("collaborate.connectFailed")); + const message = err instanceof Error && err.message ? err.message : undefined; + setError(message || t("collaborate.connectFailed")); } finally { setBusy(false); } @@ -147,12 +171,9 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog try { await api.join(code.trim(), name.trim(), color); } catch (err) { - // Show a localized message; keep the raw error in the console for - // diagnostics (collab-client throws human-readable English strings). console.error("[GeoLibre] Collaboration error", err); - setError(t("collaborate.connectFailed")); - // The invite link could not connect (e.g. an expired or invalid code), so - // reveal the full layout and let the user fix the code or host instead. + const message = err instanceof Error && err.message ? err.message : undefined; + setError(message || t("collaborate.connectFailed")); setInvited(false); } finally { setBusy(false); @@ -184,13 +205,13 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog onDismiss={() => onOpenChange(false)} onSetMode={api.setMode} onSetParticipantMode={api.setParticipantMode} + onKickParticipant={api.kickParticipant} + onBlockParticipant={api.blockParticipant} + onSetSessionConfig={api.setSessionConfig} onSetFollowHost={api.setFollowHost} /> ) : (
- {/* Name and color feed both actions below, so group them in a - shaded panel above the cards to read as shared profile inputs - rather than belonging to either Start or Join (#706). */}
@@ -206,8 +227,6 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog
- {/* Full panel width keeps every swatch on one row instead of - wrapping a lone dot to a second line (#706). */}
{COLOR_PALETTE.map((c) => (
- {/* An invited participant (arrived via a `?collab=` link) only needs - to join, so collapse the layout to a single Join action and hide - the "Start a session" controls that are irrelevant to them - (#753). They can still fall back to hosting via the link below. */} {invited ? (
@@ -269,9 +281,6 @@ export function CollaborateDialog({ open, onOpenChange, api }: CollaborateDialog
+ {identitySupported && ( + + )}
- {/* Cameras are independent by default; a non-host can opt to follow the - host's viewport (presenter mode). */} {!isHost && (