mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 00:10:47 +00:00
wip: ark
This commit is contained in:
parent
a42a2788ea
commit
95124e5ded
@ -1,4 +1,4 @@
|
||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import { NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import { downloadDir } from '@tauri-apps/api/path';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
@ -11,8 +11,7 @@ import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { ArrowLeftIcon, InfoIcon, LoaderIcon } from '@shared/icons';
|
||||
@ -29,13 +28,12 @@ export function CreateAccountScreen() {
|
||||
privkey: string;
|
||||
}>(null);
|
||||
|
||||
const { ark } = useArk();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -62,28 +60,22 @@ export function CreateAccountScreen() {
|
||||
const userNsec = nip19.nsecEncode(userPrivkey);
|
||||
|
||||
const signer = new NDKPrivateKeySigner(userPrivkey);
|
||||
ndk.signer = signer;
|
||||
ark.updateNostrSigner({ signer });
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = JSON.stringify(profile);
|
||||
event.kind = NDKKind.Metadata;
|
||||
event.pubkey = userPubkey;
|
||||
event.tags = [];
|
||||
|
||||
const publish = await event.publish();
|
||||
const publish = await ark.createEvent({
|
||||
content: JSON.stringify(profile),
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
publish: true,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
await db.createAccount(userNpub, userPubkey);
|
||||
await db.secureSave(userPubkey, userPrivkey);
|
||||
|
||||
const relayListEvent = new NDKEvent(ndk);
|
||||
relayListEvent.kind = NDKKind.RelayList;
|
||||
relayListEvent.tags = [...ndk.pool.relays.values()].map((item) => [
|
||||
'r',
|
||||
item.url,
|
||||
]);
|
||||
|
||||
await relayListEvent.publish();
|
||||
await ark.createAccount(userNpub, userPubkey, userPrivkey);
|
||||
await ark.createEvent({
|
||||
kind: NDKKind.RelayList,
|
||||
tags: [ark.relays],
|
||||
publish: true,
|
||||
});
|
||||
|
||||
setKeys({
|
||||
npub: userNpub,
|
||||
@ -93,7 +85,7 @@ export function CreateAccountScreen() {
|
||||
});
|
||||
setLoading(false);
|
||||
} else {
|
||||
toast('Create account failed');
|
||||
toast('Cannot publish user profile, please try again later.');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import * as Accordion from '@radix-ui/react-accordion';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
@ -7,8 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
@ -37,8 +36,7 @@ const POPULAR_USERS = [
|
||||
const LUME_USERS = ['npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445'];
|
||||
|
||||
export function FollowScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { db } = useStorage();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['trending-profiles-widget'],
|
||||
queryFn: async () => {
|
||||
@ -68,16 +66,16 @@ export function FollowScreen() {
|
||||
setLoading(true);
|
||||
if (!follows.length) return navigate('/auth/finish');
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = NDKKind.Contacts;
|
||||
event.tags = follows.map((item) => {
|
||||
if (item.startsWith('npub')) return ['p', nip19.decode(item).data as string];
|
||||
return ['p', item];
|
||||
const publish = await ark.createEvent({
|
||||
kind: NDKKind.Contacts,
|
||||
tags: follows.map((item) => {
|
||||
if (item.startsWith('npub')) return ['p', nip19.decode(item).data as string];
|
||||
return ['p', item];
|
||||
}),
|
||||
});
|
||||
|
||||
const publish = await event.publish();
|
||||
if (publish) {
|
||||
db.account.contacts = follows.map((item) => {
|
||||
ark.account.contacts = follows.map((item) => {
|
||||
if (item.startsWith('npub')) return nip19.decode(item).data as string;
|
||||
return item;
|
||||
});
|
||||
|
@ -8,16 +8,12 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function ImportAccountScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const [npub, setNpub] = useState<string>('');
|
||||
const [nsec, setNsec] = useState<string>('');
|
||||
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
||||
@ -25,6 +21,7 @@ export function ImportAccountScreen() {
|
||||
const [created, setCreated] = useState({ ok: false, remote: false });
|
||||
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
||||
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const submitNpub = async () => {
|
||||
@ -47,8 +44,8 @@ export function ImportAccountScreen() {
|
||||
const pubkey = nip19.decode(npub.split('#')[0]).data as string;
|
||||
const localSigner = NDKPrivateKeySigner.generate();
|
||||
|
||||
await db.createSetting('nsecbunker', '1');
|
||||
await db.secureSave(`${pubkey}-nsecbunker`, localSigner.privateKey);
|
||||
await ark.createSetting('nsecbunker', '1');
|
||||
await ark.createPrivkey(`${pubkey}-nsecbunker`, localSigner.privateKey);
|
||||
|
||||
// open nsecbunker web app in default browser
|
||||
await open('https://app.nsecbunker.com/keys');
|
||||
@ -60,8 +57,7 @@ export function ImportAccountScreen() {
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner);
|
||||
await remoteSigner.blockUntilReady();
|
||||
|
||||
ndk.signer = remoteSigner;
|
||||
ark.updateNostrSigner({ signer: remoteSigner });
|
||||
|
||||
setPubkey(pubkey);
|
||||
setCreated({ ok: false, remote: true });
|
||||
@ -80,14 +76,10 @@ export function ImportAccountScreen() {
|
||||
setLoading(true);
|
||||
|
||||
// add account to db
|
||||
await db.createAccount(npub, pubkey);
|
||||
await ark.createAccount(npub, pubkey);
|
||||
|
||||
// get account metadata
|
||||
const user = ndk.getUser({ pubkey });
|
||||
if (user) {
|
||||
db.account.contacts = [...(await user.follows())].map((user) => user.pubkey);
|
||||
db.account.relayList = await user.relayList();
|
||||
}
|
||||
// get account contacts
|
||||
await ark.getUserContacts({ pubkey });
|
||||
|
||||
setCreated((prev) => ({ ...prev, ok: true }));
|
||||
setLoading(false);
|
||||
@ -109,9 +101,8 @@ export function ImportAccountScreen() {
|
||||
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
|
||||
try {
|
||||
const privkey = nip19.decode(nsec).data as string;
|
||||
await db.secureSave(pubkey, privkey);
|
||||
|
||||
ndk.signer = new NDKPrivateKeySigner(privkey);
|
||||
await ark.createPrivkey(pubkey, privkey);
|
||||
ark.updateNostrSigner({ signer: new NDKPrivateKeySigner(privkey) });
|
||||
|
||||
setSavedPrivkey(true);
|
||||
} catch (e) {
|
||||
@ -290,9 +281,9 @@ export function ImportAccountScreen() {
|
||||
<p className="text-sm">
|
||||
Lume will put your private key to{' '}
|
||||
<b>
|
||||
{db.platform === 'macos'
|
||||
{ark.platform === 'macos'
|
||||
? 'Apple Keychain (macOS)'
|
||||
: db.platform === 'windows'
|
||||
: ark.platform === 'windows'
|
||||
? 'Credential Manager (Windows)'
|
||||
: 'Secret Service (Linux)'}
|
||||
</b>
|
||||
|
@ -1,21 +1,23 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EditIcon, ReactionIcon, ReplyIcon, RepostIcon, ZapIcon } from '@shared/icons';
|
||||
import { TextNote } from '@shared/notes';
|
||||
|
||||
export function TutorialNoteScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const exampleEvent = new NDKEvent(ndk, {
|
||||
id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821',
|
||||
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||
created_at: 1701355223,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'good morning nostr, stay humble and stack sats 🫡',
|
||||
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae',
|
||||
const { ark } = useArk();
|
||||
|
||||
const exampleEvent = ark.createNDKEvent({
|
||||
event: {
|
||||
id: 'a3527670dd9b178bf7c2a9ea673b63bc8bfe774942b196691145343623c45821',
|
||||
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||
created_at: 1701355223,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'good morning nostr, stay humble and stack sats 🫡',
|
||||
sig: '9e0bd67ec25598744f20bff0fe360fdf190c4240edb9eea260e50f77e07f94ea767ececcc6270819b7f64e5e7ca1fe20b4971f46dc120e6db43114557f3a6dae',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@ -7,8 +7,7 @@ import { VList, VListHandle } from 'virtua';
|
||||
import { ChatForm } from '@app/chats/components/chatForm';
|
||||
import { ChatMessage } from '@app/chats/components/message';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
@ -16,8 +15,7 @@ import { User } from '@shared/user';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ChatScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { pubkey } = useParams();
|
||||
const { fetchNIP04Messages } = useNostr();
|
||||
const { status, data } = useQuery({
|
||||
@ -59,7 +57,7 @@ export function ChatScreen() {
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
isSelf={message.pubkey === db.account.pubkey}
|
||||
isSelf={message.pubkey === ark.account.pubkey}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@ -71,20 +69,15 @@ export function ChatScreen() {
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub: NDKSubscription = ndk.subscribe(
|
||||
{
|
||||
const sub = ark.subscribe({
|
||||
filter: {
|
||||
kinds: [4],
|
||||
authors: [db.account.pubkey],
|
||||
authors: [ark.account.pubkey],
|
||||
'#p': [pubkey],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
{
|
||||
closeOnEose: false,
|
||||
}
|
||||
);
|
||||
|
||||
sub.addListener('event', (event) => {
|
||||
newMessage.mutate(event);
|
||||
closeOnEose: false,
|
||||
cb: (event) => newMessage.mutate(event),
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
@ -1,20 +1,15 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { ChatListItem } from '@app/chats/components/chatListItem';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ChatsScreen() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { ndk } = useNDK();
|
||||
const { getAllNIP04Chats } = useNostr();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['nip04-chats'],
|
||||
@ -34,10 +29,6 @@ export function ChatsScreen() {
|
||||
[data]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ndk.signer) navigate('/new/privkey');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-cols-3">
|
||||
<div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NDKEvent, NDKKind, NDKTag } from '@nostr-dev-kit/ndk';
|
||||
import { NDKKind, NDKTag } from '@nostr-dev-kit/ndk';
|
||||
import CharacterCount from '@tiptap/extension-character-count';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
@ -12,7 +12,7 @@ import { Markdown } from 'tiptap-markdown';
|
||||
|
||||
import { ArticleCoverUploader, MediaUploader, MentionPopup } from '@app/new/components';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
BoldIcon,
|
||||
@ -25,7 +25,7 @@ import {
|
||||
} from '@shared/icons';
|
||||
|
||||
export function NewArticleScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
|
||||
const [height, setHeight] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -69,7 +69,7 @@ export function NewArticleScreen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@ -91,16 +91,16 @@ export function NewArticleScreen() {
|
||||
tags.push(['t', tag.replace('#', '')]);
|
||||
});
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = content;
|
||||
event.kind = NDKKind.Article;
|
||||
event.tags = tags;
|
||||
|
||||
// publish
|
||||
const publishedRelays = await event.publish();
|
||||
const publish = await ark.createEvent({
|
||||
content,
|
||||
tags,
|
||||
kind: NDKKind.Article,
|
||||
publish: true,
|
||||
});
|
||||
|
||||
if (publishedRelays) {
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
if (publish) {
|
||||
toast.success(`Broadcasted to ${publish} relays successfully.`);
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { message, open } from '@tauri-apps/plugin-dialog';
|
||||
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
export function NewFileScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -86,20 +85,21 @@ export function NewFileScreen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setIsPublish(true);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.content = caption;
|
||||
event.kind = 1063;
|
||||
event.tags = metadata;
|
||||
const publish = await ark.createEvent({
|
||||
kind: 1063,
|
||||
tags: metadata,
|
||||
content: caption,
|
||||
publish: true,
|
||||
});
|
||||
|
||||
const publishedRelays = await event.publish();
|
||||
if (publishedRelays) {
|
||||
if (publish) {
|
||||
toast.success(`Broadcasted to ${publish} relays successfully.`);
|
||||
setMetadata(null);
|
||||
setIsPublish(false);
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
}
|
||||
} catch (e) {
|
||||
setIsPublish(false);
|
||||
|
@ -13,7 +13,7 @@ import { toast } from 'sonner';
|
||||
|
||||
import { MediaUploader, MentionPopup } from '@app/new/components';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CancelIcon, LoaderIcon } from '@shared/icons';
|
||||
import { MentionNote } from '@shared/notes';
|
||||
@ -23,7 +23,7 @@ import { useSuggestion } from '@utils/hooks/useSuggestion';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function NewPostScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { addWidget } = useWidget();
|
||||
const { suggestion } = useSuggestion();
|
||||
|
||||
@ -68,7 +68,7 @@ export function NewPostScreen() {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@ -81,7 +81,7 @@ export function NewPostScreen() {
|
||||
],
|
||||
});
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
const event = new NDKEvent();
|
||||
event.content = serializedContent;
|
||||
event.kind = NDKKind.Text;
|
||||
|
||||
@ -100,7 +100,10 @@ export function NewPostScreen() {
|
||||
}
|
||||
|
||||
// publish event
|
||||
const publishedRelays = await event.publish();
|
||||
const publishedRelays = await ark.createEvent({
|
||||
kind: NDKKind.Text,
|
||||
content: serializedContent,
|
||||
});
|
||||
|
||||
if (publishedRelays) {
|
||||
toast.success(`Broadcasted to ${publishedRelays.size} relays successfully.`);
|
||||
|
@ -4,19 +4,13 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function NewPrivkeyScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const [nsec, setNsec] = useState('');
|
||||
const { ark } = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const save = async (content: string) => {
|
||||
return await db.secureSave(db.account.pubkey, content);
|
||||
};
|
||||
const [nsec, setNsec] = useState('');
|
||||
|
||||
const submit = async (isSave?: boolean) => {
|
||||
try {
|
||||
@ -30,15 +24,15 @@ export function NewPrivkeyScreen() {
|
||||
const privkey = decoded.data;
|
||||
const pubkey = getPublicKey(privkey);
|
||||
|
||||
if (pubkey !== db.account.pubkey)
|
||||
if (pubkey !== ark.account.pubkey)
|
||||
return toast.info(
|
||||
'Your nsec is not match your current public key, please make sure you enter right nsec'
|
||||
);
|
||||
|
||||
const signer = new NDKPrivateKeySigner(privkey);
|
||||
ndk.signer = signer;
|
||||
ark.updateNostrSigner({ signer });
|
||||
|
||||
if (isSave) await save(privkey);
|
||||
if (isSave) await ark.createPrivkey(ark.account.pubkey, privkey);
|
||||
|
||||
navigate(-1);
|
||||
} catch (e) {
|
||||
|
@ -1,22 +1,19 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EditIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function ContactCard() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['contacts'],
|
||||
queryFn: async () => {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const follows = await user.follows();
|
||||
return [...follows];
|
||||
const contacts = await ark.getUserContacts({});
|
||||
return contacts;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
@ -1,23 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { EditIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { compactNumber } from '@utils/number';
|
||||
|
||||
export function RelayCard() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['relays'],
|
||||
queryKey: ['relays', ark.account.pubkey],
|
||||
queryFn: async () => {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const relays = await user.relayList();
|
||||
|
||||
if (!relays) return Promise.reject(new Error("user's relay set not found"));
|
||||
const relays = await ark.getUserRelays({});
|
||||
return relays;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
|
@ -1,21 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function EditContactScreen() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ['contacts'],
|
||||
queryFn: async () => {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
|
||||
const follows = await user.follows();
|
||||
return [...follows];
|
||||
return await ark.getUserContacts({});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
@ -29,10 +24,10 @@ export function EditContactScreen() {
|
||||
) : (
|
||||
data.map((item) => (
|
||||
<div
|
||||
key={item.pubkey}
|
||||
key={item}
|
||||
className="flex h-16 w-full items-center justify-between rounded-xl bg-neutral-100 px-2.5 dark:bg-neutral-900"
|
||||
>
|
||||
<User pubkey={item.pubkey} variant="simple" />
|
||||
<User pubkey={item} variant="simple" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
@ -1,29 +1,21 @@
|
||||
import { NDKEvent, NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||
import { NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { CheckCircleIcon, LoaderIcon, PlusIcon, UnverifiedIcon } from '@shared/icons';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function EditProfileScreen() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('');
|
||||
const [banner, setBanner] = useState('');
|
||||
const [nip05, setNIP05] = useState({ verified: true, text: '' });
|
||||
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { upload } = useNostr();
|
||||
const { ark } = useArk();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -32,7 +24,7 @@ export function EditProfileScreen() {
|
||||
formState: { isValid, errors },
|
||||
} = useForm({
|
||||
defaultValues: async () => {
|
||||
const res: NDKUserProfile = queryClient.getQueryData(['user', db.account.pubkey]);
|
||||
const res: NDKUserProfile = queryClient.getQueryData(['user', ark.account.pubkey]);
|
||||
if (res.image) {
|
||||
setPicture(res.image);
|
||||
}
|
||||
@ -46,13 +38,16 @@ export function EditProfileScreen() {
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const image = await upload();
|
||||
const image = await ark.upload({});
|
||||
if (image) {
|
||||
setPicture(image);
|
||||
setLoading(false);
|
||||
@ -67,7 +62,7 @@ export function EditProfileScreen() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const image = await upload();
|
||||
const image = await ark.upload({});
|
||||
|
||||
if (image) {
|
||||
setBanner(image);
|
||||
@ -83,7 +78,7 @@ export function EditProfileScreen() {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const content = {
|
||||
let content = {
|
||||
...data,
|
||||
username: data.name,
|
||||
display_name: data.name,
|
||||
@ -91,15 +86,10 @@ export function EditProfileScreen() {
|
||||
image: data.picture,
|
||||
};
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = NDKKind.Metadata;
|
||||
event.tags = [];
|
||||
|
||||
if (data.nip05) {
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const verify = await user.validateNip05(data.nip05);
|
||||
const verify = ark.validateNIP05({ pubkey: ark.account.pubkey, nip05: data.nip05 });
|
||||
if (verify) {
|
||||
event.content = JSON.stringify({ ...content, nip05: data.nip05 });
|
||||
content = { ...content, nip05: data.nip05 };
|
||||
} else {
|
||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
||||
setError('nip05', {
|
||||
@ -107,16 +97,19 @@ export function EditProfileScreen() {
|
||||
message: "Can't verify your Lume ID / NIP-05, please check again",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
event.content = JSON.stringify(content);
|
||||
}
|
||||
|
||||
const publishedRelays = await event.publish();
|
||||
const publish = await ark.createEvent({
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
content: JSON.stringify(content),
|
||||
publish: true,
|
||||
});
|
||||
|
||||
if (publishedRelays) {
|
||||
if (publish) {
|
||||
// invalid cache
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['user', db.account.pubkey],
|
||||
queryKey: ['user', ark.account.pubkey],
|
||||
});
|
||||
// reset form
|
||||
reset();
|
||||
|
661
src/libs/ark/ark.ts
Normal file
661
src/libs/ark/ark.ts
Normal file
@ -0,0 +1,661 @@
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKKind,
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
NDKSubscriptionCacheUsage,
|
||||
NDKTag,
|
||||
NDKUser,
|
||||
NostrEvent,
|
||||
} from '@nostr-dev-kit/ndk';
|
||||
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||
import { invoke } from '@tauri-apps/api/primitives';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
||||
import { Platform } from '@tauri-apps/plugin-os';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { NostrEventExt, NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { NDKCacheAdapterTauri } from '@libs/ark';
|
||||
|
||||
import {
|
||||
Account,
|
||||
NDKCacheUser,
|
||||
NDKCacheUserProfile,
|
||||
NDKEventWithReplies,
|
||||
NIP05,
|
||||
Widget,
|
||||
} from '@utils/types';
|
||||
|
||||
export class Ark {
|
||||
#ndk: NDK;
|
||||
#fetcher: NostrFetcher;
|
||||
#storage: Database;
|
||||
public account: Account | null;
|
||||
public relays: string[] | null;
|
||||
public readyToSign: boolean;
|
||||
readonly platform: Platform | null;
|
||||
readonly settings: {
|
||||
autoupdate: boolean;
|
||||
outbox: boolean;
|
||||
media: boolean;
|
||||
hashtag: boolean;
|
||||
};
|
||||
|
||||
constructor({ storage }: { storage: Database }) {
|
||||
this.#storage = storage;
|
||||
this.#init();
|
||||
}
|
||||
|
||||
async #keyring_save(key: string, value: string) {
|
||||
return await invoke('secure_save', { key, value });
|
||||
}
|
||||
|
||||
async #keyring_load(key: string) {
|
||||
try {
|
||||
const value: string = await invoke('secure_load', { key });
|
||||
if (!value) return null;
|
||||
return value;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async #keyring_remove(key: string) {
|
||||
return await invoke('secure_remove', { key });
|
||||
}
|
||||
|
||||
async #initNostrSigner({ nsecbunker }: { nsecbunker?: boolean }) {
|
||||
if (!this.account) {
|
||||
this.readyToSign = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// NIP-46 Signer
|
||||
if (nsecbunker) {
|
||||
const localSignerPrivkey = await this.#keyring_load(
|
||||
`${this.account.pubkey}-nsecbunker`
|
||||
);
|
||||
|
||||
if (!localSignerPrivkey) {
|
||||
this.readyToSign = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
|
||||
const bunker = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.nsecbunker.com', 'wss://nostr.vulpem.com'],
|
||||
});
|
||||
bunker.connect();
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(bunker, this.account.id, localSigner);
|
||||
await remoteSigner.blockUntilReady();
|
||||
|
||||
this.readyToSign = true;
|
||||
return remoteSigner;
|
||||
}
|
||||
|
||||
// Privkey Signer
|
||||
const userPrivkey = await this.#keyring_load(this.account.pubkey);
|
||||
|
||||
if (!userPrivkey) {
|
||||
this.readyToSign = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.readyToSign = true;
|
||||
return new NDKPrivateKeySigner(userPrivkey);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (e === 'Token already redeemed') {
|
||||
toast.info(
|
||||
'nsecbunker token already redeemed. You need to re-login with another token.'
|
||||
);
|
||||
await this.logout();
|
||||
}
|
||||
|
||||
this.readyToSign = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async #init() {
|
||||
const outboxSetting = await this.getSettingValue('outbox');
|
||||
const bunkerSetting = await this.getSettingValue('nsecbunker');
|
||||
|
||||
const bunker = !!parseInt(bunkerSetting);
|
||||
const enableOutboxModel = !!parseInt(outboxSetting);
|
||||
|
||||
const explicitRelayUrls = normalizeRelayUrlSet([
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://nos.lol',
|
||||
'wss://nostr.mutinywallet.com',
|
||||
]);
|
||||
|
||||
// #TODO: user should config outbox relays
|
||||
const outboxRelayUrls = normalizeRelayUrlSet(['wss://purplepag.es']);
|
||||
|
||||
// #TODO: user should config blacklist relays
|
||||
const blacklistRelayUrls = normalizeRelayUrlSet(['wss://brb.io']);
|
||||
|
||||
const cacheAdapter = new NDKCacheAdapterTauri(this.#storage);
|
||||
const ndk = new NDK({
|
||||
cacheAdapter,
|
||||
explicitRelayUrls,
|
||||
outboxRelayUrls,
|
||||
blacklistRelayUrls,
|
||||
enableOutboxModel,
|
||||
autoConnectUserRelays: true,
|
||||
autoFetchUserMutelist: true,
|
||||
// clientName: 'Lume',
|
||||
// clientNip89: '',
|
||||
});
|
||||
|
||||
// add signer if exist
|
||||
const signer = await this.#initNostrSigner({ nsecbunker: bunker });
|
||||
if (signer) ndk.signer = signer;
|
||||
|
||||
// connect
|
||||
await ndk.connect();
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
|
||||
|
||||
// update account's metadata
|
||||
if (this.account) {
|
||||
const user = ndk.getUser({ pubkey: this.account.pubkey });
|
||||
ndk.activeUser = user;
|
||||
|
||||
const contacts = await user.follows(undefined /* outbox */);
|
||||
this.account.contacts = [...contacts].map((user) => user.pubkey);
|
||||
}
|
||||
|
||||
this.#ndk = ndk;
|
||||
this.#fetcher = fetcher;
|
||||
}
|
||||
|
||||
public updateNostrSigner({ signer }: { signer: NDKNip46Signer | NDKPrivateKeySigner }) {
|
||||
this.#ndk.signer = signer;
|
||||
return this.#ndk.signer;
|
||||
}
|
||||
|
||||
public async getAllCacheUsers() {
|
||||
const results: Array<NDKCacheUser> = await this.#storage.select(
|
||||
'SELECT * FROM ndk_users ORDER BY createdAt DESC;'
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
|
||||
const users: NDKCacheUserProfile[] = results.map((item) => ({
|
||||
pubkey: item.pubkey,
|
||||
...JSON.parse(item.profile as string),
|
||||
}));
|
||||
return users;
|
||||
}
|
||||
|
||||
public async checkAccount() {
|
||||
const result: Array<{ total: string }> = await this.#storage.select(
|
||||
'SELECT COUNT(*) AS "total" FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
|
||||
);
|
||||
return parseInt(result[0].total);
|
||||
}
|
||||
|
||||
public async getActiveAccount() {
|
||||
const results: Array<Account> = await this.#storage.select(
|
||||
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
|
||||
);
|
||||
|
||||
if (results.length) {
|
||||
this.account = results[0];
|
||||
this.account.contacts = [];
|
||||
} else {
|
||||
console.log('no active account, please create new account');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async createAccount(npub: string, pubkey: string, privkey?: string) {
|
||||
const existAccounts: Array<Account> = await this.#storage.select(
|
||||
'SELECT * FROM accounts WHERE pubkey = $1 ORDER BY id DESC LIMIT 1;',
|
||||
[pubkey]
|
||||
);
|
||||
|
||||
if (existAccounts.length) {
|
||||
await this.#storage.execute(
|
||||
"UPDATE accounts SET is_active = '1' WHERE pubkey = $1;",
|
||||
[pubkey]
|
||||
);
|
||||
} else {
|
||||
await this.#storage.execute(
|
||||
'INSERT OR IGNORE INTO accounts (id, pubkey, is_active) VALUES ($1, $2, $3);',
|
||||
[npub, pubkey, 1]
|
||||
);
|
||||
|
||||
if (privkey) await this.#keyring_save(pubkey, privkey);
|
||||
}
|
||||
|
||||
return await this.getActiveAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save private key to OS secure storage
|
||||
* @deprecated this method will be marked as private in the next update
|
||||
*/
|
||||
public async createPrivkey(name: string, privkey: string) {
|
||||
await this.#keyring_save(name, privkey);
|
||||
}
|
||||
|
||||
public async updateAccount(column: string, value: string) {
|
||||
const insert = await this.#storage.execute(
|
||||
`UPDATE accounts SET ${column} = $1 WHERE id = $2;`,
|
||||
[value, this.account.id]
|
||||
);
|
||||
|
||||
if (insert) {
|
||||
const account = await this.getActiveAccount();
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public async getWidgets() {
|
||||
const widgets: Array<Widget> = await this.#storage.select(
|
||||
'SELECT * FROM widgets WHERE account_id = $1 ORDER BY created_at DESC;',
|
||||
[this.account.id]
|
||||
);
|
||||
return widgets;
|
||||
}
|
||||
|
||||
public async createWidget(kind: number, title: string, content: string | string[]) {
|
||||
const insert = await this.#storage.execute(
|
||||
'INSERT INTO widgets (account_id, kind, title, content) VALUES ($1, $2, $3, $4);',
|
||||
[this.account.id, kind, title, content]
|
||||
);
|
||||
|
||||
if (insert) {
|
||||
const widgets: Array<Widget> = await this.#storage.select(
|
||||
'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;'
|
||||
);
|
||||
if (widgets.length < 1) console.error('get created widget failed');
|
||||
return widgets[0];
|
||||
} else {
|
||||
console.error('create widget failed');
|
||||
}
|
||||
}
|
||||
|
||||
public async removeWidget(id: string) {
|
||||
const res = await this.#storage.execute('DELETE FROM widgets WHERE id = $1;', [id]);
|
||||
if (res) return id;
|
||||
}
|
||||
|
||||
public async createSetting(key: string, value: string | undefined) {
|
||||
if (value) {
|
||||
return await this.#storage.execute(
|
||||
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
|
||||
[key, value]
|
||||
);
|
||||
}
|
||||
|
||||
const currentSetting = await this.checkSettingValue(key);
|
||||
|
||||
if (!currentSetting)
|
||||
return await this.#storage.execute(
|
||||
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
|
||||
[key, value]
|
||||
);
|
||||
|
||||
const currentValue = !!parseInt(currentSetting);
|
||||
|
||||
return await this.#storage.execute('UPDATE settings SET value = $1 WHERE key = $2;', [
|
||||
+!currentValue,
|
||||
key,
|
||||
]);
|
||||
}
|
||||
|
||||
public async getAllSettings() {
|
||||
const results: { key: string; value: string }[] = await this.#storage.select(
|
||||
'SELECT * FROM settings ORDER BY id DESC;'
|
||||
);
|
||||
if (results.length < 1) return null;
|
||||
return results;
|
||||
}
|
||||
|
||||
public async checkSettingValue(key: string) {
|
||||
const results: { key: string; value: string }[] = await this.#storage.select(
|
||||
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
|
||||
[key]
|
||||
);
|
||||
if (!results.length) return false;
|
||||
return results[0].value;
|
||||
}
|
||||
|
||||
public async getSettingValue(key: string) {
|
||||
const results: { key: string; value: string }[] = await this.#storage.select(
|
||||
'SELECT * FROM settings WHERE key = $1 ORDER BY id DESC LIMIT 1;',
|
||||
[key]
|
||||
);
|
||||
if (!results.length) return '0';
|
||||
return results[0].value;
|
||||
}
|
||||
|
||||
public async clearCache() {
|
||||
await this.#storage.execute('DELETE FROM ndk_events;');
|
||||
await this.#storage.execute('DELETE FROM ndk_eventtags;');
|
||||
await this.#storage.execute('DELETE FROM ndk_users;');
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
await this.#storage.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
|
||||
this.account.id,
|
||||
]);
|
||||
await this.#keyring_remove(this.account.pubkey);
|
||||
await this.#keyring_remove(`${this.account.pubkey}-nsecbunker`);
|
||||
|
||||
this.account = null;
|
||||
this.#ndk.signer = null;
|
||||
}
|
||||
|
||||
public subscribe({
|
||||
filter,
|
||||
closeOnEose = false,
|
||||
cb,
|
||||
}: {
|
||||
filter: NDKFilter;
|
||||
closeOnEose: boolean;
|
||||
cb: (event: NDKEvent) => void;
|
||||
}) {
|
||||
const sub = this.#ndk.subscribe(filter, { closeOnEose });
|
||||
sub.addListener('event', (event: NDKEvent) => cb(event));
|
||||
return sub;
|
||||
}
|
||||
|
||||
public createNDKEvent({ event }: { event: NostrEvent | NostrEventExt }) {
|
||||
return new NDKEvent(this.#ndk, event);
|
||||
}
|
||||
|
||||
public async createEvent({
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
publish,
|
||||
}: {
|
||||
kind: NDKKind | number;
|
||||
tags: NDKTag[];
|
||||
content?: string;
|
||||
publish?: boolean;
|
||||
}) {
|
||||
try {
|
||||
const event = new NDKEvent(this.#ndk);
|
||||
if (content) event.content = content;
|
||||
event.kind = kind;
|
||||
event.tags = tags;
|
||||
|
||||
if (!publish) {
|
||||
const publish = await event.publish();
|
||||
if (!publish) throw new Error('cannot publish error');
|
||||
return publish.size;
|
||||
}
|
||||
|
||||
return event;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserProfile({ pubkey }: { pubkey: string }) {
|
||||
try {
|
||||
const user = this.#ndk.getUser({ pubkey });
|
||||
const profile = await user.fetchProfile({
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
|
||||
if (!profile) return null;
|
||||
return profile;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserContacts({
|
||||
pubkey = undefined,
|
||||
outbox = undefined,
|
||||
}: {
|
||||
pubkey?: string;
|
||||
outbox?: boolean;
|
||||
}) {
|
||||
try {
|
||||
const user = this.#ndk.getUser({ pubkey: pubkey ? pubkey : this.account.pubkey });
|
||||
const contacts = [...(await user.follows(undefined, outbox))].map(
|
||||
(user) => user.pubkey
|
||||
);
|
||||
|
||||
this.account.contacts = contacts;
|
||||
return contacts;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserRelays({ pubkey }: { pubkey?: string }) {
|
||||
try {
|
||||
const user = this.#ndk.getUser({ pubkey: pubkey ? pubkey : this.account.pubkey });
|
||||
return await user.relayList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async createContact({ pubkey }: { pubkey: string }) {
|
||||
const user = this.#ndk.getUser({ pubkey: this.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
return await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
}
|
||||
|
||||
public async deleteContact({ pubkey }: { pubkey: string }) {
|
||||
const user = this.#ndk.getUser({ pubkey: this.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
return await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
}
|
||||
|
||||
public async getAllEvents({ filter }: { filter: NDKFilter }) {
|
||||
const events = await this.#ndk.fetchEvents(filter);
|
||||
if (!events) return [];
|
||||
return [...events];
|
||||
}
|
||||
|
||||
public async getEventById({ id }: { id: string }) {
|
||||
const event = await this.#ndk.fetchEvent(id, {
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
|
||||
if (!event) return null;
|
||||
return event;
|
||||
}
|
||||
|
||||
public getEventThread({ tags }: { 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,
|
||||
};
|
||||
}
|
||||
|
||||
public async getThreads({ id, data }: { id: string; data?: NDKEventWithReplies[] }) {
|
||||
let events = data || null;
|
||||
|
||||
if (!data) {
|
||||
const relayUrls = [...this.#ndk.pool.relays.values()].map((item) => item.url);
|
||||
const rawEvents = (await this.#fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{
|
||||
kinds: [NDKKind.Text],
|
||||
'#e': [id],
|
||||
},
|
||||
{ since: 0 },
|
||||
{ sort: true }
|
||||
)) as unknown as NostrEvent[];
|
||||
events = rawEvents.map(
|
||||
(event) => new NDKEvent(this.#ndk, event)
|
||||
) as NDKEvent[] 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;
|
||||
}
|
||||
|
||||
public async getInfiniteEvents({
|
||||
filter,
|
||||
limit,
|
||||
pageParam = 0,
|
||||
signal = undefined,
|
||||
}: {
|
||||
filter: NDKFilter;
|
||||
limit: number;
|
||||
pageParam?: number;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const rootIds = new Set();
|
||||
const dedupQueue = new Set();
|
||||
|
||||
const events = await this.#fetcher.fetchLatestEvents(this.relays, filter, limit, {
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
});
|
||||
|
||||
const ndkEvents = events.map((event) => {
|
||||
return new NDKEvent(this.#ndk, event);
|
||||
});
|
||||
|
||||
ndkEvents.forEach((event) => {
|
||||
const tags = event.tags.filter((el) => el[0] === 'e');
|
||||
if (tags && tags.length > 0) {
|
||||
const rootId = tags.filter((el) => el[3] === 'root')[1] ?? tags[0][1];
|
||||
if (rootIds.has(rootId)) return dedupQueue.add(event.id);
|
||||
rootIds.add(rootId);
|
||||
}
|
||||
});
|
||||
|
||||
return ndkEvents
|
||||
.filter((event) => !dedupQueue.has(event.id))
|
||||
.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload media file to nostr.build
|
||||
* @todo support multiple backends
|
||||
*/
|
||||
public async upload({ fileExts }: { fileExts?: string[] }) {
|
||||
const defaultExts = ['png', 'jpeg', 'jpg', 'gif'].concat(fileExts);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async validateNIP05({
|
||||
pubkey,
|
||||
nip05,
|
||||
signal,
|
||||
}: {
|
||||
pubkey: string;
|
||||
nip05: string;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const localPath = nip05.split('@')[0];
|
||||
const service = nip05.split('@')[1];
|
||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||
|
||||
const res = await fetch(verifyURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||
|
||||
const data: NIP05 = await res.json();
|
||||
if (data.names) {
|
||||
if (data.names[localPath.toLowerCase()] !== pubkey) return false;
|
||||
if (data.names[localPath] !== pubkey) return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
539
src/libs/ark/cache.ts
Normal file
539
src/libs/ark/cache.ts
Normal file
@ -0,0 +1,539 @@
|
||||
// inspired by NDK Cache Dexie
|
||||
// source: https://github.com/nostr-dev-kit/ndk/tree/master/ndk-cache-dexie
|
||||
import {
|
||||
Hexpubkey,
|
||||
NDKCacheAdapter,
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKRelay,
|
||||
NDKSubscription,
|
||||
NDKUserProfile,
|
||||
profileFromEvent,
|
||||
} from '@nostr-dev-kit/ndk';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { NostrEvent } from 'nostr-fetch';
|
||||
import { matchFilter } from 'nostr-tools';
|
||||
|
||||
import { NDKCacheEvent, NDKCacheEventTag, NDKCacheUser } from '@utils/types';
|
||||
|
||||
export class NDKCacheAdapterTauri implements NDKCacheAdapter {
|
||||
#db: Database;
|
||||
private dirtyProfiles: Set<Hexpubkey> = new Set();
|
||||
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
|
||||
readonly locking: boolean;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.#db = db;
|
||||
this.locking = true;
|
||||
|
||||
this.profiles = new LRUCache({
|
||||
max: 100000,
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
this.dumpProfiles();
|
||||
}, 1000 * 10);
|
||||
}
|
||||
|
||||
async #getCacheUser(pubkey: string) {
|
||||
const results: Array<NDKCacheUser> = await this.#db.select(
|
||||
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
|
||||
[pubkey]
|
||||
);
|
||||
|
||||
if (!results.length) return null;
|
||||
|
||||
if (typeof results[0].profile === 'string')
|
||||
results[0].profile = JSON.parse(results[0].profile);
|
||||
|
||||
return results[0];
|
||||
}
|
||||
|
||||
async #getCacheEvent(id: string) {
|
||||
const results: Array<NDKCacheEvent> = await this.#db.select(
|
||||
'SELECT * FROM ndk_events WHERE id = $1 ORDER BY id DESC LIMIT 1;',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!results.length) return null;
|
||||
return results[0];
|
||||
}
|
||||
|
||||
async #getCacheEvents(ids: string[]) {
|
||||
const idsArr = `'${ids.join("','")}'`;
|
||||
|
||||
const results: Array<NDKCacheEvent> = await this.#db.select(
|
||||
`SELECT * FROM ndk_events WHERE id IN (${idsArr}) ORDER BY id;`
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
return results;
|
||||
}
|
||||
|
||||
async #getCacheEventsByPubkey(pubkey: string) {
|
||||
const results: Array<NDKCacheEvent> = await this.#db.select(
|
||||
'SELECT * FROM ndk_events WHERE pubkey = $1 ORDER BY id;',
|
||||
[pubkey]
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
return results;
|
||||
}
|
||||
|
||||
async #getCacheEventsByKind(kind: number) {
|
||||
const results: Array<NDKCacheEvent> = await this.#db.select(
|
||||
'SELECT * FROM ndk_events WHERE kind = $1 ORDER BY id;',
|
||||
[kind]
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
return results;
|
||||
}
|
||||
|
||||
async #getCacheEventsByKindAndAuthor(kind: number, pubkey: string) {
|
||||
const results: Array<NDKCacheEvent> = await this.#db.select(
|
||||
'SELECT * FROM ndk_events WHERE kind = $1 AND pubkey = $2 ORDER BY id;',
|
||||
[kind, pubkey]
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
return results;
|
||||
}
|
||||
|
||||
async #getCacheEventTagsByTagValue(tagValue: string) {
|
||||
const results: Array<NDKCacheEventTag> = await this.#db.select(
|
||||
'SELECT * FROM ndk_eventtags WHERE tagValue = $1 ORDER BY id;',
|
||||
[tagValue]
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
return results;
|
||||
}
|
||||
|
||||
async #setCacheEvent({
|
||||
id,
|
||||
pubkey,
|
||||
content,
|
||||
kind,
|
||||
createdAt,
|
||||
relay,
|
||||
event,
|
||||
}: NDKCacheEvent) {
|
||||
return await this.#db.execute(
|
||||
'INSERT OR IGNORE INTO ndk_events (id, pubkey, content, kind, createdAt, relay, event) VALUES ($1, $2, $3, $4, $5, $6, $7);',
|
||||
[id, pubkey, content, kind, createdAt, relay, event]
|
||||
);
|
||||
}
|
||||
|
||||
async #setCacheEventTag({ id, eventId, tag, value, tagValue }: NDKCacheEventTag) {
|
||||
return await this.#db.execute(
|
||||
'INSERT OR IGNORE INTO ndk_eventtags (id, eventId, tag, value, tagValue) VALUES ($1, $2, $3, $4, $5);',
|
||||
[id, eventId, tag, value, tagValue]
|
||||
);
|
||||
}
|
||||
|
||||
async #setCacheProfiles(profiles: Array<NDKCacheUser>) {
|
||||
return await Promise.all(
|
||||
profiles.map(
|
||||
async (profile) =>
|
||||
await this.#db.execute(
|
||||
'INSERT OR IGNORE INTO ndk_users (pubkey, profile, createdAt) VALUES ($1, $2, $3);',
|
||||
[profile.pubkey, profile.profile, profile.createdAt]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async query(subscription: NDKSubscription): Promise<void> {
|
||||
Promise.allSettled(
|
||||
subscription.filters.map((filter) => this.processFilter(filter, subscription))
|
||||
);
|
||||
}
|
||||
|
||||
public async fetchProfile(pubkey: Hexpubkey) {
|
||||
if (!this.profiles) return null;
|
||||
|
||||
let profile = this.profiles.get(pubkey);
|
||||
|
||||
if (!profile) {
|
||||
const user = await this.#getCacheUser(pubkey);
|
||||
if (user) {
|
||||
profile = user.profile as NDKUserProfile;
|
||||
this.profiles.set(pubkey, profile);
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public saveProfile(pubkey: Hexpubkey, profile: NDKUserProfile) {
|
||||
if (!this.profiles) return;
|
||||
|
||||
this.profiles.set(pubkey, profile);
|
||||
|
||||
this.dirtyProfiles.add(pubkey);
|
||||
}
|
||||
|
||||
private async processFilter(
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<void> {
|
||||
const _filter = { ...filter };
|
||||
delete _filter.limit;
|
||||
const filterKeys = Object.keys(_filter || {}).sort();
|
||||
|
||||
try {
|
||||
(await this.byKindAndAuthor(filterKeys, filter, subscription)) ||
|
||||
(await this.byAuthors(filterKeys, filter, subscription)) ||
|
||||
(await this.byKinds(filterKeys, filter, subscription)) ||
|
||||
(await this.byIdsQuery(filterKeys, filter, subscription)) ||
|
||||
(await this.byNip33Query(filterKeys, filter, subscription)) ||
|
||||
(await this.byTagsAndOptionallyKinds(filterKeys, filter, subscription));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async setEvent(
|
||||
event: NDKEvent,
|
||||
_filter: NDKFilter,
|
||||
relay?: NDKRelay
|
||||
): Promise<void> {
|
||||
if (event.kind === 0) {
|
||||
if (!this.profiles) return;
|
||||
|
||||
const profile: NDKUserProfile = profileFromEvent(event);
|
||||
this.profiles.set(event.pubkey, profile);
|
||||
} else {
|
||||
let addEvent = true;
|
||||
|
||||
if (event.isParamReplaceable()) {
|
||||
const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`;
|
||||
const existingEvent = await this.#getCacheEvent(replaceableId);
|
||||
if (
|
||||
existingEvent &&
|
||||
event.created_at &&
|
||||
existingEvent.createdAt > event.created_at
|
||||
) {
|
||||
addEvent = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (addEvent) {
|
||||
this.#setCacheEvent({
|
||||
id: event.tagId(),
|
||||
pubkey: event.pubkey,
|
||||
content: event.content,
|
||||
kind: event.kind!,
|
||||
createdAt: event.created_at!,
|
||||
relay: relay?.url,
|
||||
event: JSON.stringify(event.rawEvent()),
|
||||
});
|
||||
|
||||
// Don't cache contact lists as tags since it's expensive
|
||||
// and there is no use case for it
|
||||
if (event.kind !== 3) {
|
||||
event.tags.forEach((tag) => {
|
||||
if (tag[0].length !== 1) return;
|
||||
|
||||
this.#setCacheEventTag({
|
||||
id: `${event.id}:${tag[0]}:${tag[1]}`,
|
||||
eventId: event.id,
|
||||
tag: tag[0],
|
||||
value: tag[1],
|
||||
tagValue: tag[0] + tag[1],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches by authors
|
||||
*/
|
||||
private async byAuthors(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<boolean> {
|
||||
const f = ['authors'];
|
||||
const hasAllKeys =
|
||||
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||
|
||||
let foundEvents = false;
|
||||
|
||||
if (hasAllKeys && filter.authors) {
|
||||
for (const pubkey of filter.authors) {
|
||||
const events = await this.#getCacheEventsByPubkey(pubkey);
|
||||
for (const event of events) {
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
rawEvent = JSON.parse(event.event);
|
||||
} catch (e) {
|
||||
console.log('failed to parse event', e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||
subscription.eventReceived(ndkEvent, relay, true);
|
||||
foundEvents = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches by kinds
|
||||
*/
|
||||
private async byKinds(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<boolean> {
|
||||
const f = ['kinds'];
|
||||
const hasAllKeys =
|
||||
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||
|
||||
let foundEvents = false;
|
||||
|
||||
if (hasAllKeys && filter.kinds) {
|
||||
for (const kind of filter.kinds) {
|
||||
const events = await this.#getCacheEventsByKind(kind);
|
||||
for (const event of events) {
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
rawEvent = JSON.parse(event.event);
|
||||
} catch (e) {
|
||||
console.log('failed to parse event', e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||
subscription.eventReceived(ndkEvent, relay, true);
|
||||
foundEvents = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches by ids
|
||||
*/
|
||||
private async byIdsQuery(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<boolean> {
|
||||
const f = ['ids'];
|
||||
const hasAllKeys =
|
||||
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||
|
||||
if (hasAllKeys && filter.ids) {
|
||||
for (const id of filter.ids) {
|
||||
const event = await this.#getCacheEvent(id);
|
||||
if (!event) continue;
|
||||
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
rawEvent = JSON.parse(event.event);
|
||||
} catch (e) {
|
||||
console.log('failed to parse event', e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||
subscription.eventReceived(ndkEvent, relay, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches by NIP-33
|
||||
*/
|
||||
private async byNip33Query(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<boolean> {
|
||||
const f = ['#d', 'authors', 'kinds'];
|
||||
const hasAllKeys =
|
||||
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||
|
||||
if (hasAllKeys && filter.kinds && filter.authors) {
|
||||
for (const kind of filter.kinds) {
|
||||
const replaceableKind = kind >= 30000 && kind < 40000;
|
||||
|
||||
if (!replaceableKind) continue;
|
||||
|
||||
for (const author of filter.authors) {
|
||||
for (const dTag of filter['#d']) {
|
||||
const replaceableId = `${kind}:${author}:${dTag}`;
|
||||
const event = await this.#getCacheEvent(replaceableId);
|
||||
if (!event) continue;
|
||||
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
rawEvent = JSON.parse(event.event);
|
||||
} catch (e) {
|
||||
console.log('failed to parse event', e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||
subscription.eventReceived(ndkEvent, relay, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches by kind & author
|
||||
*/
|
||||
private async byKindAndAuthor(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<boolean> {
|
||||
const f = ['authors', 'kinds'];
|
||||
const hasAllKeys =
|
||||
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||
let foundEvents = false;
|
||||
|
||||
if (!hasAllKeys) return false;
|
||||
|
||||
if (filter.kinds && filter.authors) {
|
||||
for (const kind of filter.kinds) {
|
||||
for (const author of filter.authors) {
|
||||
const events = await this.#getCacheEventsByKindAndAuthor(kind, author);
|
||||
|
||||
for (const event of events) {
|
||||
let rawEvent: NostrEvent;
|
||||
try {
|
||||
rawEvent = JSON.parse(event.event);
|
||||
} catch (e) {
|
||||
console.log('failed to parse event', e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||
subscription.eventReceived(ndkEvent, relay, true);
|
||||
foundEvents = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches by tags and optionally filters by tags
|
||||
*/
|
||||
private async byTagsAndOptionallyKinds(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter,
|
||||
subscription: NDKSubscription
|
||||
): Promise<boolean> {
|
||||
for (const filterKey of filterKeys) {
|
||||
const isKind = filterKey === 'kinds';
|
||||
const isTag = filterKey.startsWith('#') && filterKey.length === 2;
|
||||
|
||||
if (!isKind && !isTag) return false;
|
||||
}
|
||||
|
||||
const events = await this.filterByTag(filterKeys, filter);
|
||||
const kinds = filter.kinds as number[];
|
||||
|
||||
for (const event of events) {
|
||||
if (!kinds?.includes(event.kind!)) continue;
|
||||
|
||||
subscription.eventReceived(event, undefined, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async filterByTag(
|
||||
filterKeys: string[],
|
||||
filter: NDKFilter
|
||||
): Promise<NDKEvent[]> {
|
||||
const retEvents: NDKEvent[] = [];
|
||||
|
||||
for (const filterKey of filterKeys) {
|
||||
if (filterKey.length !== 2) continue;
|
||||
const tag = filterKey.slice(1);
|
||||
// const values = filter[filterKey] as string[];
|
||||
const values: string[] = [];
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (key === filterKey) values.push(value as string);
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
const eventTags = await this.#getCacheEventTagsByTagValue(tag + value);
|
||||
if (!eventTags.length) continue;
|
||||
|
||||
const eventIds = eventTags.map((t) => t.eventId);
|
||||
|
||||
const events = await this.#getCacheEvents(eventIds);
|
||||
for (const event of events) {
|
||||
let rawEvent;
|
||||
try {
|
||||
rawEvent = JSON.parse(event.event);
|
||||
|
||||
// Make sure all passed filters match the event
|
||||
if (!matchFilter(filter, rawEvent)) continue;
|
||||
} catch (e) {
|
||||
console.log('failed to parse event', e);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||
ndkEvent.relay = relay;
|
||||
retEvents.push(ndkEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retEvents;
|
||||
}
|
||||
|
||||
private async dumpProfiles(): Promise<void> {
|
||||
const profiles = [];
|
||||
|
||||
if (!this.profiles) return;
|
||||
|
||||
for (const pubkey of this.dirtyProfiles) {
|
||||
const profile = this.profiles.get(pubkey);
|
||||
|
||||
if (!profile) continue;
|
||||
|
||||
profiles.push({
|
||||
pubkey,
|
||||
profile: JSON.stringify(profile),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
if (profiles.length) {
|
||||
await this.#setCacheProfiles(profiles);
|
||||
}
|
||||
|
||||
this.dirtyProfiles.clear();
|
||||
}
|
||||
}
|
3
src/libs/ark/index.ts
Normal file
3
src/libs/ark/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './ark';
|
||||
export * from './cache';
|
||||
export * from './provider';
|
123
src/libs/ark/provider.tsx
Normal file
123
src/libs/ark/provider.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { Ark } from '@libs/ark';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { QUOTES } from '@utils/constants';
|
||||
|
||||
interface ArkContext {
|
||||
ark: Ark;
|
||||
}
|
||||
|
||||
const ArkContext = createContext<ArkContext>({
|
||||
ark: undefined,
|
||||
});
|
||||
|
||||
const ArkProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
const [ark, setArk] = useState<Ark>(undefined);
|
||||
const [isNewVersion, setIsNewVersion] = useState(false);
|
||||
|
||||
async function initArk() {
|
||||
try {
|
||||
const sqlite = await Database.load('sqlite:lume_v2.db');
|
||||
const _ark = new Ark({ storage: sqlite });
|
||||
|
||||
if (!_ark.account) await _ark.getActiveAccount();
|
||||
|
||||
const settings = await _ark.getAllSettings();
|
||||
let autoUpdater = false;
|
||||
|
||||
if (settings) {
|
||||
settings.forEach((item) => {
|
||||
if (item.key === 'outbox') _ark.settings.outbox = !!parseInt(item.value);
|
||||
|
||||
if (item.key === 'media') _ark.settings.media = !!parseInt(item.value);
|
||||
|
||||
if (item.key === 'hashtag') _ark.settings.hashtag = !!parseInt(item.value);
|
||||
|
||||
if (item.key === 'autoupdate') {
|
||||
if (parseInt(item.value)) autoUpdater = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (autoUpdater) {
|
||||
// check update
|
||||
const update = await check();
|
||||
// install new version
|
||||
if (update) {
|
||||
setIsNewVersion(true);
|
||||
|
||||
await update.downloadAndInstall();
|
||||
await relaunch();
|
||||
}
|
||||
}
|
||||
|
||||
setArk(_ark);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const yes = await ask(`${e}. Click "Yes" to relaunch app`, {
|
||||
title: 'Lume',
|
||||
type: 'error',
|
||||
okLabel: 'Yes',
|
||||
});
|
||||
if (yes) relaunch();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!ark && !isNewVersion) initArk();
|
||||
}, []);
|
||||
|
||||
if (!ark) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
|
||||
>
|
||||
<div className="flex max-w-2xl flex-col items-start gap-1">
|
||||
<h5 className="font-semibold uppercase">TIP:</h5>
|
||||
<Markdown
|
||||
options={{
|
||||
overrides: {
|
||||
a: {
|
||||
props: {
|
||||
className: 'text-blue-500 hover:text-blue-600',
|
||||
target: '_blank',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="text-4xl font-semibold leading-snug text-neutral-300 dark:text-neutral-700"
|
||||
>
|
||||
{QUOTES[Math.floor(Math.random() * QUOTES.length)]}
|
||||
</Markdown>
|
||||
</div>
|
||||
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
|
||||
<LoaderIcon className="h-6 w-6 animate-spin text-blue-500" />
|
||||
<p className="font-semibold">
|
||||
{isNewVersion ? 'Found a new version, updating...' : 'Starting...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ArkContext.Provider value={{ ark }}>{children}</ArkContext.Provider>;
|
||||
};
|
||||
|
||||
const useArk = () => {
|
||||
const context = useContext(ArkContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('Please import Ark Provider to use useArk() hook');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { ArkProvider, useArk };
|
11
src/main.jsx
11
src/main.jsx
@ -3,8 +3,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Toaster } from 'sonner';
|
||||
|
||||
import { NDKProvider } from '@libs/ndk/provider';
|
||||
import { StorageProvider } from '@libs/storage/provider';
|
||||
import { ArkProvider } from '@libs/ark/provider';
|
||||
|
||||
import App from './app';
|
||||
|
||||
@ -23,10 +22,8 @@ root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<Toaster position="top-center" theme="system" closeButton />
|
||||
<StorageProvider>
|
||||
<NDKProvider>
|
||||
<App />
|
||||
</NDKProvider>
|
||||
</StorageProvider>
|
||||
<ArkProvider>
|
||||
<App />
|
||||
</ArkProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
@ -3,26 +3,18 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
export function Logout() {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
ndk.signer = null;
|
||||
|
||||
// remove private key
|
||||
await db.secureRemove(db.account.pubkey);
|
||||
await db.secureRemove(`${db.account.pubkey}-nsecbunker`);
|
||||
|
||||
// logout
|
||||
await db.accountLogout();
|
||||
await ark.logout();
|
||||
|
||||
// clear cache
|
||||
queryClient.clear();
|
||||
|
@ -2,7 +2,7 @@ import { NDKEvent, NDKKind, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import {
|
||||
MemoizedArticleKind,
|
||||
@ -14,24 +14,25 @@ import {
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function Repost({ event }: { event: NDKEvent }) {
|
||||
const { ndk } = useNDK();
|
||||
const { ark } = useArk();
|
||||
const { status, data: repostEvent } = useQuery({
|
||||
queryKey: ['repost', event.id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
let event: NDKEvent = undefined;
|
||||
|
||||
if (event.content.length > 50) {
|
||||
const embed = JSON.parse(event.content) as NostrEvent;
|
||||
const embedEvent = new NDKEvent(ndk, embed);
|
||||
return embedEvent;
|
||||
event = ark.createNDKEvent({ event: embed });
|
||||
}
|
||||
|
||||
const id = event.tags.find((el) => el[0] === 'e')[1];
|
||||
if (!id) throw new Error('Failed to get repost event id');
|
||||
|
||||
const ndkEvent = await ndk.fetchEvent(id);
|
||||
if (!ndkEvent) return Promise.reject(new Error('Failed to get repost event'));
|
||||
event = await ark.getEventById({ id });
|
||||
|
||||
return ndkEvent;
|
||||
if (!event) return Promise.reject(new Error('Failed to get repost event'));
|
||||
return event;
|
||||
} catch {
|
||||
throw new Error('Failed to get repost event');
|
||||
}
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { NDKEvent, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { QueryStatus, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { ChevronUpIcon } from '@shared/icons';
|
||||
|
||||
export function LiveUpdater({ status }: { status: QueryStatus }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const { ark } = useArk();
|
||||
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@ -30,19 +27,16 @@ export function LiveUpdater({ status }: { status: QueryStatus }) {
|
||||
useEffect(() => {
|
||||
let sub: NDKSubscription = undefined;
|
||||
|
||||
if (status === 'success' && db.account && db.account?.follows?.length > 0) {
|
||||
queryClient.fetchQuery({ queryKey: ['notification'] });
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: db.account.contacts,
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
sub = ndk.subscribe(filter, { closeOnEose: false, groupable: false });
|
||||
sub.addListener('event', (event: NDKEvent) =>
|
||||
setEvents((prev) => [...prev, event])
|
||||
);
|
||||
if (status === 'success' && ark.account && ark.account?.contacts?.length > 0) {
|
||||
sub = ark.subscribe({
|
||||
filter: {
|
||||
kinds: [NDKKind.Text, NDKKind.Repost],
|
||||
authors: ark.account.contacts,
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
closeOnEose: false,
|
||||
cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]),
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
import { useArk } from '@libs/ark';
|
||||
|
||||
import { FollowIcon } from '@shared/icons';
|
||||
|
||||
@ -16,9 +14,7 @@ export interface Profile {
|
||||
}
|
||||
|
||||
export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const { ark } = useArk();
|
||||
const [followed, setFollowed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -26,12 +22,10 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
if (!ndk.signer) return navigate('/new/privkey');
|
||||
if (!ark.readyToSign) return navigate('/new/privkey');
|
||||
setFollowed(true);
|
||||
|
||||
const user = ndk.getUser({ pubkey: db.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
const add = await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
const add = ark.createContact({ pubkey });
|
||||
|
||||
if (!add) {
|
||||
toast.success('You already follow this user');
|
||||
@ -44,7 +38,7 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (db.account.contacts.includes(data.pubkey)) {
|
||||
if (ark.account.contacts.includes(data.pubkey)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
@ -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`);
|
||||
|
@ -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(
|
||||
|
@ -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] });
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
@ -46,7 +46,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 +58,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 +90,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;
|
||||
});
|
||||
});
|
||||
|
6
src/utils/types.d.ts
vendored
6
src/utils/types.d.ts
vendored
@ -160,3 +160,9 @@ export interface NIP11 {
|
||||
payments_url: string;
|
||||
icon: string[];
|
||||
}
|
||||
|
||||
export interface NIP05 {
|
||||
names: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user