side-notes/notes-list/notes-list.js

217 lines
6.8 KiB
JavaScript
Raw Normal View History

// 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();
})();