300 lines
7.5 KiB
TypeScript
300 lines
7.5 KiB
TypeScript
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<string, NDKSubscription, void>({
|
|
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<false>[] = [];
|
|
|
|
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<string, string[]>();
|
|
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,
|
|
};
|
|
}
|