experiments in a post-browser web
10
fork

Configure Feed

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

fix(groups): filter peek:// URLs and fix IZUI stack navigation

- Filter peek:// URLs from groups queries (only show http/https web pages)
- Use IZUI for groups window initialization and child window opening
- Fix IZUI focus requests to be targeted (only parent responds, not all windows)
- Track window IDs for proper parent-child relationships

+103 -38
+61 -20
app/izui.js
··· 19 19 // Each entry: { windowId, url } 20 20 const navigationStack = []; 21 21 22 + // My window ID (set during init) 23 + let myWindowId = null; 24 + 25 + // Parent window ID (if this window was opened as a child) 26 + let parentWindowId = null; 27 + 22 28 /** 23 29 * Notify parent window to take focus. 24 30 * Called when this window is about to close/hide. 25 - * Publishes globally so any listening parent will receive. 31 + * If we know our parent window ID, only that window should respond. 26 32 * 27 - * @param {string} [parentAddress] - The parent window's address (optional, for logging) 33 + * @param {number} [targetParentId] - Specific parent window ID to notify (optional) 28 34 */ 29 - export function notifyParentFocus(parentAddress) { 30 - DEBUG && console.log('[IZUI] Notifying parent to focus:', parentAddress || '(global)'); 35 + export function notifyParentFocus(targetParentId) { 36 + const targetId = targetParentId || parentWindowId; 37 + DEBUG && console.log('[IZUI] Notifying parent to focus, target:', targetId || '(global)'); 31 38 32 - // Publish globally so listening parent windows will receive 39 + // Publish focus request - if we have a target parent ID, include it 33 40 api.publish('izui:focus-request', { 34 41 from: window.location.toString(), 35 - parentAddress: parentAddress || null 42 + fromWindowId: myWindowId, 43 + targetWindowId: targetId || null 36 44 }, api.scopes.GLOBAL); 37 45 } 38 46 39 47 /** 40 48 * Set up focus listener for this window. 41 - * When a child window closes, this window will receive focus. 42 - * 43 - * Call this in windows that may have children (e.g., settings). 49 + * When a child window closes and targets us, this window will receive focus. 50 + * Only responds if we're the target or if no specific target was given and 51 + * the request came from a window we have in our navigation stack. 44 52 */ 45 53 export function setupFocusListener() { 46 54 const myAddress = window.location.toString(); 47 55 48 56 api.subscribe('izui:focus-request', async (msg) => { 49 - DEBUG && console.log('[IZUI] Received focus request from:', msg?.from); 57 + const { targetWindowId, fromWindowId, from } = msg || {}; 58 + 59 + DEBUG && console.log('[IZUI] Received focus request from:', from, 'target:', targetWindowId, 'myId:', myWindowId); 60 + 61 + // If a specific target was given, only respond if we're that target 62 + if (targetWindowId !== null && targetWindowId !== undefined) { 63 + if (targetWindowId !== myWindowId) { 64 + DEBUG && console.log('[IZUI] Focus request not for us, ignoring'); 65 + return; 66 + } 67 + } else { 68 + // No specific target - only respond if the sender is in our nav stack 69 + const inOurStack = navigationStack.some(entry => entry.windowId === fromWindowId); 70 + if (!inOurStack && fromWindowId) { 71 + DEBUG && console.log('[IZUI] Sender not in our stack, ignoring'); 72 + return; 73 + } 74 + } 50 75 51 76 // Focus this window 52 - if (api.window && api.window.focus) { 53 - // Get our own window ID and focus it 54 - const windowId = await api.window.getWindowId(); 55 - if (windowId) { 56 - api.window.focus(windowId); 57 - } 77 + if (api.window && api.window.focus && myWindowId) { 78 + api.window.focus(myWindowId); 79 + DEBUG && console.log('[IZUI] Focused self:', myWindowId); 58 80 } 59 81 }, api.scopes.GLOBAL); 60 82 ··· 107 129 export async function openChildWindow(address, params = {}) { 108 130 const myAddress = window.location.toString(); 109 131 110 - // Track this window as the parent 132 + // Ensure we have our window ID 133 + if (!myWindowId && api.window && api.window.getWindowId) { 134 + myWindowId = await api.window.getWindowId(); 135 + } 136 + 137 + // Track this window as the parent - pass our window ID so child can target us 111 138 // Set escapeMode: 'navigate' so backend asks renderer before closing 112 139 const childParams = { 113 140 ...params, 114 141 izuiParent: myAddress, 142 + izuiParentWindowId: myWindowId, 115 143 escapeMode: params.escapeMode || 'navigate' 116 144 }; 117 145 118 - DEBUG && console.log('[IZUI] Opening child window:', address, 'parent:', myAddress); 146 + DEBUG && console.log('[IZUI] Opening child window:', address, 'parent:', myAddress, 'parentId:', myWindowId); 119 147 120 148 const result = await api.window.open(address, childParams); 121 149 ··· 188 216 * @param {boolean} [options.canHaveChildren=true] - Whether this window may open children 189 217 * @param {boolean} [options.setupEscape=true] - Whether to set up escape handling (notifies parent on close) 190 218 * @param {boolean} [options.closeOnEscape=true] - Whether ESC closes this window (when no internal nav) 219 + * @param {number} [options.parentWindowId] - Parent window ID (if known, for targeted focus return) 191 220 */ 192 - export function init(options = {}) { 193 - const { onEscape, canHaveChildren = true, setupEscape = true, closeOnEscape = true } = options; 221 + export async function init(options = {}) { 222 + const { onEscape, canHaveChildren = true, setupEscape = true, closeOnEscape = true, parentWindowId: providedParentId } = options; 223 + 224 + // Get our window ID 225 + if (api.window && api.window.getWindowId) { 226 + myWindowId = await api.window.getWindowId(); 227 + DEBUG && console.log('[IZUI] My window ID:', myWindowId); 228 + } 229 + 230 + // Store parent window ID if provided 231 + if (providedParentId) { 232 + parentWindowId = providedParentId; 233 + DEBUG && console.log('[IZUI] Parent window ID:', parentWindowId); 234 + } 194 235 195 236 if (canHaveChildren) { 196 237 setupFocusListener();
+42 -18
extensions/groups/home.js
··· 8 8 * - Viewing a group shows all addresses with that tag 9 9 */ 10 10 11 + import izui from 'peek://app/izui.js'; 12 + 11 13 const api = window.app; 12 14 const debug = api.debug; 15 + 16 + /** 17 + * Check if a URL is a navigable web URL (http/https only) 18 + * Excludes peek:// and other internal URLs 19 + */ 20 + const isWebUrl = (url) => { 21 + if (!url) return false; 22 + return url.startsWith('http://') || url.startsWith('https://'); 23 + }; 13 24 14 25 // View states 15 26 const VIEW_GROUPS = 'groups'; ··· 37 48 // Expose state for debugging in tests 38 49 window._groupsState = state; 39 50 40 - // Handle ESC - cooperative escape handling with window manager 41 - // Returns { handled: true } if we navigated internally 42 - // Returns { handled: false } if at root (groups list) and window should close 43 - api.escape.onEscape(() => { 51 + /** 52 + * Internal ESC handler for groups navigation 53 + * Returns { handled: true } if we navigated internally 54 + * Returns { handled: false } if at root (groups list) and window should close 55 + */ 56 + const handleEscape = () => { 44 57 // If search has content, clear it first 45 58 const searchInput = document.querySelector('.search-input'); 46 59 if (state.searchQuery) { ··· 62 75 } 63 76 // At root (groups list) - let window close 64 77 return { handled: false }; 65 - }); 78 + }; 66 79 67 80 /** 68 81 * Get all cards in the current view ··· 180 193 181 194 const init = async () => { 182 195 debug && console.log('Groups init'); 196 + 197 + // Initialize IZUI for proper escape handling and child window tracking 198 + await izui.init({ 199 + onEscape: handleEscape, 200 + canHaveChildren: true, 201 + closeOnEscape: true 202 + }); 183 203 184 204 // Load tags from datastore 185 205 await loadTags(); ··· 218 238 state.tags = result.data; 219 239 debug && console.log('Loaded tags:', state.tags.length); 220 240 221 - // Fetch URL item count for each tag (only URLs for now) 241 + // Fetch URL item count for each tag (only http/https URLs) 222 242 for (const tag of state.tags) { 223 243 const itemsResult = await api.datastore.getItemsByTag(tag.id); 224 244 if (itemsResult.success) { 225 - tag.addressCount = itemsResult.data.filter(item => item.type === 'url').length; 245 + tag.addressCount = itemsResult.data.filter(item => 246 + item.type === 'url' && isWebUrl(item.content) 247 + ).length; 226 248 } else { 227 249 tag.addressCount = 0; 228 250 } ··· 232 254 state.tags = []; 233 255 } 234 256 235 - // Get count of untagged URL items (only URLs for now) 257 + // Get count of untagged URL items (only http/https URLs) 236 258 // Query all items and filter out those with tags 237 259 const allItemsResult = await api.datastore.queryItems({}); 238 260 if (allItemsResult.success) { 239 261 const untaggedItems = []; 240 262 for (const item of allItemsResult.data) { 241 - // Only include URL items 242 - if (item.type !== 'url') continue; 263 + // Only include http/https URL items 264 + if (item.type !== 'url' || !isWebUrl(item.content)) continue; 243 265 const tagsResult = await api.datastore.getItemTags(item.id); 244 266 if (tagsResult.success && tagsResult.data.length === 0) { 245 267 untaggedItems.push(item); ··· 253 275 }; 254 276 255 277 /** 256 - * Load URL items for a specific tag (only URLs for now) 278 + * Load URL items for a specific tag (only http/https URLs) 257 279 */ 258 280 const loadAddressesForTag = async (tagId) => { 259 281 const result = await api.datastore.getItemsByTag(tagId); 260 282 if (result.success) { 261 - // Only include URL items 262 - state.addresses = result.data.filter(item => item.type === 'url'); 283 + // Only include http/https URL items 284 + state.addresses = result.data.filter(item => 285 + item.type === 'url' && isWebUrl(item.content) 286 + ); 263 287 debug && console.log('Loaded URL items for tag:', state.addresses.length); 264 288 } else { 265 289 console.error('Failed to load addresses:', result.error); ··· 358 382 state.currentTag = tag; 359 383 state.searchQuery = ''; 360 384 361 - // Load URL items - handle special untagged group (only URLs for now) 385 + // Load URL items - handle special untagged group (only http/https URLs) 362 386 if (tag.isSpecial && tag.id === '__untagged__') { 363 387 const allItemsResult = await api.datastore.queryItems({}); 364 388 if (allItemsResult.success) { 365 389 const untaggedItems = []; 366 390 for (const item of allItemsResult.data) { 367 - // Only include URL items 368 - if (item.type !== 'url') continue; 391 + // Only include http/https URL items 392 + if (item.type !== 'url' || !isWebUrl(item.content)) continue; 369 393 const tagsResult = await api.datastore.getItemTags(item.id); 370 394 if (tagsResult.success && tagsResult.data.length === 0) { 371 395 untaggedItems.push(item); ··· 508 532 card.appendChild(favicon); 509 533 card.appendChild(content); 510 534 511 - // Click to open address 535 + // Click to open address - use IZUI for proper stack tracking 512 536 card.addEventListener('click', async () => { 513 537 debug && console.log('Opening address:', addressUrl); 514 - const result = await api.window.open(addressUrl, { 538 + const result = await izui.openChildWindow(addressUrl, { 515 539 width: 800, 516 540 height: 600 517 541 });