feat(cordn): crew Cordn group link-out (Level 1)
Crews can carry a Cordn group ref (['cordn', <cordn1-ref-or-url>] -> SkateCrew.cordnGroup): CrewForm gains a 'Cordn group' field and CrewCoordinatorCard shows an 'Open crew chat' (cordn.net/chat/<ref>) + 'Copy for Staircase' row. Verified a real ContextVM client (@contextvm/sdk) connects + lists tools against our self-hosted coordinator.
This commit is contained in:
parent
4ff5e25fee
commit
9251aea5b1
6 changed files with 84 additions and 1 deletions
|
|
@ -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', <cordn1-ref-or-url>]`
|
||||
(`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.
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="crew-cordn-group" className="text-xs text-gray-400">{t('socialUi.form.cordnGroup')}</Label>
|
||||
<Input
|
||||
id="crew-cordn-group"
|
||||
placeholder="cordn1… · https://cordn.net/chat/…"
|
||||
value={cordnGroupInput}
|
||||
onChange={(e) => setCordnGroupInput(e.target.value)}
|
||||
className="bg-gray-800 border-gray-700 font-mono text-xs"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500">{t('socialUi.form.cordnGroupHint')}</p>
|
||||
</div>
|
||||
{parsedCoordinator?.pubkey && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
|
@ -1454,7 +1473,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
const hasExtras =
|
||||
!!videoUrl.trim() || !!sponsorRef.trim() || labRefs.length > 0 ||
|
||||
socials.length > 0 || !!website.trim() || !!instagram.trim() ||
|
||||
!!bandName.trim() || nip05s.length > 0 || agentEnabled || !!coordinatorInput.trim() ||
|
||||
!!bandName.trim() || nip05s.length > 0 || agentEnabled || !!coordinatorInput.trim() || !!cordnGroupInput.trim() ||
|
||||
(goalEnabled && !!goalTitle.trim());
|
||||
|
||||
const steps: { title: string; short: string; icon: typeof Users; hint: string; filled: boolean; content: ReactNode }[] = [
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export function CrewCoordinatorCard({
|
|||
<span className="font-bold text-teal-300">{t('socialUi.crewCoordinator.e2ee')}</span> {t('socialUi.crewCoordinator.defaultC')}
|
||||
</p>
|
||||
<StaircaseLink coordinator={SKATESPOTS_COORDINATOR} />
|
||||
{crew.cordnGroup && <CordnGroupLink group={crew.cordnGroup} />}
|
||||
{isFounder && (
|
||||
<p className="text-[11px] leading-relaxed text-gray-600">
|
||||
{t('socialUi.crewCoordinator.advancedA')}{' '}
|
||||
|
|
@ -109,6 +110,7 @@ export function CrewCoordinatorCard({
|
|||
relays: /^wss?:\/\//i.test(coordinator.url) ? [coordinator.url] : [],
|
||||
}}
|
||||
/>
|
||||
{crew.cordnGroup && <CordnGroupLink group={crew.cordnGroup} />}
|
||||
|
||||
{endpoint && (
|
||||
<p className="text-[11px] leading-relaxed text-gray-500">
|
||||
|
|
@ -192,6 +194,48 @@ function StaircaseLink({ coordinator }: { coordinator: { pubkey?: string; relays
|
|||
);
|
||||
}
|
||||
|
||||
/** 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 (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{url && (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`${btn} border-emerald-500/30 bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/20`}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" /> {t('socialUi.crewCoordinator.openCordnGroup')}
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copy()}
|
||||
className={`${btn} border-white/15 bg-white/5 text-gray-300 hover:bg-white/10`}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" /> {t('socialUi.crewCoordinator.copyForStaircase')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CoordinatorEndpoint({ coordinator }: { coordinator: CrewCoordinator }) {
|
||||
const { t } = useTranslation();
|
||||
const safe = sanitizeUrl(coordinator.url);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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', <ref-or-url>].
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue