Bump to v0.6.0 — article discovery, search, profile tab, reader polish

Article discovery feed with Latest/Following tabs, article search
(NIP-50 + hashtag for kind 30023), Notes/Articles tab on profiles,
reading time + bookmark + like buttons on article reader. Event
passed directly from card to reader to avoid relay re-fetch failures.
This commit is contained in:
Jure
2026-03-17 21:47:24 +01:00
parent 8ce1d43d2d
commit ef189932e6
20 changed files with 1647 additions and 76 deletions

View File

@@ -325,6 +325,61 @@ export async function fetchReplyCount(eventId: string): Promise<number> {
return events.size;
}
export async function fetchBatchEngagement(eventIds: string[]): Promise<Map<string, { reactions: number; replies: number; zapSats: number }>> {
const instance = getNDK();
const result = new Map<string, { reactions: number; replies: number; zapSats: number }>();
for (const id of eventIds) {
result.set(id, { reactions: 0, replies: 0, zapSats: 0 });
}
// Batch in chunks to avoid oversized filters
const chunkSize = 50;
for (let i = 0; i < eventIds.length; i += chunkSize) {
const chunk = eventIds.slice(i, i + chunkSize);
const [reactions, replies, zaps] = await Promise.all([
instance.fetchEvents(
{ kinds: [NDKKind.Reaction], "#e": chunk },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }
),
instance.fetchEvents(
{ kinds: [NDKKind.Text], "#e": chunk },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }
),
instance.fetchEvents(
{ kinds: [NDKKind.Zap], "#e": chunk },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }
),
]);
for (const event of reactions) {
const eTag = event.tags.find((t) => t[0] === "e")?.[1];
if (eTag && result.has(eTag)) result.get(eTag)!.reactions++;
}
for (const event of replies) {
const eTag = event.tags.find((t) => t[0] === "e")?.[1];
if (eTag && result.has(eTag)) result.get(eTag)!.replies++;
}
for (const event of zaps) {
const eTag = event.tags.find((t) => t[0] === "e")?.[1];
if (eTag && result.has(eTag)) {
const desc = event.tags.find((t) => t[0] === "description")?.[1];
if (desc) {
try {
const zapReq = JSON.parse(desc) as { tags?: string[][] };
const amountTag = zapReq.tags?.find((t) => t[0] === "amount");
if (amountTag?.[1]) result.get(eTag)!.zapSats += Math.round(parseInt(amountTag[1]) / 1000);
} catch { /* malformed */ }
}
}
}
}
return result;
}
export async function fetchReactionCount(eventId: string): Promise<number> {
const instance = getNDK();
const filter: NDKFilter = {
@@ -497,6 +552,28 @@ export async function fetchAuthorArticles(pubkey: string, limit = 20): Promise<N
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function fetchArticleFeed(limit = 40, authors?: string[]): Promise<NDKEvent[]> {
const instance = getNDK();
const filter: NDKFilter = { kinds: [NDKKind.Article], limit };
if (authors && authors.length > 0) filter.authors = authors;
const events = await instance.fetchEvents(filter, {
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
});
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function searchArticles(query: string, limit = 30): Promise<NDKEvent[]> {
const instance = getNDK();
const isHashtag = query.startsWith("#");
const filter: NDKFilter & { search?: string } = isHashtag
? { kinds: [NDKKind.Article], "#t": [query.slice(1).toLowerCase()], limit }
: { kinds: [NDKKind.Article], search: query, limit };
const events = await instance.fetchEvents(filter, {
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
});
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
}
export async function fetchZapsReceived(pubkey: string, limit = 50): Promise<NDKEvent[]> {
const instance = getNDK();
const filter: NDKFilter = { kinds: [NDKKind.Zap], "#p": [pubkey], limit };

View File

@@ -1,2 +1,2 @@
export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishRepost, publishQuote, publishReply, publishContactList, fetchReactionCount, fetchReplyCount, fetchZapCount, fetchNoteById, fetchUserNotes, fetchProfile, fetchArticle, fetchAuthorArticles, fetchZapsReceived, fetchZapsSent, fetchDMConversations, fetchDMThread, sendDM, decryptDM, fetchBookmarkList, publishBookmarkList, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchFollowSuggestions, fetchMentions } from "./client";
export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishRepost, publishQuote, publishReply, publishContactList, fetchBatchEngagement, fetchReactionCount, fetchReplyCount, fetchZapCount, fetchNoteById, fetchUserNotes, fetchProfile, fetchArticle, fetchAuthorArticles, fetchArticleFeed, searchArticles, fetchZapsReceived, fetchZapsSent, fetchDMConversations, fetchDMThread, sendDM, decryptDM, fetchBookmarkList, publishBookmarkList, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchFollowSuggestions, fetchMentions } from "./client";
export type { UserRelayList } from "./client";