// Side Notes — Notes List Page // // A dedicated extension page that lists all notes grouped by day and sorted // by recency. Clicking an entry either focuses an already-open tab (if one // exists for that note's URL key) or navigates the current tab to it. // The side panel is then opened on the target tab. import { buildNoteKey } from '../shared/key-builder.js'; import { listNotes } from '../shared/storage.js'; // --------------------------------------------------------------------------- // DOM // --------------------------------------------------------------------------- const listEl = document.getElementById('note-list'); const emptyMsg = document.getElementById('empty-message'); // --------------------------------------------------------------------------- // Group notes by day // --------------------------------------------------------------------------- /** * Return a human-readable day label for a date. * Today → "Today", Yesterday → "Yesterday", otherwise the locale date. */ function dayLabel(date) { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const target = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const diffDays = Math.round((today - target) / (1000 * 60 * 60 * 24)); if (diffDays === 0) return 'Today'; if (diffDays === 1) return 'Yesterday'; return target.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); } /** * Format a time string from an ISO timestamp. */ function formatTime(isoString) { const d = new Date(isoString); return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); } function groupByDay(notes) { const groups = new Map(); for (const note of notes) { const d = new Date(note.updatedAt); const key = dayLabel(d); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(note); } return groups; } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- async function render() { const notes = await listNotes(); if (notes.length === 0) { listEl.innerHTML = ''; listEl.appendChild(emptyMsg); return; } emptyMsg.remove(); listEl.innerHTML = ''; const groups = groupByDay(notes); for (const [label, groupNotes] of groups) { const section = document.createElement('div'); section.className = 'day-section'; const labelEl = document.createElement('div'); labelEl.className = 'day-label'; labelEl.textContent = label; section.appendChild(labelEl); for (const note of groupNotes) { section.appendChild(buildEntry(note)); } listEl.appendChild(section); } } function buildEntry(note) { const entry = document.createElement('div'); entry.className = 'note-entry'; entry.dataset.noteId = note.id; entry.dataset.key = note.key; entry.dataset.url = note.url || note.key; // fallback: reconstruct from key // Favicon const img = document.createElement('img'); img.src = note.favIconUrl || ''; img.alt = ''; img.width = 16; img.height = 16; img.onerror = () => { img.style.visibility = 'hidden'; }; entry.appendChild(img); // Text block const textBlock = document.createElement('div'); textBlock.className = 'entry-text'; const title = document.createElement('div'); title.className = 'entry-title'; title.textContent = note.pageTitle || note.key || 'Untitled'; const url = document.createElement('div'); url.className = 'entry-url'; url.textContent = note.key || ''; textBlock.appendChild(title); textBlock.appendChild(url); entry.appendChild(textBlock); // Time const time = document.createElement('div'); time.className = 'entry-time'; time.textContent = formatTime(note.updatedAt); entry.appendChild(time); // Click handler — see handleEntryClick below entry.addEventListener('click', () => handleEntryClick(note)); return entry; } // Our own tab ID — resolved at init time so we can call sidePanel.open() // synchronously in the click handler (required for user-gesture propagation). let ownTabId = null; // --------------------------------------------------------------------------- // Click handling — focus existing tab or navigate current // --------------------------------------------------------------------------- /** * Handle a click on a notes-list entry. * * IMPORTANT: `chrome.sidePanel.open()` requires a live user gesture and MUST * be called synchronously (no preceding await) in the click handler. We call * it first on our own tab as a fallback, then do the async matching work. If * a matching tab is found we call sidePanel.open() again on the matched tab. */ async function handleEntryClick(note) { const noteKey = note.key; // Step 1 (SYNCHRONOUS): open the side panel on our own tab to satisfy the // user-gesture requirement. This will be superseded if a matching tab is // found below, or if we navigate away. if (ownTabId != null) { chrome.sidePanel.open({ tabId: ownTabId }); } // Step 2: query all tabs and check for a match. let allTabs; try { allTabs = await chrome.tabs.query({}); } catch { return; } let matchTab = null; for (const tab of allTabs) { if (!tab.url) continue; try { const tabKey = buildNoteKey(tab.url); if (tabKey === noteKey) { matchTab = tab; break; } } catch { // URL parsing may fail for special URLs (chrome://, etc.) — skip. } } if (matchTab) { // A tab is already open at this note's URL → focus it and open its panel. chrome.windows.update(matchTab.windowId, { focused: true }); chrome.tabs.update(matchTab.id, { active: true }); chrome.sidePanel.open({ tabId: matchTab.id }); } else if (ownTabId != null) { // No match → navigate our own tab to the note's URL. // The key is the normalized URL (scheme://host/path?search), sufficient // for navigation even though the hash was stripped. chrome.tabs.update(ownTabId, { url: noteKey }); chrome.sidePanel.open({ tabId: ownTabId }); } else { // Fallback (shouldn't normally happen): create a new tab. try { const tab = await chrome.tabs.create({ url: noteKey }); chrome.sidePanel.open({ tabId: tab.id }); } catch { /* last resort — do nothing */ } } } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- (async function init() { // Resolve our own tab ID so click handlers can use it synchronously. try { const tab = await chrome.tabs.getCurrent(); if (tab) ownTabId = tab.id; } catch { /* not opened as a tab — ownTabId stays null */ } render(); })();