mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 04:39:12 -07:00
Grouped emoji reactions, npub search, notification fix, dev logger
- Emoji reactions now display as grouped pills (❤️5 🤙3 🔥2) instead of a single aggregated count. Multi-reaction per note supported. - Throttled reaction fetch queue (max 4 concurrent) prevents relay overload. - Searching a bare npub/nprofile navigates directly to that profile. - Notification poller waits for relay connection before first fetch, fixing empty results on startup. - Dev-only debug logger (src/lib/debug.ts) — silent in production builds.
This commit is contained in:
8
src/lib/debug.ts
Normal file
8
src/lib/debug.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/** Dev-only logger — all output is stripped in production builds. */
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
||||
export const debug = {
|
||||
log: (...args: unknown[]) => { if (isDev) console.log("[Wrystr]", ...args); },
|
||||
warn: (...args: unknown[]) => { if (isDev) console.warn("[Wrystr]", ...args); },
|
||||
error: (...args: unknown[]) => { if (isDev) console.error("[Wrystr]", ...args); },
|
||||
};
|
||||
@@ -22,6 +22,45 @@ export async function fetchReactionCount(eventId: string): Promise<number> {
|
||||
return events.size;
|
||||
}
|
||||
|
||||
export interface GroupedReactions {
|
||||
groups: Map<string, number>;
|
||||
myReactions: Set<string>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** Normalize reaction content: "+" and empty → "❤️", ignore "-" */
|
||||
function normalizeEmoji(content: string): string | null {
|
||||
if (content === "-") return null; // downvote — ignore
|
||||
if (!content || content === "+") return "❤️";
|
||||
return content;
|
||||
}
|
||||
|
||||
/** Group reaction events by emoji. Pass myPubkey to track which emojis the user sent. */
|
||||
export function groupReactions(events: Iterable<NDKEvent>, myPubkey?: string): GroupedReactions {
|
||||
const groups = new Map<string, number>();
|
||||
const myReactions = new Set<string>();
|
||||
let total = 0;
|
||||
|
||||
for (const event of events) {
|
||||
const emoji = normalizeEmoji(event.content);
|
||||
if (!emoji) continue;
|
||||
groups.set(emoji, (groups.get(emoji) ?? 0) + 1);
|
||||
total++;
|
||||
if (myPubkey && event.pubkey === myPubkey) {
|
||||
myReactions.add(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
return { groups, myReactions, total };
|
||||
}
|
||||
|
||||
export async function fetchReactions(eventId: string, myPubkey?: string): Promise<GroupedReactions> {
|
||||
const instance = getNDK();
|
||||
const filter: NDKFilter = { kinds: [NDKKind.Reaction], "#e": [eventId] };
|
||||
const events = await fetchWithTimeout(instance, filter, SINGLE_TIMEOUT);
|
||||
return groupReactions(events, myPubkey);
|
||||
}
|
||||
|
||||
export async function fetchReplyCount(eventId: string): Promise<number> {
|
||||
const instance = getNDK();
|
||||
const filter: NDKFilter = { kinds: [NDKKind.Text], "#e": [eventId] };
|
||||
@@ -47,11 +86,19 @@ export async function fetchZapCount(eventId: string): Promise<{ count: number; t
|
||||
return { count: events.size, totalSats };
|
||||
}
|
||||
|
||||
export async function fetchBatchEngagement(eventIds: string[]): Promise<Map<string, { reactions: number; replies: number; zapSats: number }>> {
|
||||
export interface BatchEngagement {
|
||||
reactions: number;
|
||||
replies: number;
|
||||
zapSats: number;
|
||||
reactionGroups: Map<string, number>;
|
||||
myReactions: Set<string>;
|
||||
}
|
||||
|
||||
export async function fetchBatchEngagement(eventIds: string[], myPubkey?: string): Promise<Map<string, BatchEngagement>> {
|
||||
const instance = getNDK();
|
||||
const result = new Map<string, { reactions: number; replies: number; zapSats: number }>();
|
||||
const result = new Map<string, BatchEngagement>();
|
||||
for (const id of eventIds) {
|
||||
result.set(id, { reactions: 0, replies: 0, zapSats: 0 });
|
||||
result.set(id, { reactions: 0, replies: 0, zapSats: 0, reactionGroups: new Map(), myReactions: new Set() });
|
||||
}
|
||||
|
||||
// Batch in chunks to avoid oversized filters
|
||||
@@ -65,13 +112,23 @@ export async function fetchBatchEngagement(eventIds: string[]): Promise<Map<stri
|
||||
fetchWithTimeout(instance, { kinds: [NDKKind.Text], "#e": chunk }, FEED_TIMEOUT),
|
||||
fetchWithTimeout(instance, { kinds: [NDKKind.Zap], "#e": chunk }, FEED_TIMEOUT),
|
||||
]),
|
||||
FEED_TIMEOUT + 2000, // outer timeout slightly longer than inner
|
||||
FEED_TIMEOUT + 2000,
|
||||
[new Set<NDKEvent>(), new Set<NDKEvent>(), new Set<NDKEvent>()],
|
||||
);
|
||||
|
||||
for (const event of reactions) {
|
||||
const eTag = event.tags.find((t) => t[0] === "e")?.[1];
|
||||
if (eTag && result.has(eTag)) result.get(eTag)!.reactions++;
|
||||
if (eTag && result.has(eTag)) {
|
||||
const entry = result.get(eTag)!;
|
||||
const emoji = normalizeEmoji(event.content);
|
||||
if (emoji) {
|
||||
entry.reactions++;
|
||||
entry.reactionGroups.set(emoji, (entry.reactionGroups.get(emoji) ?? 0) + 1);
|
||||
if (myPubkey && event.pubkey === myPubkey) {
|
||||
entry.myReactions.add(emoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of replies) {
|
||||
|
||||
@@ -2,7 +2,8 @@ export { getNDK, getNDKUptimeMs, connectToRelays, ensureConnected, resetNDK, get
|
||||
export { fetchGlobalFeed, fetchFollowFeed, fetchUserNotes, fetchUserNotesNIP65, fetchNoteById, fetchReplies, publishNote, publishReply, publishRepost, publishQuote, fetchHashtagFeed, fetchThreadEvents, fetchAncestors } from "./notes";
|
||||
export { publishProfile, publishContactList, fetchProfile, fetchFollowSuggestions, fetchMentions, fetchFollowers, fetchNewFollowers } from "./social";
|
||||
export { publishArticle, fetchArticle, fetchAuthorArticles, fetchArticleFeed, searchArticles, fetchByAddr } from "./articles";
|
||||
export { publishReaction, fetchReactionCount, fetchReplyCount, fetchZapCount, fetchBatchEngagement, fetchZapsReceived, fetchZapsSent } from "./engagement";
|
||||
export { publishReaction, fetchReactionCount, fetchReplyCount, fetchZapCount, fetchReactions, groupReactions, fetchBatchEngagement, fetchZapsReceived, fetchZapsSent } from "./engagement";
|
||||
export type { GroupedReactions, BatchEngagement } from "./engagement";
|
||||
export { fetchDMConversations, fetchDMThread, sendDM, decryptDM } from "./dms";
|
||||
export { fetchBookmarkList, publishBookmarkList, fetchBookmarkListFull, publishBookmarkListFull } from "./bookmarks";
|
||||
export { fetchMuteList, publishMuteList } from "./muting";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fetchMentions, fetchZapsReceived, fetchNewFollowers, fetchProfile } from "./nostr";
|
||||
import { fetchMentions, fetchZapsReceived, fetchNewFollowers, fetchProfile, ensureConnected } from "./nostr";
|
||||
import { notifyMention, notifyZap, notifyFollower } from "./notifications";
|
||||
import { useNotificationsStore } from "../stores/notifications";
|
||||
import { debug } from "./debug";
|
||||
|
||||
const POLL_INTERVAL = 60_000; // 60 seconds
|
||||
const POLL_TS_KEY = "wrystr_notif_poll_ts";
|
||||
@@ -42,6 +43,15 @@ async function getProfileName(pubkey: string): Promise<string> {
|
||||
}
|
||||
|
||||
async function pollOnce(pubkey: string) {
|
||||
// Skip polling if no relays are connected — avoids empty results
|
||||
try {
|
||||
const connected = await ensureConnected();
|
||||
if (!connected) {
|
||||
debug.warn("notif:poll skipped — no relays connected");
|
||||
return;
|
||||
}
|
||||
} catch { return; }
|
||||
|
||||
const ts = loadPollTimestamps();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -109,10 +119,17 @@ async function pollOnce(pubkey: string) {
|
||||
|
||||
export function startNotificationPoller(pubkey: string) {
|
||||
stopNotificationPoller();
|
||||
// Fetch notification counts immediately (before full poll)
|
||||
useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {});
|
||||
// Run first full poll after a short delay (let relays connect)
|
||||
setTimeout(() => pollOnce(pubkey).catch(() => {}), 5000);
|
||||
// Wait for relay connection before first fetch — avoids empty results on startup
|
||||
(async () => {
|
||||
try {
|
||||
const connected = await ensureConnected();
|
||||
debug.log("notif:poller ensureConnected →", connected);
|
||||
} catch { /* continue anyway */ }
|
||||
debug.log("notif:poller initial fetch for", pubkey.slice(0, 8));
|
||||
useNotificationsStore.getState().fetchNotifications(pubkey).catch(() => {});
|
||||
})();
|
||||
// Run first full poll after a longer delay (give relays more time)
|
||||
setTimeout(() => pollOnce(pubkey).catch(() => {}), 8000);
|
||||
intervalId = setInterval(() => pollOnce(pubkey).catch(() => {}), POLL_INTERVAL);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user