Bump to v0.8.0 — polish, portability, discovery

Profile banner polish (hero height, click-to-lightbox, avatar overlap),
data export (bookmarks/follows/relays as JSON), relay recommendations
(discover from follows' NIP-65 lists), reading list tracking (read/unread
on bookmarked articles with sidebar badge), trending hashtags (clickable
pills on search idle screen). Updated CLAUDE.md and release notes.
This commit is contained in:
Jure
2026-03-19 19:54:14 +01:00
parent d257075023
commit d993ae1131
16 changed files with 390 additions and 23 deletions

View File

@@ -799,6 +799,73 @@ export async function resolveNip05(identifier: string): Promise<string | null> {
}
}
// ── Relay Recommendations ─────────────────────────────────────────────────────
export async function fetchRelayRecommendations(
follows: string[],
ownRelays: string[],
sampleSize = 30
): Promise<{ url: string; count: number }[]> {
if (follows.length === 0) return [];
// Sample random follows to avoid hammering relays
const shuffled = [...follows].sort(() => Math.random() - 0.5);
const sample = shuffled.slice(0, sampleSize);
const results = await Promise.allSettled(
sample.map((pk) => fetchUserRelayList(pk))
);
const ownSet = new Set(ownRelays.map((u) => u.replace(/\/$/, "")));
const tally = new Map<string, number>();
for (const result of results) {
if (result.status !== "fulfilled") continue;
const allUrls = Array.from(new Set([...result.value.read, ...result.value.write]));
for (const url of allUrls) {
const normalized = url.replace(/\/$/, "");
if (ownSet.has(normalized)) continue;
tally.set(normalized, (tally.get(normalized) ?? 0) + 1);
}
}
return Array.from(tally.entries())
.map(([url, count]) => ({ url, count }))
.filter((r) => r.count >= 2)
.sort((a, b) => b.count - a.count)
.slice(0, 8);
}
// ── Trending Hashtags ─────────────────────────────────────────────────────────
export async function fetchTrendingHashtags(limit = 15): Promise<{ tag: string; count: number }[]> {
const instance = getNDK();
const since = Math.floor(Date.now() / 1000) - 24 * 60 * 60;
const filter: NDKFilter = {
kinds: [NDKKind.Text],
since,
limit: 500,
};
const events = await instance.fetchEvents(filter, {
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
});
const counts = new Map<string, number>();
for (const event of events) {
for (const tag of event.tags) {
if (tag[0] !== "t" || !tag[1]) continue;
const normalized = tag[1].toLowerCase().trim();
if (normalized.length === 0) continue;
counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
}
}
return Array.from(counts.entries())
.filter(([, count]) => count >= 2)
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => b.count - a.count)
.slice(0, limit);
}
// ── Advanced Search ───────────────────────────────────────────────────────────
export interface AdvancedSearchResults {

View File

@@ -1,2 +1,2 @@
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, fetchBookmarkListFull, publishBookmarkListFull, fetchByAddr, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchFollowSuggestions, fetchMentions, resolveNip05, advancedSearch } 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, fetchBookmarkListFull, publishBookmarkListFull, fetchByAddr, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchFollowSuggestions, fetchMentions, resolveNip05, advancedSearch, fetchRelayRecommendations, fetchTrendingHashtags } from "./client";
export type { UserRelayList, AdvancedSearchResults } from "./client";