From 9251aea5b1c74d288b69ac04fdaee55f199eb205 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 12 Oct 2026 21:46:03 +0700 Subject: [PATCH] feat(cordn): crew Cordn group link-out (Level 1) Crews can carry a Cordn group ref (['cordn', ] -> SkateCrew.cordnGroup): CrewForm gains a 'Cordn group' field and CrewCoordinatorCard shows an 'Open crew chat' (cordn.net/chat/) + 'Copy for Staircase' row. Verified a real ContextVM client (@contextvm/sdk) connects + lists tools against our self-hosted coordinator. --- AGENTS.md | 9 +++++ src/components/CrewForm.tsx | 21 +++++++++- src/components/crew/CrewCoordinatorCard.tsx | 44 +++++++++++++++++++++ src/i18n/en.ts | 3 ++ src/i18n/es.ts | 3 ++ src/lib/types.ts | 5 +++ 6 files changed, 84 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 419c1c5..3370f64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2154,6 +2154,15 @@ The community relay moved off the hosted relay.tools instance pubkey. Also fixed a status-badge bug: `STATUS_META` labels in `CoordinatorSettingsCard` were missing the `socialUi.` prefix and rendered raw keys (e.g. `coordinator.statusStale`). + **Verified (2026-10-12):** the reference `@contextvm/sdk` client (encrypted + ContextVM transport) connects to our coordinator and lists its tools (`kp_*`, + `welcome_*`, `join_request_*`, `msg_*`) — a real Cordn client works. The + **cordn.net web app does not** complete the handshake (connects + NIP-42-auths, + then sends no request; upstream issue drafted). **Crew Cordn group link + (Level 1, 2026-10-12):** a crew may carry `['cordn', ]` + (`SkateCrew.cordnGroup`); `CrewForm` has a "Cordn group" field and + `CrewCoordinatorCard` shows an "Open crew chat" + "Copy for Staircase" row. + In-app Cordn/MLS (Level 2) remains the big client build. - **Cutover (2026-10-12):** `SKATE_RELAY` (`src/lib/types.ts`) → the new relay; the default pools, the DM/CVM/zap relay arrays, the `cvm-server` + `push-service` defaults, and the archive default all point at the new relay. diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx index dd03ee5..b6d5992 100644 --- a/src/components/CrewForm.tsx +++ b/src/components/CrewForm.tsx @@ -338,6 +338,7 @@ export function CrewForm({ existing }: CrewFormProps) { // The coordinator the user set in Settings → Coordinator, for a one-tap prefill. const { coordinator: myCoordinator } = useCoordinator(); // Live parse so the founder gets feedback (and the publish path reuses it). + const [cordnGroupInput, setCordnGroupInput] = useState(existing?.cordnGroup ?? ''); const parsedCoordinator = parseCrewCoordinatorInput(coordinatorInput); // On-demand health probe (the "Check" button) for a coordinator addressed by // pubkey — mirrors the Settings → Coordinator card. @@ -615,6 +616,11 @@ export function CrewForm({ existing }: CrewFormProps) { tags.push(coordinatorTag); } + // Cordn group link for the crew's MLS chat (created in Staircase / Cordn web). + if (cordnGroupInput.trim()) { + tags.push(['cordn', cordnGroupInput.trim()]); + } + // Crew video / montage — only stored if it's a valid YouTube/Instagram URL. const vid = videoUrl.trim(); if (vid && isCrewVideoUrl(vid)) { @@ -1225,6 +1231,19 @@ export function CrewForm({ existing }: CrewFormProps) { {t('socialUi.coordinator.useMine')} )} + +
+ + setCordnGroupInput(e.target.value)} + className="bg-gray-800 border-gray-700 font-mono text-xs" + spellCheck={false} + /> +

{t('socialUi.form.cordnGroupHint')}

+
{parsedCoordinator?.pubkey && (
+
+ ); +} + function CoordinatorEndpoint({ coordinator }: { coordinator: CrewCoordinator }) { const { t } = useTranslation(); const safe = sanitizeUrl(coordinator.url); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index a287c2a..860c04f 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -4046,6 +4046,7 @@ socialUi: { manage: 'Manage coordinator', open: 'Open {{domain}}', openInCordnWeb: 'Cordn (web)', + openCordnGroup: 'Open crew chat', copyForStaircase: 'Copy for Staircase', runBy: 'Coordinator run by', }, @@ -4124,6 +4125,8 @@ socialUi: { crewCreated: 'Crew created!', crewUpdated: 'Crew updated!', crewName: 'Crew Name', + cordnGroup: 'Cordn group', + cordnGroupHint: 'Optional: paste the group link from Staircase or Cordn web. Your crew then gets real end-to-end MLS chat, hosted on our coordinator.', crewOnlyWord: 'Crew-only', crewType: 'Crew Type', crewTypeDescA: 'Is your crew focused on a craft? Pick one to also show up in that scene in', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 52fc53d..a286476 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -4046,6 +4046,7 @@ socialUi: { manage: 'Gestionar coordinador', open: 'Abrir {{domain}}', openInCordnWeb: 'Cordn (web)', + openCordnGroup: 'Abrir chat de la peña', copyForStaircase: 'Copiar para Staircase', runBy: 'Coordinador gestionado por', }, @@ -4124,6 +4125,8 @@ socialUi: { crewCreated: '¡Peña creada!', crewUpdated: '¡Peña actualizada!', crewName: 'Nombre de la peña', + cordnGroup: 'Grupo de Cordn', + cordnGroupHint: 'Opcional: pega el enlace del grupo desde Staircase o Cordn web. Así tu peña tiene chat MLS cifrado de extremo a extremo, alojado en nuestro coordinador.', crewOnlyWord: 'Solo peña', crewType: 'Tipo de peña', crewTypeDescA: '¿Tu peña se centra en un oficio? Elige uno para aparecer también en esa escena en', diff --git a/src/lib/types.ts b/src/lib/types.ts index b15055c..d8470f5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2247,6 +2247,7 @@ export interface SkateCrew { lookingFor: string[]; // Lab craft slugs the crew is hiring for (e.g. "filmer", "editor") sponsorRef?: string; // Sponsoring skate shop coordinate (30017:pubkey:d) via `sponsor` tag coordinator?: CrewCoordinator; // Cordn/MLS encrypted-chat coordinator for this crew + cordnGroup?: string; // Cordn group ref/link for the crew's MLS chat (from Staircase/Cordn web) (`cordn` tag) videoUrl?: string; // Crew's headline video / montage — YouTube or Instagram URL (`video` tag) crewRoles: CrewMemberRole[]; // Optional skate roles (filmer/photographer/…) assigned to members createdAt: number; @@ -3266,6 +3267,9 @@ export function parseSkateCrew(event: NostrEvent): SkateCrew | null { } : undefined; // Crew video / montage — only kept if it parses as a YouTube/Instagram URL. + // Cordn group link for the crew's MLS chat (optional): ['cordn', ]. + const cordnGroup = event.tags.find(([name, value]) => name === 'cordn' && !!value?.trim())?.[1]?.trim() || undefined; + const videoRaw = event.tags.find(([name]) => name === 'video')?.[1]?.trim(); const videoUrl = videoRaw && parseCrewVideo(videoRaw) ? videoRaw : undefined; // Skate roles (filmer/photographer/…) — only for people actually on the roster. @@ -3302,6 +3306,7 @@ export function parseSkateCrew(event: NostrEvent): SkateCrew | null { lookingFor, sponsorRef, coordinator, + cordnGroup, videoUrl, crewRoles, createdAt: event.created_at,