// 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.",
)}