Merge pull request #130 from luminous-devs/feat/ark

Introduction Ark
This commit is contained in:
Ren Amamiya
2023-12-10 11:19:04 +07:00
committed by GitHub
101 changed files with 2743 additions and 3523 deletions

View File

@@ -1,45 +1,41 @@
import { NDKEvent, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk';
import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { AddressPointer } from 'nostr-tools/lib/types/nip19';
import { useNDK } from '@libs/ndk/provider';
import { useArk } from '@libs/ark';
export function useEvent(id: undefined | string, embed?: undefined | string) {
const { ndk } = useNDK();
const { ark } = useArk();
const { status, isFetching, isError, data } = useQuery({
queryKey: ['event', id],
queryFn: async () => {
let event: NDKEvent = undefined;
const naddr = id.startsWith('naddr')
? (nip19.decode(id).data as AddressPointer)
: null;
// return event refer from naddr
if (naddr) {
const rEvents = await ndk.fetchEvents({
kinds: [naddr.kind],
'#d': [naddr.identifier],
authors: [naddr.pubkey],
const events = await ark.getAllEvents({
filter: {
kinds: [naddr.kind],
'#d': [naddr.identifier],
authors: [naddr.pubkey],
},
});
const rEvent = [...rEvents].slice(-1)[0];
if (!rEvent) throw new Error('event not found');
return rEvent;
event = events.slice(-1)[0];
}
// return embed event (nostr.band api)
if (embed) {
const embedEvent: NostrEvent = JSON.parse(embed);
const ndkEvent = new NDKEvent(ndk, embedEvent);
return ndkEvent;
event = ark.createNDKEvent({ event: embedEvent });
}
// get event from relay
const event = await ndk.fetchEvent(id, {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
event = await ark.getEventById({ id });
if (!event)
throw new Error(`Cannot get event with ${id}, will be retry after 10 seconds`);

View File

@@ -1,299 +0,0 @@
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,
};
}

View File

@@ -1,11 +1,11 @@
import { NDKSubscriptionCacheUsage, NDKUserProfile } from '@nostr-dev-kit/ndk';
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { useNDK } from '@libs/ndk/provider';
import { useArk } from '@libs/ark';
export function useProfile(pubkey: string, embed?: string) {
const { ndk } = useNDK();
const { ark } = useArk();
const {
isLoading,
isError,
@@ -29,10 +29,7 @@ export function useProfile(pubkey: string, embed?: string) {
if (decoded.type === 'npub') hexstring = decoded.data;
}
const user = ndk.getUser({ pubkey: hexstring });
const profile = await user.fetchProfile({
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
});
const profile = await ark.getUserProfile({ pubkey: hexstring });
if (!profile)
throw new Error(

View File

@@ -1,46 +1,44 @@
import { NDKEvent, NDKKind, NDKRelayUrl, NDKTag } from '@nostr-dev-kit/ndk';
import { NDKKind, NDKRelayUrl, NDKTag } from '@nostr-dev-kit/ndk';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { useArk } from '@libs/ark';
export function useRelay() {
const { db } = useStorage();
const { ndk } = useNDK();
const { ark } = useArk();
const queryClient = useQueryClient();
const connectRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl, purpose?: 'read' | 'write' | undefined) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] });
await queryClient.cancelQueries({ queryKey: ['relays', ark.account.pubkey] });
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
'relays',
db.account.pubkey,
ark.account.pubkey,
]);
// create new relay list if not exist
if (!prevRelays) {
const newListEvent = new NDKEvent(ndk);
newListEvent.kind = NDKKind.RelayList;
newListEvent.tags = [['r', relay, purpose ?? '']];
await newListEvent.publish();
await ark.createEvent({
kind: NDKKind.RelayList,
tags: [['r', relay, purpose ?? '']],
publish: true,
});
}
// add relay to exist list
const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) return;
const event = new NDKEvent(ndk);
event.kind = NDKKind.RelayList;
event.tags = [...prevRelays, ['r', relay, purpose ?? '']];
await event.publish();
await ark.createEvent({
kind: NDKKind.RelayList,
tags: [...prevRelays, ['r', relay, purpose ?? '']],
publish: true,
});
// Optimistically update to the new value
queryClient.setQueryData(['relays', db.account.pubkey], (prev: NDKTag[]) => [
queryClient.setQueryData(['relays', ark.account.pubkey], (prev: NDKTag[]) => [
...prev,
['r', relay, purpose ?? ''],
]);
@@ -49,19 +47,19 @@ export function useRelay() {
return { prevRelays };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] });
queryClient.invalidateQueries({ queryKey: ['relays', ark.account.pubkey] });
},
});
const removeRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['relays', db.account.pubkey] });
await queryClient.cancelQueries({ queryKey: ['relays', ark.account.pubkey] });
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
'relays',
db.account.pubkey,
ark.account.pubkey,
]);
if (!prevRelays) return;
@@ -69,19 +67,20 @@ export function useRelay() {
const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) prevRelays.splice(index, 1);
const event = new NDKEvent(ndk);
event.kind = NDKKind.RelayList;
event.tags = prevRelays;
await event.publish();
await ark.createEvent({
kind: NDKKind.RelayList,
tags: prevRelays,
publish: true,
});
// Optimistically update to the new value
queryClient.setQueryData(['relays', db.account.pubkey], prevRelays);
queryClient.setQueryData(['relays', ark.account.pubkey], prevRelays);
// Return a context object with the snapshotted value
return { prevRelays };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['relays', db.account.pubkey] });
queryClient.invalidateQueries({ queryKey: ['relays', ark.account.pubkey] });
},
});

View File

@@ -4,7 +4,7 @@ import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import reactStringReplace from 'react-string-replace';
import { useStorage } from '@libs/storage/provider';
import { useArk } from '@libs/ark';
import {
Hashtag,
@@ -23,9 +23,19 @@ const NOSTR_MENTIONS = [
'npub1',
'nprofile1',
'naddr1',
'Nostr:npub1',
'Nostr:nprofile1',
'Nostr:naddre1',
];
const NOSTR_EVENTS = ['nostr:note1', 'note1', 'nostr:nevent1', 'nevent1'];
const NOSTR_EVENTS = [
'nostr:note1',
'note1',
'nostr:nevent1',
'nevent1',
'Nostr:note1',
'Nostr:nevent1',
];
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
@@ -46,7 +56,7 @@ const VIDEOS = [
];
export function useRichContent(content: string, textmode: boolean = false) {
const { db } = useStorage();
const { ark } = useArk();
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
let linkPreview: string;
@@ -58,7 +68,7 @@ export function useRichContent(content: string, textmode: boolean = false) {
const words = text.split(/( |\n)/);
if (!textmode) {
if (db.settings.media) {
if (ark.settings.media) {
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
}
@@ -90,7 +100,7 @@ export function useRichContent(content: string, textmode: boolean = false) {
if (hashtags.length) {
hashtags.forEach((hashtag) => {
parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => {
if (db.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />;
if (ark.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />;
return null;
});
});

View File

@@ -4,14 +4,14 @@ import tippy from 'tippy.js';
import { MentionList } from '@app/new/components';
import { useStorage } from '@libs/storage/provider';
import { useArk } from '@libs/ark';
export function useSuggestion() {
const { db } = useStorage();
const { ark } = useArk();
const suggestion: MentionOptions['suggestion'] = {
items: async ({ query }) => {
const users = await db.getAllCacheUsers();
const users = await ark.getAllCacheUsers();
return users
.filter((item) => {
if (item.name) return item.name.toLowerCase().startsWith(query.toLowerCase());

View File

@@ -1,16 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useStorage } from '@libs/storage/provider';
import { useArk } from '@libs/ark';
import { Widget } from '@utils/types';
export function useWidget() {
const { db } = useStorage();
const { ark } = useArk();
const queryClient = useQueryClient();
const addWidget = useMutation({
mutationFn: async (widget: Widget) => {
return await db.createWidget(widget.kind, widget.title, widget.content);
return await ark.createWidget(widget.kind, widget.title, widget.content);
},
onSuccess: (data) => {
queryClient.setQueryData(['widgets'], (old: Widget[]) => [...old, data]);
@@ -26,8 +26,8 @@ export function useWidget() {
const prevWidgets = queryClient.getQueryData(['widgets']);
// create new widget
await db.removeWidget(currentId);
const newWidget = await db.createWidget(widget.kind, widget.title, widget.content);
await ark.removeWidget(currentId);
const newWidget = await ark.createWidget(widget.kind, widget.title, widget.content);
// Optimistically update to the new value
queryClient.setQueryData(['widgets'], (prev: Widget[]) => [
@@ -57,7 +57,7 @@ export function useWidget() {
);
// Update in database
await db.removeWidget(id);
await ark.removeWidget(id);
// Return a context object with the snapshotted value
return { prevWidgets };

View File

@@ -6,7 +6,7 @@ export function shortenKey(pubkey: string) {
}
export function displayNpub(pubkey: string, len: number, separator?: string) {
const npub = nip19.npubEncode(pubkey) as string;
const npub = pubkey.startsWith('npub1') ? pubkey : (nip19.npubEncode(pubkey) as string);
if (npub.length <= len) return npub;
separator = separator || ' ... ';

View File

@@ -160,3 +160,9 @@ export interface NIP11 {
payments_url: string;
icon: string[];
}
export interface NIP05 {
names: {
[key: string]: string;
};
}