import { NDKEvent, NDKFilter, NDKKind, NDKSubscription, NDKTag, } from '@nostr-dev-kit/ndk'; import { open } from '@tauri-apps/plugin-dialog'; import { readBinaryFile } from '@tauri-apps/plugin-fs'; import { fetch } from '@tauri-apps/plugin-http'; import { LRUCache } from 'lru-cache'; import { NostrEventExt } from 'nostr-fetch'; import { useMemo } from 'react'; import { useNDK } from '@libs/ndk/provider'; import { useStorage } from '@libs/storage/provider'; import { nHoursAgo } from '@utils/date'; import { getMultipleRandom } from '@utils/transform'; import { NDKEventWithReplies } from '@utils/types'; export function useNostr() { const { db } = useStorage(); const { ndk, relayUrls, fetcher } = useNDK(); const subManager = useMemo( () => new LRUCache({ max: 4, dispose: (sub) => sub.stop(), }), [] ); const sub = async ( filter: NDKFilter, callback: (event: NDKEvent) => void, groupable?: boolean, subKey?: string ) => { if (!ndk) throw new Error('NDK instance not found'); const key = subKey ?? JSON.stringify(filter); if (!subManager.get(key)) { const subEvent = ndk.subscribe(filter, { closeOnEose: false, groupable: groupable ?? true, }); subEvent.addListener('event', (event: NDKEvent) => { callback(event); }); subManager.set(JSON.stringify(filter), subEvent); console.log('sub: ', key); } }; const getEventThread = (tags: NDKTag[]) => { let rootEventId: string = null; let replyEventId: string = null; const events = tags.filter((el) => el[0] === 'e'); if (!events.length) return null; if (events.length === 1) return { rootEventId: events[0][1], replyEventId: null, }; if (events.length > 1) { rootEventId = events.find((el) => el[3] === 'root')?.[1]; replyEventId = events.find((el) => el[3] === 'reply')?.[1]; if (!rootEventId && !replyEventId) { rootEventId = events[0][1]; replyEventId = events[1][1]; } } return { rootEventId, replyEventId, }; }; const getAllActivities = async (limit?: number) => { try { const events = await ndk.fetchEvents({ kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], '#p': [db.account.pubkey], limit: limit ?? 50, }); return [...events]; } catch (e) { console.error('Error fetching activities', e); } }; const fetchNIP04Messages = async (sender: string) => { let senderMessages: NostrEventExt[] = []; if (sender !== db.account.pubkey) { senderMessages = await fetcher.fetchAllEvents( relayUrls, { kinds: [NDKKind.EncryptedDirectMessage], authors: [sender], '#p': [db.account.pubkey], }, { since: 0 } ); } const userMessages = await fetcher.fetchAllEvents( relayUrls, { kinds: [NDKKind.EncryptedDirectMessage], authors: [db.account.pubkey], '#p': [sender], }, { since: 0 } ); const all = [...senderMessages, ...userMessages].sort( (a, b) => a.created_at - b.created_at ); return all as unknown as NDKEvent[]; }; const fetchAllReplies = async (id: string, data?: NDKEventWithReplies[]) => { let events = data || null; if (!data) { events = (await fetcher.fetchAllEvents( relayUrls, { kinds: [NDKKind.Text], '#e': [id], }, { since: 0 }, { sort: true } )) as unknown as NDKEventWithReplies[]; } if (events.length > 0) { const replies = new Set(); events.forEach((event) => { const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id); if (tags.length > 0) { tags.forEach((tag) => { const rootIndex = events.findIndex((el) => el.id === tag[1]); if (rootIndex !== -1) { const rootEvent = events[rootIndex]; if (rootEvent && rootEvent.replies) { rootEvent.replies.push(event); } else { rootEvent.replies = [event]; } replies.add(event.id); } }); } }); const cleanEvents = events.filter((ev) => !replies.has(ev.id)); return cleanEvents; } return events; }; const getAllNIP04Chats = async () => { const events = await fetcher.fetchAllEvents( relayUrls, { kinds: [NDKKind.EncryptedDirectMessage], '#p': [db.account.pubkey], }, { since: 0 } ); const dedup: NDKEvent[] = Object.values( events.reduce((ev, { id, content, pubkey, created_at, tags }) => { if (ev[pubkey]) { if (ev[pubkey].created_at < created_at) { ev[pubkey] = { id, content, pubkey, created_at, tags }; } } else { ev[pubkey] = { id, content, pubkey, created_at, tags }; } return ev; }, {}) ); return dedup; }; const getContactsByPubkey = async (pubkey: string) => { const user = ndk.getUser({ pubkey: pubkey }); const follows = [...(await user.follows())].map((user) => user.hexpubkey); return getMultipleRandom([...follows], 10); }; const getEventsByPubkey = async (pubkey: string) => { const events = await fetcher.fetchAllEvents( relayUrls, { authors: [pubkey], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article] }, { since: nHoursAgo(24) }, { sort: true } ); return events as unknown as NDKEvent[]; }; const getAllRelaysByUsers = async () => { const relayMap = new Map(); const relayEvents = fetcher.fetchLatestEventsPerAuthor( { authors: db.account.contacts, relayUrls: relayUrls, }, { kinds: [NDKKind.RelayList] }, 5 ); for await (const { author, events } of relayEvents) { if (events[0]) { events[0].tags.forEach((tag) => { const users = relayMap.get(tag[1]); if (!users) return relayMap.set(tag[1], [author]); return users.push(author); }); } } return relayMap; }; const createZap = async (event: NDKEvent, amount: number, message?: string) => { // @ts-expect-error, NostrEvent to NDKEvent const ndkEvent = new NDKEvent(ndk, event); const res = await ndkEvent.zap(amount, message ?? 'zap from lume'); return res; }; const upload = async (ext: string[] = []) => { const defaultExts = ['png', 'jpeg', 'jpg', 'gif'].concat(ext); const selected = await open({ multiple: false, filters: [ { name: 'Image', extensions: defaultExts, }, ], }); if (!selected) return null; const file = await readBinaryFile(selected.path); const blob = new Blob([file]); const data = new FormData(); data.append('fileToUpload', blob); data.append('submit', 'Upload Image'); const res = await fetch('https://nostr.build/api/v2/upload/files', { method: 'POST', body: data, }); if (!res.ok) return null; const json = await res.json(); const content = json.data[0]; return content.url as string; }; return { sub, getEventThread, getAllNIP04Chats, getContactsByPubkey, getEventsByPubkey, getAllRelaysByUsers, getAllActivities, fetchNIP04Messages, fetchAllReplies, createZap, upload, }; }