Add Like/Zap/Nutzap/Comment/Quote to notification dropdown menu via hidden interaction bar
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.87",
|
||||
"VERSION_NUMBER": "0.7.87",
|
||||
"BUILD_DATE": "2026-07-01T13:33:29.318Z"
|
||||
"VERSION": "v0.7.88",
|
||||
"VERSION_NUMBER": "0.7.88",
|
||||
"BUILD_DATE": "2026-07-01T19:56:16.258Z"
|
||||
}
|
||||
|
||||
@@ -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,9 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
const unreadNotificationsById = new Map();
|
||||
const timeElements = new Map();
|
||||
const postImageCache = new Map();
|
||||
const targetEventCache = new Map();
|
||||
let interactions = null;
|
||||
let notifInteractionsSubId = null;
|
||||
let visibleNotificationsCount = NOTIFICATIONS_PAGE_SIZE;
|
||||
|
||||
const profileCache = createProfileCache({
|
||||
@@ -443,6 +478,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 +1101,62 @@ 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 hidden interaction bar appended to
|
||||
// the row. Menu items trigger clicks on the corresponding buttons
|
||||
// in that hidden bar.
|
||||
if (item.targetEventId && interactions) {
|
||||
const targetPubkey = item.targetPubkey || currentPubkey;
|
||||
const targetEventData = { id: item.targetEventId, pubkey: targetPubkey };
|
||||
|
||||
const hiddenBar = interactions.renderInteractionBar(
|
||||
item.targetEventId,
|
||||
targetEventData,
|
||||
{ currentPubkey }
|
||||
);
|
||||
hiddenBar.style.display = 'none';
|
||||
hiddenBar.dataset.notifHiddenBar = item.targetEventId;
|
||||
row.appendChild(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 +1177,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));
|
||||
@@ -1073,6 +1220,7 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
updateHint();
|
||||
refreshTimes();
|
||||
renderNotificationFilters();
|
||||
wireNotificationInteractions();
|
||||
}
|
||||
|
||||
function prependNotificationRow(item) {
|
||||
@@ -1122,6 +1270,10 @@ import { createProfileCache } from './js/profile-cache.mjs';
|
||||
updateHint();
|
||||
refreshTimes();
|
||||
|
||||
if (item.targetEventId && interactions) {
|
||||
interactions.fetchExistingInteractions([item.targetEventId], { currentPubkey });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1204,7 +1356,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 +1403,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();
|
||||
}
|
||||
@@ -1364,6 +1558,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) {
|
||||
|
||||
Reference in New Issue
Block a user