Files
odysseus/static/js/features/notes/ui/reminderDisplay.js
T
Alexandre TeixeiraandLéo f1a47f3812 refactor(notes): decompose Notes into feature-owned modules (#6713)
* refactor(notes): decompose Notes into feature-owned modules

* fix(notes): avoid HTML interpolation in reminder picker

* docs(notes): drop checkpoint archives, session logs and publication drafts

The N0-N6 tarballs and raw pytest/node logs are not read by any script or
test, and the logs carry absolute home-directory paths. Once squashed into
dev they would stay in history for good. notes-pr.md and notes-issue.md
were the drafts used to open #6712 and #6713, not documentation.

notes-validation.md keeps the counts, the checkpoint hashes and
implementation.sha256 as the record; local interpreter paths and the local
Docker image name are replaced with portable placeholders.

* fix(notes): keep the state store raw and stop masonry re-observing after close

- state: writes built from views (filter, spread, splice) stored nested
  read-only views in the store, so structuredClone on a stored note threw
  DataCloneError. Store writes now unwrap nested views.
- layout: a save that resolves after closePanel re-renders the leaving pane
  and re-created the masonry ResizeObserver that teardown had disposed.
  Skip creating it once Notes is closed.
- index.js: drop 30 imported or declared names that were never used and
  group the imports at the top. Import order is unchanged.
- Revert unrelated punctuation edits in .gitattributes and keep the header
  comment and docstring first in compare/vote.js and
  test_notes_dom_xss_helpers.py.

Both fixes have a node test that fails on the previous head.

* test(notes): make script rejection assertion case-insensitive

Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum.

---------

Co-authored-by: Léo <leograndcontact@gmail.com>
2026-10-15 09:29:41 +01:00

130 lines
4.7 KiB
JavaScript

/** Reminder badges, sticky card highlights and entry scrolling. */
import { _isNoteFullyDone } from '../domain/noteRules.js';
import { _hasTimeComponent } from '../domain/recurrence.js';
export function createReminderDisplay({
_countFiredReminders,
_hasActiveReminder,
_loadActiveHighlights,
_loadGlowedReminders,
_loadPendingHighlights,
_saveActiveHighlights,
_saveGlowedReminders,
_savePendingHighlights,
_smoothScrollToNoteCard,
state
}) {
function _clearViewedReminderGlows() {
const active = _loadActiveHighlights();
if (!active.size) return;
_saveActiveHighlights(new Set());
document.querySelectorAll('.note-card-reminder-fired-sticky').forEach(card => {
card.classList.remove('note-card-reminder-fired-sticky');
});
}
function _setReminderCardGlow(noteId, on = true) {
if (!noteId) return;
const active = _loadActiveHighlights();
if (on) active.add(noteId);
else active.delete(noteId);
_saveActiveHighlights(active);
document.querySelectorAll(`.note-card[data-note-id="${noteId}"]`).forEach(card => {
card.classList.toggle('note-card-reminder-fired-sticky', on);
});
}
function _queuePendingHighlight(noteId) {
const set = _loadPendingHighlights();
set.add(noteId);
_savePendingHighlights(set);
}
function _flushPendingHighlights() {
// Fresh firings (queued by the background loop while the panel was closed)
// glow unconditionally : a notification just told the user something
// happened, so we always point at the note even if it was glowed before.
const queued = _loadPendingHighlights();
const glowed = _loadGlowedReminders();
const toGlow = new Set(queued);
// For notes that are merely overdue at open time (no fresh firing event),
// only glow the ones we haven't already shown : otherwise reopening the
// panel keeps lighting up old reminders forever.
for (const n of state.notes) {
if (!_hasActiveReminder(n) || !_hasTimeComponent(n.due_date)) continue;
if (queued.has(n.id) || !glowed.has(n.id)) toGlow.add(n.id);
}
// Always consume the queue.
_savePendingHighlights(new Set());
if (!toGlow.size) return;
let firstCard = null;
for (const id of toGlow) {
const card = document.querySelector(`.note-card[data-note-id="${id}"]`);
if (!card) continue;
_setReminderCardGlow(id, true);
if (!firstCard) firstCard = card;
glowed.add(id);
}
_saveGlowedReminders(glowed);
// Bring the first one into view so it can't get buried below the fold.
if (firstCard) {
requestAnimationFrame(() => {
_smoothScrollToNoteCard(firstCard);
});
}
}
function _updateRailBadge() {
const fired = _countFiredReminders();
// Rail (mini sidebar) : only show the count when reminders have ACTUALLY
// fired since the last dismissal (i.e. you haven't opened notes yet).
// Showing every overdue note forever made the badge feel permanent.
const railBtn = document.getElementById('rail-notes');
if (railBtn) {
let badge = railBtn.querySelector('.rail-notes-badge');
if (fired > 0) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'rail-notes-badge';
railBtn.appendChild(badge);
}
badge.textContent = fired > 99 ? '99+' : String(fired);
badge.classList.add('fired');
} else if (badge) {
badge.remove();
}
}
// Main sidebar button
const sidebarBtn = document.getElementById('tool-notes-btn');
if (sidebarBtn) {
let dot = sidebarBtn.querySelector('.tool-notes-dot');
if (fired > 0) {
if (!dot) {
dot = document.createElement('span');
dot.className = 'tool-notes-dot';
const newNoteBtn = sidebarBtn.querySelector('#notes-new-note-btn');
sidebarBtn.insertBefore(dot, newNoteBtn || null);
}
} else if (dot) {
dot.remove();
}
}
// Individual note cards : pulse ones with fired reminders
document.querySelectorAll('.note-card').forEach(card => {
const id = card.dataset.noteId;
const note = state.notes.find(n => n.id === id);
if (!note || note.archived || _isNoteFullyDone(note)) {
card.classList.remove('note-card-reminder-due');
return;
}
if (note.due_date && _hasTimeComponent(note.due_date)) {
const t = new Date(note.due_date).getTime();
card.classList.toggle('note-card-reminder-due', !isNaN(t) && t <= Date.now());
} else {
card.classList.remove('note-card-reminder-due');
}
});
}
return { _clearViewedReminderGlows, _setReminderCardGlow, _queuePendingHighlight, _flushPendingHighlights, _updateRailBadge };
}