mirror of
https://github.com/hoornet/vega.git
synced 2026-05-06 20:29:12 -07:00
Bump to v0.4.0 — Phase 3: image lightbox, bookmarks, discover, language filter, UI polish
This commit is contained in:
71
src/lib/language.ts
Normal file
71
src/lib/language.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Unicode script detection for feed filtering
|
||||
|
||||
const SCRIPT_RANGES: [string, RegExp][] = [
|
||||
["Latin", /[\u0041-\u024F\u1E00-\u1EFF]/],
|
||||
["CJK", /[\u2E80-\u2FFF\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]|[\uD840-\uD87F][\uDC00-\uDFFF]/],
|
||||
["Cyrillic", /[\u0400-\u04FF\u0500-\u052F]/],
|
||||
["Arabic", /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/],
|
||||
["Devanagari", /[\u0900-\u097F]/],
|
||||
["Thai", /[\u0E00-\u0E7F]/],
|
||||
["Korean", /[\uAC00-\uD7AF\u1100-\u11FF]/],
|
||||
["Hebrew", /[\u0590-\u05FF]/],
|
||||
["Greek", /[\u0370-\u03FF]/],
|
||||
["Georgian", /[\u10A0-\u10FF]/],
|
||||
["Armenian", /[\u0530-\u058F]/],
|
||||
];
|
||||
|
||||
export function detectScript(text: string): string {
|
||||
// Strip URLs, mentions, hashtags to avoid noise
|
||||
const cleaned = text
|
||||
.replace(/https?:\/\/\S+/g, "")
|
||||
.replace(/nostr:\S+/g, "")
|
||||
.replace(/#\w+/g, "")
|
||||
.trim();
|
||||
|
||||
if (!cleaned) return "Unknown";
|
||||
|
||||
// Count characters per script
|
||||
const counts = new Map<string, number>();
|
||||
for (const char of cleaned) {
|
||||
for (const [name, regex] of SCRIPT_RANGES) {
|
||||
if (regex.test(char)) {
|
||||
counts.set(name, (counts.get(name) ?? 0) + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (counts.size === 0) return "Unknown";
|
||||
|
||||
// Return dominant script
|
||||
let maxScript = "Unknown";
|
||||
let maxCount = 0;
|
||||
for (const [script, count] of counts) {
|
||||
if (count > maxCount) {
|
||||
maxScript = script;
|
||||
maxCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return maxScript;
|
||||
}
|
||||
|
||||
// Check NIP-32 language tags on an event
|
||||
export function getEventLanguageTag(tags: string[][]): string | null {
|
||||
const langTag = tags.find(
|
||||
(t) => t[0] === "l" && t[2] === "ISO-639-1"
|
||||
);
|
||||
return langTag?.[1] ?? null;
|
||||
}
|
||||
|
||||
export const FILTER_SCRIPTS = [
|
||||
"Latin",
|
||||
"CJK",
|
||||
"Cyrillic",
|
||||
"Arabic",
|
||||
"Devanagari",
|
||||
"Thai",
|
||||
"Korean",
|
||||
"Hebrew",
|
||||
"Greek",
|
||||
] as const;
|
||||
@@ -445,6 +445,29 @@ export async function fetchZapsSent(pubkey: string, limit = 50): Promise<NDKEven
|
||||
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
||||
}
|
||||
|
||||
// ── Bookmarks (NIP-51 kind 10003) ────────────────────────────────────────────
|
||||
|
||||
export async function fetchBookmarkList(pubkey: string): Promise<string[]> {
|
||||
const instance = getNDK();
|
||||
const filter: NDKFilter = { kinds: [10003 as NDKKind], authors: [pubkey], limit: 1 };
|
||||
const events = await instance.fetchEvents(filter, {
|
||||
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
||||
});
|
||||
if (events.size === 0) return [];
|
||||
const event = Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))[0];
|
||||
return event.tags.filter((t) => t[0] === "e" && t[1]).map((t) => t[1]);
|
||||
}
|
||||
|
||||
export async function publishBookmarkList(eventIds: string[]): Promise<void> {
|
||||
const instance = getNDK();
|
||||
if (!instance.signer) return;
|
||||
const event = new NDKEvent(instance);
|
||||
event.kind = 10003 as NDKKind;
|
||||
event.content = "";
|
||||
event.tags = eventIds.map((id) => ["e", id]);
|
||||
await event.publish();
|
||||
}
|
||||
|
||||
export async function fetchMuteList(pubkey: string): Promise<string[]> {
|
||||
const instance = getNDK();
|
||||
const filter: NDKFilter = { kinds: [10000 as NDKKind], authors: [pubkey], limit: 1 };
|
||||
@@ -520,6 +543,44 @@ export async function fetchUserNotesNIP65(pubkey: string, limit = 30): Promise<N
|
||||
|
||||
// ── Notifications (mentions) ──────────────────────────────────────────────────
|
||||
|
||||
// ── Follow Suggestions (follows-of-follows) ─────────────────────────────────
|
||||
|
||||
export async function fetchFollowSuggestions(myFollows: string[]): Promise<{ pubkey: string; mutualCount: number }[]> {
|
||||
if (myFollows.length === 0) return [];
|
||||
const instance = getNDK();
|
||||
// Fetch contact lists (kind 3) from our follows
|
||||
const batchSize = 20;
|
||||
const allContactEvents: NDKEvent[] = [];
|
||||
for (let i = 0; i < myFollows.length; i += batchSize) {
|
||||
const batch = myFollows.slice(i, i + batchSize);
|
||||
const filter: NDKFilter = { kinds: [3 as NDKKind], authors: batch, limit: batch.length };
|
||||
const events = await instance.fetchEvents(filter, {
|
||||
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
||||
});
|
||||
allContactEvents.push(...Array.from(events));
|
||||
}
|
||||
|
||||
// Count how many of our follows follow each pubkey
|
||||
const myFollowSet = new Set(myFollows);
|
||||
const counts = new Map<string, number>();
|
||||
for (const event of allContactEvents) {
|
||||
const pubkeys = event.tags.filter((t) => t[0] === "p" && t[1]).map((t) => t[1]);
|
||||
for (const pk of pubkeys) {
|
||||
if (myFollowSet.has(pk)) continue; // already following
|
||||
counts.set(pk, (counts.get(pk) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove self
|
||||
const myPubkey = (await instance.signer?.user())?.pubkey;
|
||||
if (myPubkey) counts.delete(myPubkey);
|
||||
|
||||
return Array.from(counts.entries())
|
||||
.map(([pubkey, mutualCount]) => ({ pubkey, mutualCount }))
|
||||
.sort((a, b) => b.mutualCount - a.mutualCount)
|
||||
.slice(0, 30);
|
||||
}
|
||||
|
||||
export async function fetchMentions(pubkey: string, since: number, limit = 50): Promise<NDKEvent[]> {
|
||||
const instance = getNDK();
|
||||
const events = await instance.fetchEvents(
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishRepost, publishQuote, publishReply, publishContactList, fetchReactionCount, fetchZapCount, fetchNoteById, fetchUserNotes, fetchProfile, fetchArticle, fetchAuthorArticles, fetchZapsReceived, fetchZapsSent, fetchDMConversations, fetchDMThread, sendDM, decryptDM, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchMentions } from "./client";
|
||||
export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishRepost, publishQuote, publishReply, publishContactList, fetchReactionCount, 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 type { UserRelayList } from "./client";
|
||||
|
||||
Reference in New Issue
Block a user