Implement Phase 1 remaining tasks: notifications append-only, listener leak fix, memory caps
Task 1.2: Append-only rendering for notifications page
- Extracted buildNotificationRow(item) helper from renderNotifications()
- Added prependNotificationRow(item) with sorted insertion by created_at
- addNotificationFromEvent uses prepend when DOM has children
Task 1.3: Fix notification document.click listener leak
- Removed per-row document.addEventListener('click') that leaked on every render
- Added single delegated click listener in main() using .notif-menu-panel class
- Menu panels still close when clicking outside .notif-right
Task 1.4: Cap posts[] and postIds with rolling eviction
- Added MAX_STORED_POSTS=500 constant to feed.html and post.html
- Added prunePostsArray() that sorts, slices, rebuilds postIds/renderedPostIds
- Called in upsertFeedPost/upsertPost with +50 batch threshold
- post.html skips pruning in event mode
Task 1.5: Cap notificationsById/unreadNotificationsById with periodic pruning
- Added MAX_STORED_NOTIFICATIONS=500, MAX_POST_IMAGE_CACHE=200
- Added pruneNotificationMaps() that prunes both Maps and LRU-caps postImageCache
- Called in addNotificationFromEvent, hydrateNotificationsFromCache/Relays
Updated plan checklist marking all Phase 1 tasks complete.
This commit is contained in:
@@ -436,23 +436,26 @@ flowchart LR
|
||||
- [x] "See More" still does full re-render
|
||||
- [x] Verified: `data-post-id` attribute set by `renderPostItem` for trim logic
|
||||
- [x] Reviewed: no errors found, fallback paths correct
|
||||
- [ ] **1.2** Append-only rendering for notifications page
|
||||
- [ ] Split `renderNotifications()` into full rebuild + `prependNotificationRow()`
|
||||
- [ ] New notifications prepend without rebuilding existing rows
|
||||
- [ ] Filter toggles and "View more" still do full re-renders
|
||||
- [ ] **1.3** Fix notification `document.click` listener leak
|
||||
- [ ] Remove per-row `document.addEventListener('click', ...)`
|
||||
- [ ] Register single delegated document-level click listener in `main()`
|
||||
- [ ] Menu panels still close when clicking outside
|
||||
- [ ] **1.4** Cap `posts[]` and `postIds` with rolling eviction
|
||||
- [ ] Add `MAX_STORED_POSTS = 500` constant
|
||||
- [ ] Evict oldest posts when array exceeds cap
|
||||
- [ ] Rebuild `postIds` and `renderedPostIds` after eviction
|
||||
- [ ] **1.5** Cap `notificationsById` / `unreadNotificationsById` with periodic pruning
|
||||
- [ ] Add `MAX_STORED_NOTIFICATIONS = 500`
|
||||
- [ ] Add `pruneNotificationMaps()` function
|
||||
- [ ] Call prune on overflow, after cache/relay hydration
|
||||
- [ ] LRU-cap `postImageCache` to 200 entries
|
||||
- [x] **1.2** Append-only rendering for notifications page
|
||||
- [x] Split `renderNotifications()` into full rebuild + `prependNotificationRow()`
|
||||
- [x] Extracted `buildNotificationRow(item)` helper shared by both functions
|
||||
- [x] New notifications prepend without rebuilding existing rows
|
||||
- [x] Filter toggles and "View more" still do full re-renders
|
||||
- [x] **1.3** Fix notification `document.click` listener leak
|
||||
- [x] Remove per-row `document.addEventListener('click', ...)`
|
||||
- [x] Register single delegated document-level click listener in `main()`
|
||||
- [x] Added `notif-menu-panel` class for panel discovery
|
||||
- [x] Menu panels still close when clicking outside
|
||||
- [x] **1.4** Cap `posts[]` and `postIds` with rolling eviction
|
||||
- [x] Add `MAX_STORED_POSTS = 500` constant
|
||||
- [x] Evict oldest posts when array exceeds cap (+50 batch threshold)
|
||||
- [x] Rebuild `postIds` and `renderedPostIds` after eviction
|
||||
- [x] Skip pruning in post.html event mode
|
||||
- [x] **1.5** Cap `notificationsById` / `unreadNotificationsById` with periodic pruning
|
||||
- [x] Add `MAX_STORED_NOTIFICATIONS = 500`
|
||||
- [x] Add `pruneNotificationMaps()` function
|
||||
- [x] Call prune on overflow, after cache/relay hydration
|
||||
- [x] LRU-cap `postImageCache` to 200 entries
|
||||
|
||||
### Phase 2: P1 — Subscription Hygiene & Polling Reduction
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ import { initPostCards } from './js/post-interactions2.mjs';
|
||||
const MIN_BOOTSTRAP_POSTS = 10;
|
||||
const FEED_BOOTSTRAP_WINDOWS_SECONDS = [3600, 6 * 3600, 24 * 3600, 7 * 24 * 3600, 30 * 24 * 3600];
|
||||
const FEED_BOOTSTRAP_WINDOW_LIMIT = 120;
|
||||
const MAX_STORED_POSTS = 500;
|
||||
|
||||
let currentPubkey = null;
|
||||
let updateIntervalId = null;
|
||||
@@ -449,11 +450,24 @@ import { initPostCards } from './js/post-interactions2.mjs';
|
||||
}
|
||||
}
|
||||
|
||||
function prunePostsArray() {
|
||||
posts.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
||||
if (posts.length > MAX_STORED_POSTS) {
|
||||
posts.length = MAX_STORED_POSTS;
|
||||
postIds.clear();
|
||||
posts.forEach((p) => postIds.add(p.id));
|
||||
for (const id of Array.from(renderedPostIds)) {
|
||||
if (!postIds.has(id)) renderedPostIds.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function upsertFeedPost(evt) {
|
||||
if (!evt?.id || postIds.has(evt.id) || !isOriginalPost(evt)) return false;
|
||||
if (isEventMuted(evt)) return false;
|
||||
postIds.add(evt.id);
|
||||
posts.push(evt);
|
||||
if (posts.length > MAX_STORED_POSTS + 50) prunePostsArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.86",
|
||||
"VERSION_NUMBER": "0.7.86",
|
||||
"BUILD_DATE": "2026-07-01T13:22:33.456Z"
|
||||
"VERSION": "v0.7.87",
|
||||
"VERSION_NUMBER": "0.7.87",
|
||||
"BUILD_DATE": "2026-07-01T13:33:29.318Z"
|
||||
}
|
||||
|
||||
@@ -393,6 +393,8 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
|
||||
const NOTIFICATION_KINDS = [1, 3, 4, 6, 7, 9735];
|
||||
const NOTIFICATIONS_PAGE_SIZE = 40;
|
||||
const MAX_STORED_NOTIFICATIONS = 500;
|
||||
const MAX_POST_IMAGE_CACHE = 200;
|
||||
const SVG_UNCHECKED = `<svg class="svgBtn svgBtn_unsel" viewBox="0 0 10 10"><rect x="2" y="2" width="6" height="6" /></svg>`;
|
||||
const SVG_CHECKED = `<svg class="svgBtn svgBtn_sel" viewBox="0 0 10 10"><path d="M2,6 L4,8 L8,2 " /></svg>`;
|
||||
const NOTIFICATION_FILTER_DEFS = [
|
||||
@@ -883,6 +885,173 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
}
|
||||
}
|
||||
|
||||
function buildNotificationRow(item) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'notif-item';
|
||||
row.dataset.notifId = item.id;
|
||||
row.dataset.notifCreatedAt = String(item.created_at || 0);
|
||||
|
||||
const avatar = document.createElement('img');
|
||||
avatar.className = 'notif-avatar';
|
||||
avatar.alt = '';
|
||||
avatar.src = item.actor.picture || './favicon/favicon-dots2.ico';
|
||||
avatar.onerror = () => {
|
||||
console.warn('[notifications] actor avatar failed to load', {
|
||||
eventId: item.id,
|
||||
actor: item.actor.name,
|
||||
src: avatar.src
|
||||
});
|
||||
avatar.onerror = null;
|
||||
avatar.src = './favicon/favicon-dots2.ico';
|
||||
};
|
||||
|
||||
if (item.actor?.pubkey) {
|
||||
avatar.style.cursor = 'pointer';
|
||||
avatar.title = 'Open profile';
|
||||
avatar.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const profileUrl = `./post.html?profile=${encodeURIComponent(item.actor.pubkey)}`;
|
||||
window.open(profileUrl, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
}
|
||||
|
||||
const secondaryImageUrl = normalizeMediaUrl(item.secondaryImage || '');
|
||||
const secondaryEventUrl = makeEventPermalink(item.targetEventId);
|
||||
|
||||
let secondaryNode;
|
||||
|
||||
if (secondaryImageUrl) {
|
||||
const secondaryAvatar = document.createElement('img');
|
||||
secondaryAvatar.className = 'notif-avatar notif-avatar-secondary';
|
||||
secondaryAvatar.alt = '';
|
||||
secondaryAvatar.src = secondaryImageUrl;
|
||||
secondaryAvatar.onerror = () => {
|
||||
console.debug('[notifications] secondary image failed to load', {
|
||||
eventId: item.id,
|
||||
src: secondaryAvatar.src
|
||||
});
|
||||
secondaryAvatar.style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
if (secondaryEventUrl) {
|
||||
secondaryAvatar.style.cursor = 'pointer';
|
||||
secondaryAvatar.title = 'Open related event';
|
||||
secondaryAvatar.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
window.open(secondaryEventUrl, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
}
|
||||
|
||||
secondaryNode = secondaryAvatar;
|
||||
} else if (secondaryEventUrl) {
|
||||
const secondaryPlaceholder = document.createElement('a');
|
||||
secondaryPlaceholder.className = 'notif-avatar notif-avatar-secondary notif-avatar-placeholder';
|
||||
secondaryPlaceholder.href = secondaryEventUrl;
|
||||
secondaryPlaceholder.target = '_blank';
|
||||
secondaryPlaceholder.rel = 'noopener noreferrer';
|
||||
secondaryPlaceholder.title = 'Open related event';
|
||||
secondaryPlaceholder.setAttribute('aria-label', 'Open related event');
|
||||
secondaryPlaceholder.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
secondaryNode = secondaryPlaceholder;
|
||||
} else {
|
||||
const secondaryAvatar = document.createElement('div');
|
||||
secondaryAvatar.className = 'notif-avatar notif-avatar-secondary';
|
||||
secondaryAvatar.style.visibility = 'hidden';
|
||||
secondaryNode = secondaryAvatar;
|
||||
}
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'notif-content';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'notif-name';
|
||||
name.textContent = item.actor.name;
|
||||
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'notif-desc';
|
||||
desc.textContent = item.description;
|
||||
|
||||
content.appendChild(name);
|
||||
content.appendChild(desc);
|
||||
|
||||
const time = document.createElement('div');
|
||||
time.className = 'notif-time';
|
||||
time.textContent = formatTimeAgo(item.created_at || 0);
|
||||
timeElements.set(time, item.created_at || 0);
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.className = 'notif-right';
|
||||
|
||||
const menuButton = document.createElement('button');
|
||||
menuButton.className = 'notif-menu-btn';
|
||||
menuButton.type = 'button';
|
||||
menuButton.textContent = '⋯';
|
||||
menuButton.title = 'Notification menu';
|
||||
|
||||
const menuPanel = document.createElement('div');
|
||||
menuPanel.className = 'notif-menu-panel';
|
||||
menuPanel.style.position = 'absolute';
|
||||
menuPanel.style.right = '0';
|
||||
menuPanel.style.top = '26px';
|
||||
menuPanel.style.zIndex = '10';
|
||||
menuPanel.style.display = 'none';
|
||||
menuPanel.style.minWidth = '140px';
|
||||
menuPanel.style.padding = '6px';
|
||||
menuPanel.style.border = 'var(--border)';
|
||||
menuPanel.style.borderRadius = 'var(--border-radius)';
|
||||
menuPanel.style.background = 'var(--secondary-color)';
|
||||
|
||||
const blockButton = document.createElement('button');
|
||||
blockButton.type = 'button';
|
||||
blockButton.textContent = 'Block account';
|
||||
blockButton.style.width = '100%';
|
||||
blockButton.style.textAlign = 'left';
|
||||
blockButton.style.border = 'var(--button-border-width) solid var(--button-border-color)';
|
||||
blockButton.style.borderRadius = 'var(--button-border-radius)';
|
||||
blockButton.style.background = 'var(--button-background-color)';
|
||||
blockButton.style.color = 'var(--button-color)';
|
||||
blockButton.style.padding = '6px 8px';
|
||||
blockButton.style.cursor = 'pointer';
|
||||
|
||||
blockButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
const actorPubkey = String(item?.actor?.pubkey || '').trim();
|
||||
if (!actorPubkey) return;
|
||||
|
||||
const ok = window.confirm(`Block ${item.actor.name || actorPubkey.slice(0, 8)}?`);
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
await blockActorPubkey(actorPubkey);
|
||||
} catch (error) {
|
||||
console.warn('[notifications] Failed to block account:', error?.message || error);
|
||||
} finally {
|
||||
menuPanel.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
menuPanel.appendChild(blockButton);
|
||||
|
||||
right.style.position = 'relative';
|
||||
menuButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
menuPanel.style.display = menuPanel.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
right.appendChild(menuButton);
|
||||
right.appendChild(menuPanel);
|
||||
right.appendChild(time);
|
||||
|
||||
row.appendChild(avatar);
|
||||
row.appendChild(secondaryNode);
|
||||
row.appendChild(content);
|
||||
row.appendChild(right);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderNotifications() {
|
||||
const items = Array.from(notificationsById.values())
|
||||
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
||||
@@ -892,172 +1061,7 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
timeElements.clear();
|
||||
|
||||
for (const item of visibleItems) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'notif-item';
|
||||
|
||||
const avatar = document.createElement('img');
|
||||
avatar.className = 'notif-avatar';
|
||||
avatar.alt = '';
|
||||
avatar.src = item.actor.picture || './favicon/favicon-dots2.ico';
|
||||
avatar.onerror = () => {
|
||||
console.warn('[notifications] actor avatar failed to load', {
|
||||
eventId: item.id,
|
||||
actor: item.actor.name,
|
||||
src: avatar.src
|
||||
});
|
||||
avatar.onerror = null;
|
||||
avatar.src = './favicon/favicon-dots2.ico';
|
||||
};
|
||||
|
||||
if (item.actor?.pubkey) {
|
||||
avatar.style.cursor = 'pointer';
|
||||
avatar.title = 'Open profile';
|
||||
avatar.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const profileUrl = `./post.html?profile=${encodeURIComponent(item.actor.pubkey)}`;
|
||||
window.open(profileUrl, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
}
|
||||
|
||||
const secondaryImageUrl = normalizeMediaUrl(item.secondaryImage || '');
|
||||
const secondaryEventUrl = makeEventPermalink(item.targetEventId);
|
||||
|
||||
let secondaryNode;
|
||||
|
||||
if (secondaryImageUrl) {
|
||||
const secondaryAvatar = document.createElement('img');
|
||||
secondaryAvatar.className = 'notif-avatar notif-avatar-secondary';
|
||||
secondaryAvatar.alt = '';
|
||||
secondaryAvatar.src = secondaryImageUrl;
|
||||
secondaryAvatar.onerror = () => {
|
||||
console.debug('[notifications] secondary image failed to load', {
|
||||
eventId: item.id,
|
||||
src: secondaryAvatar.src
|
||||
});
|
||||
secondaryAvatar.style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
if (secondaryEventUrl) {
|
||||
secondaryAvatar.style.cursor = 'pointer';
|
||||
secondaryAvatar.title = 'Open related event';
|
||||
secondaryAvatar.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
window.open(secondaryEventUrl, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
}
|
||||
|
||||
secondaryNode = secondaryAvatar;
|
||||
} else if (secondaryEventUrl) {
|
||||
const secondaryPlaceholder = document.createElement('a');
|
||||
secondaryPlaceholder.className = 'notif-avatar notif-avatar-secondary notif-avatar-placeholder';
|
||||
secondaryPlaceholder.href = secondaryEventUrl;
|
||||
secondaryPlaceholder.target = '_blank';
|
||||
secondaryPlaceholder.rel = 'noopener noreferrer';
|
||||
secondaryPlaceholder.title = 'Open related event';
|
||||
secondaryPlaceholder.setAttribute('aria-label', 'Open related event');
|
||||
secondaryPlaceholder.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
secondaryNode = secondaryPlaceholder;
|
||||
} else {
|
||||
const secondaryAvatar = document.createElement('div');
|
||||
secondaryAvatar.className = 'notif-avatar notif-avatar-secondary';
|
||||
secondaryAvatar.style.visibility = 'hidden';
|
||||
secondaryNode = secondaryAvatar;
|
||||
}
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'notif-content';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'notif-name';
|
||||
name.textContent = item.actor.name;
|
||||
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'notif-desc';
|
||||
desc.textContent = item.description;
|
||||
|
||||
content.appendChild(name);
|
||||
content.appendChild(desc);
|
||||
|
||||
const time = document.createElement('div');
|
||||
time.className = 'notif-time';
|
||||
time.textContent = formatTimeAgo(item.created_at || 0);
|
||||
timeElements.set(time, item.created_at || 0);
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.className = 'notif-right';
|
||||
|
||||
const menuButton = document.createElement('button');
|
||||
menuButton.className = 'notif-menu-btn';
|
||||
menuButton.type = 'button';
|
||||
menuButton.textContent = '⋯';
|
||||
menuButton.title = 'Notification menu';
|
||||
|
||||
const menuPanel = document.createElement('div');
|
||||
menuPanel.style.position = 'absolute';
|
||||
menuPanel.style.right = '0';
|
||||
menuPanel.style.top = '26px';
|
||||
menuPanel.style.zIndex = '10';
|
||||
menuPanel.style.display = 'none';
|
||||
menuPanel.style.minWidth = '140px';
|
||||
menuPanel.style.padding = '6px';
|
||||
menuPanel.style.border = 'var(--border)';
|
||||
menuPanel.style.borderRadius = 'var(--border-radius)';
|
||||
menuPanel.style.background = 'var(--secondary-color)';
|
||||
|
||||
const blockButton = document.createElement('button');
|
||||
blockButton.type = 'button';
|
||||
blockButton.textContent = 'Block account';
|
||||
blockButton.style.width = '100%';
|
||||
blockButton.style.textAlign = 'left';
|
||||
blockButton.style.border = 'var(--button-border-width) solid var(--button-border-color)';
|
||||
blockButton.style.borderRadius = 'var(--button-border-radius)';
|
||||
blockButton.style.background = 'var(--button-background-color)';
|
||||
blockButton.style.color = 'var(--button-color)';
|
||||
blockButton.style.padding = '6px 8px';
|
||||
blockButton.style.cursor = 'pointer';
|
||||
|
||||
blockButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
const actorPubkey = String(item?.actor?.pubkey || '').trim();
|
||||
if (!actorPubkey) return;
|
||||
|
||||
const ok = window.confirm(`Block ${item.actor.name || actorPubkey.slice(0, 8)}?`);
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
await blockActorPubkey(actorPubkey);
|
||||
} catch (error) {
|
||||
console.warn('[notifications] Failed to block account:', error?.message || error);
|
||||
} finally {
|
||||
menuPanel.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
menuPanel.appendChild(blockButton);
|
||||
|
||||
right.style.position = 'relative';
|
||||
menuButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
menuPanel.style.display = menuPanel.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!right.contains(event.target)) {
|
||||
menuPanel.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
right.appendChild(menuButton);
|
||||
right.appendChild(menuPanel);
|
||||
right.appendChild(time);
|
||||
|
||||
row.appendChild(avatar);
|
||||
row.appendChild(secondaryNode);
|
||||
row.appendChild(content);
|
||||
row.appendChild(right);
|
||||
|
||||
const row = buildNotificationRow(item);
|
||||
divNotifications.appendChild(row);
|
||||
}
|
||||
|
||||
@@ -1071,6 +1075,56 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
renderNotificationFilters();
|
||||
}
|
||||
|
||||
function prependNotificationRow(item) {
|
||||
if (divNotifications.querySelector(`[data-notif-id="${CSS.escape(String(item.id))}"]`)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (divNotifications.children.length === 0) {
|
||||
renderNotifications();
|
||||
return false;
|
||||
}
|
||||
|
||||
const row = buildNotificationRow(item);
|
||||
const newCreatedAt = Number(item.created_at || 0);
|
||||
|
||||
let inserted = false;
|
||||
for (const existing of divNotifications.children) {
|
||||
const existingCreatedAt = Number(existing.dataset?.notifCreatedAt || 0);
|
||||
if (newCreatedAt > existingCreatedAt) {
|
||||
divNotifications.insertBefore(row, existing);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inserted) {
|
||||
divNotifications.appendChild(row);
|
||||
}
|
||||
|
||||
while (divNotifications.children.length > visibleNotificationsCount) {
|
||||
const last = divNotifications.lastElementChild;
|
||||
if (last) {
|
||||
const timeEl = last.querySelector('.notif-time');
|
||||
if (timeEl) timeElements.delete(timeEl);
|
||||
last.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const total = notificationsById.size;
|
||||
const hasMore = total > visibleNotificationsCount;
|
||||
if (btnViewMoreNotifications) {
|
||||
btnViewMoreNotifications.style.display = hasMore ? 'block' : 'none';
|
||||
}
|
||||
|
||||
updateHint();
|
||||
refreshTimes();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function blockActorPubkey(pubkey) {
|
||||
const target = String(pubkey || '').trim();
|
||||
if (!target || target === currentPubkey) return;
|
||||
@@ -1092,6 +1146,29 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
renderNotifications();
|
||||
}
|
||||
|
||||
function pruneNotificationMaps() {
|
||||
if (notificationsById.size > MAX_STORED_NOTIFICATIONS) {
|
||||
const sorted = Array.from(notificationsById.values())
|
||||
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
||||
const keep = new Set(sorted.slice(0, MAX_STORED_NOTIFICATIONS).map((n) => n.id));
|
||||
for (const id of Array.from(notificationsById.keys())) {
|
||||
if (!keep.has(id)) {
|
||||
notificationsById.delete(id);
|
||||
unreadNotificationsById.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (postImageCache.size > MAX_POST_IMAGE_CACHE) {
|
||||
const excess = postImageCache.size - MAX_POST_IMAGE_CACHE;
|
||||
let removed = 0;
|
||||
for (const key of Array.from(postImageCache.keys())) {
|
||||
if (removed >= excess) break;
|
||||
postImageCache.delete(key);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addNotificationFromEvent(evt) {
|
||||
if (!evt || !evt.id || !evt.pubkey) return;
|
||||
if (evt.pubkey === currentPubkey) return;
|
||||
@@ -1138,7 +1215,15 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
isUnread: createdAt > notificationsReadAt
|
||||
});
|
||||
|
||||
renderNotifications();
|
||||
if (divNotifications.children.length > 0) {
|
||||
prependNotificationRow(item);
|
||||
} else {
|
||||
renderNotifications();
|
||||
}
|
||||
|
||||
if (notificationsById.size > MAX_STORED_NOTIFICATIONS + 100) {
|
||||
pruneNotificationMaps();
|
||||
}
|
||||
}
|
||||
|
||||
function eventTargetsCurrentPubkey(evt) {
|
||||
@@ -1162,6 +1247,8 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
await addNotificationFromEvent(evt);
|
||||
}
|
||||
|
||||
pruneNotificationMaps();
|
||||
|
||||
console.log('[notifications] cache preload complete', {
|
||||
cachedCount: Array.isArray(cached) ? cached.length : 0,
|
||||
loadedCount: filtered.length
|
||||
@@ -1184,6 +1271,8 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
await addNotificationFromEvent(evt);
|
||||
}
|
||||
|
||||
pruneNotificationMaps();
|
||||
|
||||
console.log('[notifications] relay hydration complete', {
|
||||
relayCount: sorted.length
|
||||
});
|
||||
@@ -1297,6 +1386,19 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
(async function main() {
|
||||
try {
|
||||
initHamburgerMenu();
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (target.closest('.notif-right')) return;
|
||||
const panels = document.querySelectorAll('.notif-menu-panel');
|
||||
for (const panel of panels) {
|
||||
if (panel.style.display !== 'none') {
|
||||
panel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
syncNotificationFiltersFromSettings({});
|
||||
|
||||
const divSvgHam = document.getElementById('divSvgHam');
|
||||
|
||||
@@ -240,6 +240,7 @@ import { initPostCards } from './js/post-interactions2.mjs';
|
||||
const MIN_BOOTSTRAP_POSTS = 10;
|
||||
const BOOTSTRAP_WINDOWS_SECONDS = [24 * 3600, 7 * 24 * 3600, 30 * 24 * 3600, 90 * 24 * 3600];
|
||||
const BOOTSTRAP_PER_WINDOW_LIMIT = 120;
|
||||
const MAX_STORED_POSTS = 500;
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const profileParamRaw = (urlParams.get('profile') || '').trim();
|
||||
@@ -612,11 +613,24 @@ import { initPostCards } from './js/post-interactions2.mjs';
|
||||
return eTags.every(t => t[3] === 'mention');
|
||||
}
|
||||
|
||||
function prunePostsArray() {
|
||||
posts.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
||||
if (posts.length > MAX_STORED_POSTS) {
|
||||
posts.length = MAX_STORED_POSTS;
|
||||
postIds.clear();
|
||||
posts.forEach((p) => postIds.add(p.id));
|
||||
for (const id of Array.from(renderedPostIds)) {
|
||||
if (!postIds.has(id)) renderedPostIds.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function upsertPost(evt) {
|
||||
if (!evt?.id || postIds.has(evt.id) || !isOriginalPost(evt)) return false;
|
||||
if (isEventMuted(evt)) return false;
|
||||
postIds.add(evt.id);
|
||||
posts.push(evt);
|
||||
if (!isEventMode && posts.length > MAX_STORED_POSTS + 50) prunePostsArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user