123456789import { type Component, Show, createSignal, onMount } from "solid-js";10import { sendMessage } from "../../shared/bridge";11import { DEFAULT_RELAY_URL } from "../../shared/rendezvous";1213export type SyncMode = "public" | "private" | "off";1415export interface SyncSettingsSnapshot {16 mode: SyncMode;17 relayUrl: string;18}19202122232425export const RELAY_DISCLOSURE =26 "End-to-end encrypted, through a relay that never stores or accesses any vault or user data.";2728export function modeLabel(mode: SyncMode): string {29 return mode === "public" ? "Public relay" : mode === "private" ? "Private relay" : "No sync";30}3132export interface SyncModeSelectorProps {33 34 compact?: boolean;35 36 onChange?(settings: SyncSettingsSnapshot): void;37 3839 onExpand?(expanded: boolean): void;40}4142export const SyncModeSelector: Component<SyncModeSelectorProps> = (props) => {43 const [mode, setMode] = createSignal<SyncMode>("public");44 const [relayUrl, setRelayUrl] = createSignal("");45 const [draftUrl, setDraftUrl] = createSignal("wss://");46 const [saving, setSaving] = createSignal(false);47 const [status, setStatus] = createSignal<string | null>(null);48 const [expanded, setExpandedSignal] = createSignal(false);4950 function setExpanded(next: boolean) {51 setExpandedSignal(next);52 props.onExpand?.(next);53 }5455 56 const needsRelayUrl = () => mode() === "private" && relayUrl() === DEFAULT_RELAY_URL;5758 function apply(next: SyncSettingsSnapshot) {59 setMode(next.mode);60 setRelayUrl(next.relayUrl);61 setDraftUrl(next.relayUrl === DEFAULT_RELAY_URL ? "wss://" : next.relayUrl);62 }6364 onMount(async () => {65 const result = (await sendMessage({ type: "GET_SYNC_SETTINGS" })) as {66 ok: boolean;67 data?: SyncSettingsSnapshot;68 };69 if (result.ok && result.data) apply(result.data);70 });7172 73 async function save(next?: SyncMode) {74 setSaving(true);75 setStatus(null);76 const result = (await sendMessage({77 type: "SET_SYNC_SETTINGS",78 payload: next ? { mode: next } : { relayUrl: draftUrl() },79 })) as { ok: boolean; data?: SyncSettingsSnapshot };80 if (result.ok && result.data) {81 apply(result.data);82 setStatus(next ? null : "Saved");83 props.onChange?.(result.data);84 } else {85 setStatus("Use a valid wss:// relay URL");86 }87 setSaving(false);88 }8990 const option = (value: SyncMode, title: string, sub: string) => (91 <button92 class="peach-sync-mode"93 classList={{ "is-active": mode() === value }}94 role="radio"95 aria-checked={mode() === value}96 disabled={saving()}97 onClick={() => void save(value)}98 >99 <span class="peach-sync-mode-main">100 <span class="peach-sync-mode-title">{title}</span>101 <span class="peach-sync-mode-sub">{sub}</span>102 </span>103 <span class="peach-sync-mode-pip" aria-hidden="true" />104 </button>105 );106107 return (108 <Show109 when={!props.compact || expanded()}110 fallback={111 <p class="peach-sync-mode-summary">112 Using {modeLabel(mode())}113 {needsRelayUrl() ? " — no URL set" : ""}114 {" · "}115 <button class="peach-sync-mode-change" onClick={() => setExpanded(true)}>change</button>116 </p>117 }118 >119 <div class="peach-sync-mode-group" role="radiogroup" aria-label="How this vault syncs">120 {option(121 "public",122 "Public relay",123 "Peach's own relay. Works anywhere, nothing to set up.",124 )}125 {option(126 "private",127 "Private relay",128 "Your own peach-relay, on your own domain. Same open-source code.",129 )}130131 <Show when={mode() === "private"}>132 <label class="peach-label" for="sync-relay-url">relay URL</label>133 <input134 id="sync-relay-url"135 class="peach-underline-input"136 type="url"137 inputmode="url"138 autocomplete="off"139 spellcheck={false}140 placeholder="wss://relay.example.com"141 value={draftUrl()}142 disabled={saving()}143 onInput={(event) => setDraftUrl((event.target as HTMLInputElement).value)}144 onKeyDown={(event) => {145 if (event.key === "Enter") void save();146 }}147 />148 <div class="peach-settings-row peach-settings-row--meta">149 <span150 class="peach-settings-value"151 classList={{ "peach-sync-mode-warn": needsRelayUrl() || status()?.startsWith("Use") }}152 >153 {status() ??154 (needsRelayUrl()155 ? "Sync is paused until you enter your relay URL."156 : "Only secure wss:// relays are accepted.")}157 </span>158 <button159 class="peach-btn peach-btn--subtle peach-btn--sm"160 disabled={saving()}161 onClick={() => void save()}162 >{saving() ? "saving…" : "save"}</button>163 </div>164 </Show>165166 {option(167 "off",168 "No sync",169 "This device keeps to itself. Move your vault by hand with Codex or CSV export and import.",170 )}171172 <p class="peach-sync-mode-disclosure">{RELAY_DISCLOSURE}</p>173174 <Show when={props.compact}>175 <button class="peach-btn peach-btn--ghost peach-btn--sm" onClick={() => setExpanded(false)}>176 Done177 </button>178 </Show>179 </div>180 </Show>181 );182};