Compare commits

...

6 Commits

4 changed files with 222 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.87",
"VERSION_NUMBER": "0.7.87",
"BUILD_DATE": "2026-07-01T13:33:29.318Z"
"VERSION": "v0.7.93",
"VERSION_NUMBER": "0.7.93",
"BUILD_DATE": "2026-07-03T10:29:34.780Z"
}

View File

@@ -1049,9 +1049,8 @@ function pruneSeenEventIds(seenEventIds) {
function wireSubscriptionHandlers(subId, state, filtersForCache) {
if (!state?.sub) return;
state.sub.on('event', async (event) => {
state.sub.on('event', (event) => {
if (state.seenEventIds.has(event.id)) {
console.log('[Worker] Dedup dropped event for sub:', subId, 'id:', event.id);
return;
}
@@ -1063,14 +1062,7 @@ function wireSubscriptionHandlers(subId, state, filtersForCache) {
state.lastEventCreatedAt = Math.max(Number(state.lastEventCreatedAt || 0), createdAt);
}
console.log('[Worker] Event received for sub:', subId, 'kind:', event.kind, 'id:', event.id);
if (isWorkerEventMuted(event)) {
console.log('[Worker][mute] Filtered muted event for sub:', subId, {
id: event?.id || null,
kind: event?.kind || null,
pubkey: event?.pubkey || null,
});
return;
}
@@ -1079,14 +1071,14 @@ function wireSubscriptionHandlers(subId, state, filtersForCache) {
trackRelayRead(event.relay.url);
}
// Cache the event to IndexedDB
// Cache the event to IndexedDB — fire-and-forget (NOT awaited).
// Awaiting serializes all incoming events through IndexedDB I/O,
// which blocks the worker's event loop and starves port.onmessage
// handlers (getRelayData, getRelayStats, etc.), causing 15s timeouts
// and relay indicators disappearing over extended sessions.
if (ndk.cacheAdapter && typeof ndk.cacheAdapter.setEvent === 'function') {
try {
await ndk.cacheAdapter.setEvent(event, filtersForCache, event.relay);
console.log('[Worker] Cached event:', event.id, 'from relay:', event.relay?.url);
} catch (error) {
console.error('[Worker] Failed to cache event:', error);
}
void ndk.cacheAdapter.setEvent(event, filtersForCache, event.relay)
.catch(error => console.error('[Worker] Failed to cache event:', error));
}
broadcast({

View File

@@ -173,6 +173,32 @@
color: var(--accent-color);
}
.notif-menu-item {
width: 100%;
text-align: left;
border: var(--button-border-width) solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-color);
padding: 6px 8px;
cursor: pointer;
font: inherit;
font-size: 90%;
margin-bottom: 2px;
}
.notif-menu-item:hover {
color: var(--button-hover-color);
border-color: var(--button-hover-color);
}
.notif-menu-divider {
height: 1px;
background: var(--muted-color);
margin: 4px 0;
border: none;
}
.svgBtn {
fill: none;
stroke: var(--button-color);
@@ -377,7 +403,13 @@
publishEvent,
ensureMuteListLoaded,
isEventMuted,
addMute
addMute,
walletPayInvoice,
walletSendNutzap,
walletFetchMintList,
walletGetMints,
walletGetBalance,
getRelayData
} from './js/init-ndk.mjs';
import { HamburgerMorphing } from './hamburger_morphing/hamburger.mjs';
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
@@ -385,7 +417,7 @@
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
import { createProfileCache } from './js/profile-cache.mjs';
import { formatTimeAgo } from './js/post-interactions.mjs';
import { formatTimeAgo, initInteractions } from './js/post-interactions.mjs';
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
@@ -424,6 +456,10 @@ import { createProfileCache } from './js/profile-cache.mjs';
const unreadNotificationsById = new Map();
const timeElements = new Map();
const postImageCache = new Map();
const targetEventCache = new Map();
const detachedInteractionBars = new Map();
let interactions = null;
let notifInteractionsSubId = null;
let visibleNotificationsCount = NOTIFICATIONS_PAGE_SIZE;
const profileCache = createProfileCache({
@@ -443,6 +479,40 @@ import { createProfileCache } from './js/profile-cache.mjs';
const divFooterRight = document.getElementById('divFooterRight');
const divNotificationFiltersList = document.getElementById('divNotificationFiltersList');
// Initialize the post-interactions module so notification dropdown menus
// can offer Like / Zap / Nutzap / Quote / Comment on the referenced post.
// The interaction bar itself is rendered hidden inside each notification
// row; menu items trigger clicks on its buttons.
interactions = initInteractions({
subscribe,
publishEvent,
getPubkey,
ndkFetchEvents,
queryCache,
fetchCachedProfile,
storeProfile,
walletPayInvoice,
walletSendNutzap,
walletFetchMintList,
walletGetMints,
walletGetBalance,
getRelayData,
getUserSettings,
patchUserSettings,
onCommentIntent: ({ postId }) => {
if (!postId) return false;
window.open('./post-feed.html?event=' + encodeURIComponent(postId), '_blank');
return true;
},
onMuteIntent: async ({ eventData }) => {
const targetPubkey = String(eventData?.pubkey || '').trim();
if (!targetPubkey || targetPubkey === currentPubkey) return;
const ok = window.confirm(`Block ${targetPubkey.slice(0, 8)}${targetPubkey.slice(-4)}?`);
if (!ok) return;
await blockActorPubkey(targetPubkey);
}
});
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
@@ -1032,6 +1102,63 @@ import { createProfileCache } from './js/profile-cache.mjs';
}
});
// Add interaction menu items (Like, Comment, Quote, Zap, Nutzap) when
// the notification references a target post. The actual interaction
// logic (publishing, zap dialogs, balance checks) is handled by the
// post-interactions module via a detached interaction bar (kept out of
// the DOM so it never shows up in the row). Menu items trigger clicks
// on the corresponding buttons in that detached bar.
if (item.targetEventId && interactions) {
const targetPubkey = item.targetPubkey || currentPubkey;
const targetEventData = { id: item.targetEventId, pubkey: targetPubkey };
let hiddenBar = detachedInteractionBars.get(item.id);
if (!hiddenBar) {
hiddenBar = interactions.renderInteractionBar(
item.targetEventId,
targetEventData,
{ currentPubkey }
);
detachedInteractionBars.set(item.id, hiddenBar);
}
const addMenuBtn = (label, selector) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'notif-menu-item';
btn.textContent = label;
btn.addEventListener('click', (ev) => {
ev.stopPropagation();
const target = hiddenBar.querySelector(selector);
if (target) target.click();
menuPanel.style.display = 'none';
});
menuPanel.appendChild(btn);
};
addMenuBtn('Like', '.interaction-item.like');
// Comment opens the post-feed thread page in a new tab.
const commentBtn = document.createElement('button');
commentBtn.type = 'button';
commentBtn.className = 'notif-menu-item';
commentBtn.textContent = 'Comment';
commentBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
window.open('./post-feed.html?event=' + encodeURIComponent(item.targetEventId), '_blank');
menuPanel.style.display = 'none';
});
menuPanel.appendChild(commentBtn);
addMenuBtn('Quote', '.interaction-item.quote');
addMenuBtn('Zap', '.interaction-item.zap');
addMenuBtn('Nutzap', '.interaction-item.nutzap');
const divider = document.createElement('hr');
divider.className = 'notif-menu-divider';
menuPanel.appendChild(divider);
}
menuPanel.appendChild(blockButton);
right.style.position = 'relative';
@@ -1052,6 +1179,28 @@ import { createProfileCache } from './js/profile-cache.mjs';
return row;
}
function wireNotificationInteractions() {
if (!interactions) return;
const targetIds = Array.from(new Set(
Array.from(notificationsById.values())
.map((item) => item.targetEventId)
.filter(Boolean)
));
if (targetIds.length === 0) return;
interactions.fetchExistingInteractions(targetIds, { currentPubkey });
if (notifInteractionsSubId) {
interactions.unsubscribeFromInteractions(notifInteractionsSubId);
notifInteractionsSubId = null;
}
notifInteractionsSubId = interactions.subscribeToInteractions(targetIds, {
currentPubkey
});
}
function renderNotifications() {
const items = Array.from(notificationsById.values())
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
@@ -1059,6 +1208,7 @@ import { createProfileCache } from './js/profile-cache.mjs';
divNotifications.innerHTML = '';
timeElements.clear();
detachedInteractionBars.clear();
for (const item of visibleItems) {
const row = buildNotificationRow(item);
@@ -1073,6 +1223,7 @@ import { createProfileCache } from './js/profile-cache.mjs';
updateHint();
refreshTimes();
renderNotificationFilters();
wireNotificationInteractions();
}
function prependNotificationRow(item) {
@@ -1122,6 +1273,10 @@ import { createProfileCache } from './js/profile-cache.mjs';
updateHint();
refreshTimes();
if (item.targetEventId && interactions) {
interactions.fetchExistingInteractions([item.targetEventId], { currentPubkey });
}
return true;
}
@@ -1204,7 +1359,46 @@ import { createProfileCache } from './js/profile-cache.mjs';
const targetEventId = getNotificationTargetEventId(evt);
notificationsById.set(evt.id, {
// Resolve the target event's pubkey so interaction handlers (like,
// zap, nutzap) target the correct post author. Most notifications
// reference the current user's own post, so default to currentPubkey
// and refine from cache/relays when available.
let targetPubkey = currentPubkey;
if (targetEventId) {
if (targetEventCache.has(targetEventId)) {
const cachedTarget = targetEventCache.get(targetEventId);
if (cachedTarget?.pubkey) targetPubkey = cachedTarget.pubkey;
} else {
try {
const cachedRefs = await queryCache({ ids: [targetEventId], limit: 1 });
const targetEvt = Array.isArray(cachedRefs)
? cachedRefs.find((e) => e?.id === targetEventId)
: null;
if (targetEvt) {
targetEventCache.set(targetEventId, targetEvt);
if (targetEvt.pubkey) targetPubkey = targetEvt.pubkey;
}
} catch (_) {}
// Async relay hydration (non-blocking).
void ndkFetchEvents({ ids: [targetEventId], limit: 1 })
.then((relayRefs) => {
const relayEvt = Array.isArray(relayRefs)
? relayRefs.find((e) => e?.id === targetEventId)
: null;
if (relayEvt) {
targetEventCache.set(targetEventId, relayEvt);
const existing = notificationsById.get(evt.id);
if (existing && existing.targetPubkey !== relayEvt.pubkey) {
existing.targetPubkey = relayEvt.pubkey;
}
}
})
.catch(() => {});
}
}
const notifItem = {
id: evt.id,
created_at: createdAt,
kind,
@@ -1212,11 +1406,14 @@ import { createProfileCache } from './js/profile-cache.mjs';
secondaryImage,
description,
targetEventId,
targetPubkey,
isUnread: createdAt > notificationsReadAt
});
};
notificationsById.set(evt.id, notifItem);
if (divNotifications.children.length > 0) {
prependNotificationRow(item);
prependNotificationRow(notifItem);
} else {
renderNotifications();
}
@@ -1308,6 +1505,7 @@ import { createProfileCache } from './js/profile-cache.mjs';
notificationsReadAt = readAt;
notificationsById.clear();
unreadNotificationsById.clear();
detachedInteractionBars.clear();
visibleNotificationsCount = NOTIFICATIONS_PAGE_SIZE;
renderNotifications();
@@ -1364,6 +1562,11 @@ import { createProfileCache } from './js/profile-cache.mjs';
notificationsSub = null;
}
if (notifInteractionsSubId && interactions) {
interactions.unsubscribeFromInteractions(notifInteractionsSubId);
notifInteractionsSubId = null;
}
disconnect();
if (window.NOSTR_LOGIN_LITE?.logout) {

View File

@@ -491,17 +491,9 @@ import { initPostCards } from './js/post-interactions2.mjs';
}
function handleComposerCommentIntent({ postId, postPubkey }) {
const postEl = document.querySelector(`.divPostItem[data-post-id="${postId}"]`);
if (!postEl) return false;
const entry = getOrCreateInlineComposer(postId, postEl);
if (!entry) return false;
entry.tags = [
['e', postId, '', 'reply'],
['p', postPubkey]
];
if (entry.instance?.show) entry.instance.show();
return focusInlineComposer(entry.hostEl, '');
if (!postId) return false;
window.open('./post-feed.html?event=' + encodeURIComponent(postId), '_blank');
return true;
}
function handleComposerQuoteIntent({ postId, postPubkey }) {