import { useState } from 'react'; import { Link } from 'react-router-dom'; import { Lock, ShieldCheck, Server, ExternalLink, Settings, Copy, MessagesSquare, Users, QrCode } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { QRCodeCanvas } from '@/components/ui/qrcode'; import { useAuthor } from '@/hooks/useAuthor'; import { useTranslation } from '@/contexts/language'; import { useToast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl'; import { resolveCordnEndpoint } from '@/lib/cordn/endpoint'; import { CORDN_RELAY_URL, coordinatorAddUrl, CORDN_HOME, SKATESPOTS_COORDINATOR } from '@/lib/coordinator'; import { cn } from '@/lib/utils'; import type { CrewCoordinator, SkateCrew } from '@/lib/types'; import { nip19 } from 'nostr-tools'; /** * The crew's encrypted-chat card (sidebar). * * A crew has **two** chats and the card must not conflate them: * - the **Chat** tab — built-in NIP-17 group chat, always on, no setup; and * - the **Private chat** tab — real MLS (forward secrecy, member removal), * created once by an admin and routed through a coordinator. * * It leads with an honest status for each and a one-tap entry into the private * chat; the coordinator details and any external clients (Staircase / Cordn web) * are secondary. ⚠️ The status must match the tab: an earlier version badged * "Private chat · On" while the MLS tab said "not created yet". */ export function CrewCoordinatorCard({ crew, isFounder, canManage, nativeGroupExists, }: { crew: SkateCrew; isFounder: boolean; /** Founder/admin — may create the MLS group. */ canManage: boolean; /** Whether this device already holds the crew's MLS group (local check). */ nativeGroupExists: boolean | undefined; }) { const { t } = useTranslation(); const { coordinator } = crew; // Which relay the messages actually travel over — a `wss://` coordinator URL // IS the relay; an `https://` one is addressed by pubkey over the shared // ContextVM relay (public). The copy below must not overclaim either way. const endpoint = resolveCordnEndpoint(crew); const overPublicRelay = endpoint?.relayUrl === CORDN_RELAY_URL; const privateChatTab = `/crew/${crew.pubkey}/${crew.identifier}?tab=mls`; const mlsLabel = nativeGroupExists === true ? t('socialUi.crewCoordinator.on') : nativeGroupExists === undefined ? t('socialUi.crewCoordinator.checking') : canManage ? t('socialUi.crewCoordinator.setUpChat') : t('socialUi.crewCoordinator.joinChat'); const mlsCta = nativeGroupExists === true ? t('socialUi.crewCoordinator.openChat') : canManage ? t('socialUi.crewCoordinator.setUpChat') : t('socialUi.crewCoordinator.joinChat'); return ( {t('socialUi.crewCoordinator.encryptedChat')} {/* Chat (NIP-17) — always on */}

{t('socialUi.crewCoordinator.chatWord')} {t('socialUi.crewCoordinator.on')}

{t('socialUi.crewCoordinator.chatRowDesc')}

{/* Private chat (MLS) — created once, then joined */}

{t('socialUi.crewCoordinator.privateChatName')} {mlsLabel}

{t('socialUi.crewCoordinator.privateChatDesc')}

{mlsCta}
{/* Coordinator details (only when one is configured) */} {coordinator && (
{coordinator.operator && } {endpoint && (

{t('socialUi.crewCoordinator.deliveredOver')}{' '} {urlDomain(endpoint.relayUrl) || endpoint.relayUrl} {overPublicRelay ? ` ${t('socialUi.crewCoordinator.sharedRelay')}` : ` ${t('socialUi.crewCoordinator.ownRelay')}`}

)}

{t('socialUi.crewCoordinator.chatLive')}

{t('socialUi.crewCoordinator.chatRowDesc')}{' '} {overPublicRelay ? t('socialUi.crewCoordinator.publicWarning') : t('socialUi.crewCoordinator.ownRelaySafe')}

{isFounder && ( {t('socialUi.crewCoordinator.manage')} )}
)} {/* External clients — secondary, clearly labelled */} {(coordinator || crew.cordnGroup) && (

{t('socialUi.crewCoordinator.openInAnotherApp')}

{crew.cordnGroup && }
)} {/* Advanced (founders, no coordinator configured) */} {!coordinator && isFounder && (

{t('socialUi.crewCoordinator.advancedA')}{' '} {t('socialUi.crewCoordinator.cordnCoordinator')} ?{' '} {t('socialUi.crewCoordinator.routeThrough')}

)}

{t('socialUi.crewCoordinator.privateChatDescFull')}

); } /** Real-MLS entry points for a coordinator: open the Cordn **web** client * (cordn.net add-link), or copy the nprofile to add it in the **Staircase** * native app. Falls back to the Cordn home when there's no pubkey. */ function StaircaseLink({ coordinator }: { coordinator: { pubkey?: string; relays?: string[] } }) { const { t } = useTranslation(); const { toast } = useToast(); const [showQr, setShowQr] = useState(false); const nprofile = coordinator.pubkey ? nip19.nprofileEncode({ pubkey: coordinator.pubkey, relays: coordinator.relays ?? [] }) : null; const webUrl = coordinator.pubkey ? coordinatorAddUrl({ pubkey: coordinator.pubkey, relays: coordinator.relays ?? [] }) : CORDN_HOME; const copyNprofile = async () => { if (!nprofile) return; try { await navigator.clipboard.writeText(nprofile); toast({ title: t('common.copied') }); } catch { toast({ title: t('common.tryAgain'), variant: 'destructive' }); } }; const btn = 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-bold transition-colors'; return ( <> {t('socialUi.crewCoordinator.openInCordnWeb')} {nprofile && ( <> )} {showQr && nprofile && (

{t('socialUi.coordinator.scanToJoin')}

)} ); } /** Link a crew to its Cordn group (created in Staircase / Cordn web) for real MLS chat. */ function CordnGroupLink({ group }: { group: string }) { const { t } = useTranslation(); const { toast } = useToast(); const url = /^cordn1/i.test(group) ? `https://cordn.net/chat/${group}` : /^https?:\/\//i.test(group) ? group : null; const copy = async () => { try { await navigator.clipboard.writeText(group); toast({ title: t('common.copied') }); } catch { toast({ title: t('common.tryAgain'), variant: 'destructive' }); } }; const btn = 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-bold transition-colors'; return ( <> {url && ( {t('socialUi.crewCoordinator.openCordnGroup')} )} ); } function CoordinatorEndpoint({ coordinator }: { coordinator: CrewCoordinator }) { const { t } = useTranslation(); const safe = sanitizeUrl(coordinator.url); return (

{t('socialUi.crewCoordinator.coordinator')}

{urlDomain(coordinator.url)}

{safe && ( )}
); } function CoordinatorOperator({ pubkey }: { pubkey: string }) { const { t } = useTranslation(); const author = useAuthor(pubkey); const name = author.data?.metadata?.name ?? genUserName(pubkey); const pic = author.data?.metadata?.picture; const npub = nip19.npubEncode(pubkey); return ( {name.slice(0, 2).toUpperCase()}

{t('socialUi.crewCoordinator.runBy')}

{name}

); }