stores.js raw
1 import { writable, derived } from 'svelte/store';
2
3 // ==================== Relay Connection State ====================
4
5 // Configured relay URL (empty = use same origin / embedded mode)
6 export const relayUrl = writable(localStorage.getItem("relayUrl") || "");
7 export const isStandaloneMode = writable(false);
8 export const relayInfo = writable(null); // NIP-11 relay info
9 export const relayConnectionStatus = writable("disconnected"); // disconnected, connecting, connected, error
10 export const isOrlyRelay = writable(true); // true if connected to ORLY relay with API endpoints
11
12 // Saved relays list - each entry: { url: string, name: string, lastConnected?: number }
13 const storedRelays = localStorage.getItem("savedRelays");
14 export const savedRelays = writable(storedRelays ? JSON.parse(storedRelays) : []);
15
16 // Persist relay URL to localStorage
17 relayUrl.subscribe(url => {
18 if (url) {
19 localStorage.setItem("relayUrl", url);
20 } else {
21 localStorage.removeItem("relayUrl");
22 }
23 });
24
25 // Persist saved relays to localStorage
26 savedRelays.subscribe(relays => {
27 localStorage.setItem("savedRelays", JSON.stringify(relays));
28 });
29
30 // ==================== User/Auth State ====================
31
32 export const isLoggedIn = writable(false);
33 export const userPubkey = writable("");
34 export const userProfile = writable(null);
35 export const userRole = writable("");
36 export const userSigner = writable(null);
37 export const authMethod = writable("");
38
39 // View-as role for permission testing
40 export const viewAsRole = writable("");
41
42 // Derived: effective role (actual or view-as)
43 export const currentEffectiveRole = derived(
44 [userRole, viewAsRole],
45 ([$userRole, $viewAsRole]) => $viewAsRole || $userRole
46 );
47
48 // ==================== UI State ====================
49
50 export const isDarkTheme = writable(false);
51 export const showLoginModal = writable(false);
52 export const showSettingsDrawer = writable(false);
53 export const selectedTab = writable(localStorage.getItem("selectedTab") || "export");
54 export const showFilterBuilder = writable(false);
55
56 // ==================== Navigation State (v0.59) ====================
57
58 // Active top-level view: feed, chat, library, admin, or specific admin sub-view
59 export const activeView = writable(localStorage.getItem("activeView") || "feed");
60 activeView.subscribe(v => { if (v) localStorage.setItem("activeView", v); });
61
62 // Accordion expanded section (null = none expanded)
63 export const expandedSection = writable(localStorage.getItem("expandedSection") || null);
64 expandedSection.subscribe(v => {
65 if (v) localStorage.setItem("expandedSection", v);
66 else localStorage.removeItem("expandedSection");
67 });
68
69 // Search overlay state
70 export const searchActive = writable(false);
71
72 // Notification dropdown state
73 export const notificationDropdownOpen = writable(false);
74
75 // User menu dropdown state
76 export const userMenuOpen = writable(false);
77
78 // ==================== ACL State ====================
79
80 export const aclMode = writable("");
81 export const isPolicyAdmin = writable(false);
82 export const policyEnabled = writable(false);
83
84 // ==================== Events Cache ====================
85
86 export const globalEventsCache = writable([]);
87 export const globalCacheTimestamp = writable(0);
88
89 // ==================== Search State ====================
90
91 export const searchQuery = writable("");
92 export const searchTabs = writable([]);
93 export const searchResults = writable(new Map());
94
95 // ==================== Helper Functions ====================
96
97 /**
98 * Reset all auth-related stores on logout
99 */
100 export function resetAuthState() {
101 isLoggedIn.set(false);
102 userPubkey.set("");
103 userProfile.set(null);
104 userRole.set("");
105 userSigner.set(null);
106 authMethod.set("");
107 viewAsRole.set("");
108 isPolicyAdmin.set(false);
109 }
110
111 /**
112 * Clear the events cache
113 */
114 export function clearEventsCache() {
115 globalEventsCache.set([]);
116 globalCacheTimestamp.set(0);
117 }
118
119 /**
120 * Update the events cache
121 * @param {Array} events - Events to cache
122 */
123 export function updateEventsCache(events) {
124 globalEventsCache.set(events);
125 globalCacheTimestamp.set(Date.now());
126 }
127
128 /**
129 * Check if cache is still valid
130 * @param {number} cacheDuration - Cache duration in ms
131 * @returns {boolean}
132 */
133 export function isCacheValid(cacheDuration = 5 * 60 * 1000) {
134 let timestamp;
135 globalCacheTimestamp.subscribe(v => timestamp = v)();
136 return Date.now() - timestamp < cacheDuration;
137 }
138
139 /**
140 * Clear relay connection and reset to embedded mode
141 */
142 export function clearRelayConnection() {
143 relayUrl.set("");
144 relayInfo.set(null);
145 relayConnectionStatus.set("disconnected");
146 // Also clear auth state since we're changing relays
147 resetAuthState();
148 clearEventsCache();
149 }
150
151 /**
152 * Add or update a relay in the saved relays list
153 * @param {string} url - Relay URL
154 * @param {string} name - Relay name (from NIP-11 or user input)
155 */
156 export function saveRelay(url, name) {
157 savedRelays.update(relays => {
158 const existing = relays.findIndex(r => r.url === url);
159 const entry = { url, name, lastConnected: Date.now() };
160 if (existing >= 0) {
161 relays[existing] = entry;
162 } else {
163 relays.unshift(entry);
164 }
165 return relays;
166 });
167 }
168
169 /**
170 * Remove a relay from the saved relays list
171 * @param {string} url - Relay URL to remove
172 */
173 export function removeRelay(url) {
174 savedRelays.update(relays => relays.filter(r => r.url !== url));
175 }
176
177 /**
178 * Update the last connected timestamp for a relay
179 * @param {string} url - Relay URL
180 */
181 export function touchRelay(url) {
182 savedRelays.update(relays => {
183 const relay = relays.find(r => r.url === url);
184 if (relay) {
185 relay.lastConnected = Date.now();
186 }
187 return relays;
188 });
189 }
190
191 // ==================== Bunker Service State ====================
192
193 export const bunkerServiceActive = writable(false);
194 export const bunkerConnectedClients = writable([]);
195
196 // Bunker worker instance (persists across component mounts)
197 let bunkerWorker = null;
198
199 /**
200 * Get or create the bunker worker
201 */
202 function getBunkerWorker() {
203 if (!bunkerWorker) {
204 bunkerWorker = new Worker(new URL('./bunker-worker.js', import.meta.url), { type: 'module' });
205 bunkerWorker.onmessage = (event) => {
206 const { type, ...data } = event.data;
207 switch (type) {
208 case 'status':
209 bunkerServiceActive.set(data.status === 'connected');
210 break;
211 case 'clients':
212 bunkerConnectedClients.set(data.clients || []);
213 break;
214 case 'error':
215 console.error('[BunkerStore] Worker error:', data.error);
216 break;
217 case 'request':
218 console.log('[BunkerStore] Request:', data.method, 'from:', data.from);
219 break;
220 }
221 };
222 }
223 return bunkerWorker;
224 }
225
226 /**
227 * Configure the bunker worker
228 */
229 export function configureBunkerWorker(config) {
230 const worker = getBunkerWorker();
231 worker.postMessage({ type: 'configure', ...config });
232 }
233
234 /**
235 * Connect the bunker worker
236 */
237 export function connectBunkerWorker() {
238 const worker = getBunkerWorker();
239 worker.postMessage({ type: 'connect' });
240 }
241
242 /**
243 * Disconnect the bunker worker
244 */
245 export function disconnectBunkerWorker() {
246 const worker = getBunkerWorker();
247 worker.postMessage({ type: 'disconnect' });
248 }
249
250 /**
251 * Add a secret to the bunker worker
252 */
253 export function addBunkerSecret(secret) {
254 const worker = getBunkerWorker();
255 worker.postMessage({ type: 'addSecret', secret });
256 }
257
258 /**
259 * Request current bunker status
260 */
261 export function requestBunkerStatus() {
262 const worker = getBunkerWorker();
263 worker.postMessage({ type: 'getStatus' });
264 }
265
266 /**
267 * Reset bunker state
268 */
269 export function resetBunkerState() {
270 disconnectBunkerWorker();
271 bunkerServiceActive.set(false);
272 bunkerConnectedClients.set([]);
273 }
274