experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

more extension transition fixes

+78 -38
+2
app/features.js
··· 22 22 // Extension schemas (for Settings UI only - extensions run in isolated processes) 23 23 import peeks from './peeks/index.js'; 24 24 import slides from './slides/index.js'; 25 + import groups from './groups/index.js'; 25 26 26 27 const fc = {}; 27 28 ··· 32 33 // Extension schemas for Settings UI 33 34 fc[peeks.id] = peeks; 34 35 fc[slides.id] = slides; 36 + fc[groups.id] = groups; 35 37 36 38 export default fc;
+69 -7
app/settings/settings.js
··· 154 154 container.appendChild(prefsSection); 155 155 } 156 156 157 - // Features 158 - if (features && features.length > 0) { 157 + // Features (core only - extensions are managed in Extensions section) 158 + const extensionNames = ['groups', 'peeks', 'slides']; 159 + const coreFeatures = features ? features.filter(f => !extensionNames.includes(f.name.toLowerCase())) : []; 160 + if (coreFeatures.length > 0) { 159 161 const featuresSection = document.createElement('div'); 160 162 featuresSection.className = 'form-section'; 161 163 ··· 164 166 title.textContent = 'Features'; 165 167 featuresSection.appendChild(title); 166 168 167 - features.forEach((feature, i) => { 169 + coreFeatures.forEach((feature) => { 170 + // Find original index for saving 171 + const i = features.findIndex(f => f.id === feature.id); 172 + 168 173 const item = document.createElement('div'); 169 174 item.className = 'feature-item'; 170 175 ··· 341 346 const feature = features.find(f => f.name.toLowerCase() === extId); 342 347 const isEnabled = feature ? feature.enabled : false; 343 348 344 - if (running && running.manifest?.builtin) { 349 + if (running) { 350 + // Extension is running 345 351 allExtensions.push({ 346 352 ...running, 353 + manifest: running.manifest || { 354 + id: extId, 355 + name: extId.charAt(0).toUpperCase() + extId.slice(1), 356 + shortname: extId, 357 + builtin: true 358 + }, 347 359 source: 'builtin', 348 360 isRunning: true, 349 361 enabled: isEnabled 350 362 }); 351 363 } else { 352 - // Extension not running - show it as disabled 364 + // Extension not running - show it as stopped 353 365 allExtensions.push({ 354 366 id: extId, 355 367 manifest: { ··· 626 638 } 627 639 }; 628 640 629 - // Initial load 641 + // Initial load (may be incomplete if extensions still loading) 630 642 await refreshExtensionsList(); 643 + 644 + // Reactively update when extensions finish loading 645 + api.subscribe('ext:all-loaded', () => { 646 + refreshExtensionsList(); 647 + }, api.scopes.GLOBAL); 631 648 632 649 return container; 633 650 }; ··· 640 657 // Use extension shortname for datastore key (e.g., 'peeks', 'slides', 'groups') 641 658 const extId = labels.name.toLowerCase(); 642 659 643 - // For now, still use localStorage as fallback (will be migrated) 660 + // Read from localStorage (legacy source) 644 661 const store = openStore(id, defaults, clear); 645 662 646 663 let prefs = store.get(storageKeys.PREFS); 647 664 let items = store.get(storageKeys.ITEMS); 665 + 666 + // Migrate localStorage to datastore if datastore is empty 667 + // This ensures extensions (which read from datastore) get the user's settings 668 + const migrateToDatastore = async () => { 669 + try { 670 + const rowIdPrefs = `${extId}:prefs`; 671 + const tableResult = await api.datastore.getTable('extension_settings'); 672 + const table = tableResult.success ? (tableResult.data || {}) : {}; 673 + 674 + // Check if this extension already has prefs in datastore 675 + const hasPrefs = Object.values(table).some(row => row.extensionId === extId && row.key === 'prefs'); 676 + 677 + // If datastore doesn't have prefs yet, migrate from localStorage 678 + if (!hasPrefs) { 679 + console.log(`[settings] Migrating ${extId} settings to datastore`); 680 + const now = Date.now(); 681 + 682 + await api.datastore.setRow('extension_settings', rowIdPrefs, { 683 + extensionId: extId, 684 + key: 'prefs', 685 + value: JSON.stringify(prefs), 686 + updatedAt: now 687 + }); 688 + 689 + if (items) { 690 + const rowIdItems = `${extId}:items`; 691 + await api.datastore.setRow('extension_settings', rowIdItems, { 692 + extensionId: extId, 693 + key: 'items', 694 + value: JSON.stringify(items), 695 + updatedAt: now 696 + }); 697 + } 698 + 699 + // Notify extension to reload settings 700 + const settingsChangedTopic = `${extId}:settings-changed`; 701 + api.publish(settingsChangedTopic, {}, api.scopes.GLOBAL); 702 + } 703 + } catch (err) { 704 + console.error(`[settings] Migration error for ${extId}:`, err); 705 + } 706 + }; 707 + 708 + // Run migration async (don't block UI) 709 + migrateToDatastore(); 648 710 649 711 const container = document.createElement('div'); 650 712
+6
index.js
··· 673 673 } 674 674 675 675 console.log(`[ext:win] Loaded ${extensionWindows.size} extensions`); 676 + 677 + // Signal that all extensions are loaded (GLOBAL so Settings can receive it) 678 + pubsub.publish('system', scopes.GLOBAL, 'ext:all-loaded', { 679 + count: extensionWindows.size 680 + }); 676 681 }; 677 682 678 683 // TODO: unhack all this trash fire ··· 2346 2351 ipcMain.handle('extension-window-list', async (ev) => { 2347 2352 try { 2348 2353 const running = getRunningExtensions(); 2354 + console.log('extension-window-list:', running.map(e => e.id)); 2349 2355 return { success: true, data: running }; 2350 2356 } catch (error) { 2351 2357 console.error('extension-window-list error:', error);
+1 -31
preload.js
··· 415 415 * @returns {Promise<{success: boolean, data?: Array, error?: string}>} 416 416 */ 417 417 list: () => { 418 - return new Promise((resolve) => { 419 - const replyTopic = `ext:list:reply:${rndm()}`; 420 - 421 - // One-time subscription for reply 422 - ipcRenderer.send('subscribe', { 423 - source: sourceAddress, 424 - scope: 1, // SYSTEM 425 - topic: replyTopic, 426 - replyTopic: replyTopic 427 - }); 428 - 429 - const handler = (ev, msg) => { 430 - ipcRenderer.removeListener(replyTopic, handler); 431 - resolve(msg); 432 - }; 433 - ipcRenderer.on(replyTopic, handler); 434 - 435 - // Request list 436 - ipcRenderer.send('publish', { 437 - source: sourceAddress, 438 - scope: 1, 439 - topic: 'ext:list', 440 - data: { replyTopic } 441 - }); 442 - 443 - // Timeout after 5s 444 - setTimeout(() => { 445 - ipcRenderer.removeListener(replyTopic, handler); 446 - resolve({ success: false, error: 'Timeout waiting for extension list' }); 447 - }, 5000); 448 - }); 418 + return ipcRenderer.invoke('extension-window-list'); 449 419 }, 450 420 451 421 /**