123456789import { 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}3839export const SyncModeSelector: Component<SyncModeSelectorProps> = (props) => {40 const [mode, setMode] = createSignal<SyncMode>("public");41 const [relayUrl, setRelayUrl] = createSignal("");42 const [draftUrl, setDraftUrl] = createSignal("");43 const [saving, setSaving] = createSignal(false);44 const [status, setStatus] = createSignal<string | null>(null);45 const [expanded, setExpanded] = createSignal(false);4647 48 const needsRelayUrl = () => mode() === "private" && relayUrl() === DEFAULT_RELAY_URL;4950 function apply(next: SyncSettingsSnapshot) {51 setMode(next.mode);52 setRelayUrl(next.relayUrl);53 setDraftUrl(next.relayUrl === DEFAULT_RELAY_URL ? "" : next.relayUrl);54 }5556 onMount(async () => {57 const result = (await sendMessage({ type: "GET_SYNC_SETTINGS" })) as {58 ok: boolean;59 data?: SyncSettingsSnapshot;60 };61 if (result.ok && result.data) apply(result.data);62 });6364 65 async function save(next?: SyncMode) {66 setSaving(true);67 setStatus(null);68 const result = (await sendMessage({69 type: "SET_SYNC_SETTINGS",70 payload: next ? { mode: next } : { relayUrl: draftUrl() },71 })) as { ok: boolean; data?: SyncSettingsSnapshot };72 if (result.ok && result.data) {73 apply(result.data);74 setStatus(next ? null : "Saved");75 props.onChange?.(result.data);76 } else {77 setStatus("Use a valid wss:// relay URL");78 }79 setSaving(false);80 }8182 const option = (value: SyncMode, title: string, sub: string) => (83 <button84 class="peach-sync-mode"85 classList={{ "is-active": mode() === value }}86 role="radio"87 aria-checked={mode() === value}88 disabled={saving()}89 onClick={() => void save(value)}90 >91 <span class="peach-sync-mode-main">92 <span class="peach-sync-mode-title">{title}</span>93 <span class="peach-sync-mode-sub">{sub}</span>94 </span>95 <span class="peach-sync-mode-pip" aria-hidden="true" />96 </button>97 );9899 return (100 <Show101 when={!props.compact || expanded()}102 fallback={103 <p class="peach-sync-mode-summary">104 Using {modeLabel(mode())}105 {needsRelayUrl() ? " — no URL set" : ""}106 {" · "}107 <button class="peach-sync-mode-change" onClick={() => setExpanded(true)}>change</button>108 </p>109 }110 >111 <div class="peach-sync-mode-group" role="radiogroup" aria-label="How this vault syncs">112 {option(113 "public",114 "Public relay",115 "Peach's own relay. Works anywhere, nothing to set up.",116 )}117 {option(118 "private",119 "Private relay",120 "Your own peach-relay, on your own domain. Same open-source code.",121 )}122123 <Show when={mode() === "private"}>124 <label class="peach-label" for="sync-relay-url">relay URL</label>125 <input126 id="sync-relay-url"127 class="peach-underline-input"128 type="url"129 inputmode="url"130 autocomplete="off"131 spellcheck={false}132 placeholder="wss://relay.example.com"133 value={draftUrl()}134 disabled={saving()}135 onInput={(event) => setDraftUrl((event.target as HTMLInputElement).value)}136 onKeyDown={(event) => {137 if (event.key === "Enter") void save();138 }}139 />140 <div class="peach-settings-row peach-settings-row--meta">141 <span142 class="peach-settings-value"143 classList={{ "peach-sync-mode-warn": needsRelayUrl() || status()?.startsWith("Use") }}144 >145 {status() ??146 (needsRelayUrl()147 ? "Sync is paused until you enter your relay URL."148 : "Only secure wss:// relays are accepted.")}149 </span>150 <button151 class="peach-btn peach-btn--subtle peach-btn--sm"152 disabled={saving()}153 onClick={() => void save()}154 >{saving() ? "saving…" : "save"}</button>155 </div>156 </Show>157158 {option(159 "off",160 "No sync",161 "This device keeps to itself. Move your vault by hand with Codex or CSV export and import.",162 )}163164 <p class="peach-sync-mode-disclosure">{RELAY_DISCLOSURE}</p>165166 <Show when={props.compact}>167 <button class="peach-btn peach-btn--ghost peach-btn--sm" onClick={() => setExpanded(false)}>168 Done169 </button>170 </Show>171 </div>172 </Show>173 );174};