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