// SyncModeSelector — the one place the relay choice is made. // // Settings → Sync renders it expanded and is the source of truth; the pairing // surfaces render it `compact`, as a quiet "Using Public relay · change" line // that expands in place. Both read and write the same GET/SET_SYNC_SETTINGS // value, so the two surfaces cannot drift apart // (docs/sync-relay-surfacing.md). import { type Component, Show, createSignal, onMount } from "solid-js"; import { sendMessage } from "../../shared/bridge"; import { DEFAULT_RELAY_URL } from "../../shared/rendezvous"; export type SyncMode = "public" | "private" | "off"; export interface SyncSettingsSnapshot { mode: SyncMode; relayUrl: string; } /** * True of the same phrase everywhere: the relay is a dumb pipe under both * relay modes, and its absence is what makes "No sync" a visibly smaller * footprint rather than a different kind of trust. */ export const RELAY_DISCLOSURE = "End-to-end encrypted, through a relay that never stores or accesses any vault or user data."; export function modeLabel(mode: SyncMode): string { return mode === "public" ? "Public relay" : mode === "private" ? "Private relay" : "No sync"; } export interface SyncModeSelectorProps { /** Render as a one-line summary that expands on "change". */ compact?: boolean; /** Fires on every committed change, so a host mid-pairing can react. */ onChange?(settings: SyncSettingsSnapshot): void; /** Compact only: the expanded selector is taller than the popup, so the host * needs to know when to fold away whatever it was showing. */ onExpand?(expanded: boolean): void; } export const SyncModeSelector: Component = (props) => { const [mode, setMode] = createSignal("public"); const [relayUrl, setRelayUrl] = createSignal(""); const [draftUrl, setDraftUrl] = createSignal("wss://"); const [saving, setSaving] = createSignal(false); const [status, setStatus] = createSignal(null); const [expanded, setExpandedSignal] = createSignal(false); function setExpanded(next: boolean) { setExpandedSignal(next); props.onExpand?.(next); } /** Private chosen, no URL yet — never silently fall back to the public relay. */ const needsRelayUrl = () => mode() === "private" && relayUrl() === DEFAULT_RELAY_URL; function apply(next: SyncSettingsSnapshot) { setMode(next.mode); setRelayUrl(next.relayUrl); setDraftUrl(next.relayUrl === DEFAULT_RELAY_URL ? "wss://" : next.relayUrl); } onMount(async () => { const result = (await sendMessage({ type: "GET_SYNC_SETTINGS" })) as { ok: boolean; data?: SyncSettingsSnapshot; }; if (result.ok && result.data) apply(result.data); }); /** Pass a mode to switch; pass nothing to save the typed private relay URL. */ async function save(next?: SyncMode) { setSaving(true); setStatus(null); const result = (await sendMessage({ type: "SET_SYNC_SETTINGS", payload: next ? { mode: next } : { relayUrl: draftUrl() }, })) as { ok: boolean; data?: SyncSettingsSnapshot }; if (result.ok && result.data) { apply(result.data); setStatus(next ? null : "Saved"); props.onChange?.(result.data); } else { setStatus("Use a valid wss:// relay URL"); } setSaving(false); } const option = (value: SyncMode, title: string, sub: string) => ( ); return ( Using {modeLabel(mode())} {needsRelayUrl() ? " — no URL set" : ""} {" · "}

} >
{option( "public", "Public relay", "Peach's own relay. Works anywhere, nothing to set up.", )} {option( "private", "Private relay", "Your own peach-relay, on your own domain. Same open-source code.", )} setDraftUrl((event.target as HTMLInputElement).value)} onKeyDown={(event) => { if (event.key === "Enter") void save(); }} />
{status() ?? (needsRelayUrl() ? "Sync is paused until you enter your relay URL." : "Only secure wss:// relays are accepted.")}
{option( "off", "No sync", "This device keeps to itself. Move your vault by hand with Codex or CSV export and import.", )}

{RELAY_DISCLOSURE}

); };