wip: add relay discover to onboarding

This commit is contained in:
Ren Amamiya 2023-08-08 18:52:46 +07:00
parent 9c7b58ee99
commit e6c6793f6e
20 changed files with 974 additions and 535 deletions

View File

@ -21,7 +21,7 @@
"@noble/ciphers": "^0.2.0",
"@noble/curves": "^1.1.0",
"@noble/hashes": "^1.3.1",
"@nostr-dev-kit/ndk": "^0.8.3",
"@nostr-dev-kit/ndk": "^0.8.7",
"@nostr-fetch/adapter-ndk": "^0.11.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
@ -57,7 +57,7 @@
"cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.9",
"destr": "^1.2.2",
"framer-motion": "^10.15.0",
"framer-motion": "^10.15.1",
"get-urls": "^11.0.0",
"html-to-text": "^9.0.5",
"immer": "^10.0.2",
@ -79,7 +79,7 @@
"remark-gfm": "^3.0.1",
"tauri-controls": "^0.0.5",
"tippy.js": "^6.3.7",
"zustand": "^4.4.0"
"zustand": "^4.4.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
@ -87,7 +87,7 @@
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.1",
"@types/node": "^18.17.3",
"@types/react": "^18.2.18",
"@types/react": "^18.2.19",
"@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^5.62.0",
@ -112,7 +112,7 @@
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"typescript": "^4.9.5",
"vite": "^4.4.8",
"vite": "^4.4.9",
"vite-plugin-top-level-await": "^1.3.1",
"vite-tsconfig-paths": "^4.2.0"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
-- Add migration script here
CREATE TABLE
relays (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
relay TEXT NOT NULL,
purpose TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);

View File

@ -105,6 +105,12 @@ fn main() {
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230808085847,
description: "add relays",
sql: include_str!("../migrations/20230808085847_add_relays_table.sql"),
kind: MigrationKind::Up,
},
],
)
.build(),

View File

@ -10,6 +10,9 @@ import { ImportStep2Screen } from '@app/auth/import/step-2';
import { ImportStep3Screen } from '@app/auth/import/step-3';
import { MigrateScreen } from '@app/auth/migrate';
import { OnboardingScreen } from '@app/auth/onboarding';
import { OnboardStep1Screen } from '@app/auth/onboarding/step-1';
import { OnboardStep2Screen } from '@app/auth/onboarding/step-2';
import { OnboardStep3Screen } from '@app/auth/onboarding/step-3';
import { ResetScreen } from '@app/auth/reset';
import { UnlockScreen } from '@app/auth/unlock';
import { WelcomeScreen } from '@app/auth/welcome';
@ -77,7 +80,6 @@ const router = createBrowserRouter([
element: <AuthLayout />,
children: [
{ path: 'welcome', element: <WelcomeScreen /> },
{ path: 'onboarding', element: <OnboardingScreen /> },
{
path: 'import',
element: <AuthImportScreen />,
@ -96,6 +98,15 @@ const router = createBrowserRouter([
{ path: 'step-3', element: <CreateStep3Screen /> },
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,
children: [
{ path: '', element: <OnboardStep1Screen /> },
{ path: 'step-2', element: <OnboardStep2Screen /> },
{ path: 'step-3', element: <OnboardStep3Screen /> },
],
},
{ path: 'unlock', element: <UnlockScreen /> },
{ path: 'migrate', element: <MigrateScreen /> },
{ path: 'reset', element: <ResetScreen /> },

View File

@ -3,7 +3,7 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { displayNpub } from '@utils/shortenKey';
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
const { status, user } = useProfile(pubkey, fallback);
@ -32,10 +32,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-white">
{user?.name || user?.displayName || user?.display_name}
{user?.name || user?.display_name || user?.nip05}
</span>
<span className="max-w-[15rem] truncate text-base leading-tight text-white/50">
{user?.nip05?.toLowerCase() || shortenKey(pubkey)}
{user?.nip05?.toLowerCase() || displayNpub(pubkey, 16)}
</span>
</div>
</div>

View File

@ -0,0 +1,39 @@
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
export function UserRelay({ pubkey }: { pubkey: string }) {
const { status, user } = useProfile(pubkey);
if (status === 'loading') {
return (
<div className="flex items-center gap-2">
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-white/10" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10" />
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10" />
</div>
</div>
);
}
return (
<div className="inline-flex items-center gap-2 text-white/50">
<span className="text-sm">Use by</span>
<div className="inline-flex items-center gap-1">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="h-5 w-5 shrink-0 rounded object-cover"
/>
<span className="truncate text-sm font-medium leading-none text-white">
{user?.name || user?.display_name || user?.nip05 || displayNpub(pubkey, 16)}
</span>
</div>
</div>
);
}

View File

@ -1,3 +1,4 @@
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@ -14,7 +15,9 @@ import { usePublish } from '@utils/hooks/usePublish';
export function CreateStep3Screen() {
const { publish } = usePublish();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState(DEFAULT_AVATAR);
@ -43,8 +46,10 @@ export function CreateStep3Screen() {
tags: [],
});
queryClient.invalidateQueries(['currentAccount']);
if (event) {
setTimeout(() => navigate('/auth/create/step-4', { replace: true }), 1000);
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1000);
}
} catch (e) {
console.log('error: ', e);

View File

@ -6,8 +6,7 @@ import { User } from '@app/auth/components/user';
import { updateLastLogin } from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { useAccount } from '@utils/hooks/useAccount';
import { useNostr } from '@utils/hooks/useNostr';
@ -33,7 +32,7 @@ export function ImportStep3Screen() {
queryClient.invalidateQueries(['currentAccount']);
navigate('/', { replace: true });
navigate('/auth/onboarding/step-2', { replace: true });
} catch (e) {
console.log('error: ', e);
setLoading(false);

View File

@ -1,100 +0,0 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { User } from '@shared/user';
import { useAccount } from '@utils/hooks/useAccount';
import { usePublish } from '@utils/hooks/usePublish';
export function OnboardingScreen() {
const navigate = useNavigate();
const { publish } = usePublish();
const { status, account } = useAccount();
const [loading, setLoading] = useState(false);
const submit = async () => {
try {
setLoading(true);
// publish event
publish({
content: 'Running Lume, join with me #nostr #lume : https://lume.nu',
kind: 1,
tags: [],
});
// redirect to home
setTimeout(() => navigate('/', { replace: true }), 1200);
} catch (error) {
console.log(error);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<div className="mb-4 text-center">
<h1 className="mb-2 text-xl font-semibold text-white">
👋 Hello, welcome you to Lume
</h1>
<p className="text-sm text-zinc-300">
You&apos;re a part of Nostr community now
</p>
<p className="text-sm text-zinc-300">
If Lume gets your attention, please help us spread it and don&apos;t forget
invite your friend join with you, we can have fun togother
</p>
</div>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full px-5 py-3">
{status === 'success' && (
<User pubkey={account.pubkey} time={Math.floor(Date.now() / 1000)} />
)}
<div className="-mt-6 select-text whitespace-pre-line break-words pl-[49px] text-base text-white">
<p>Running Lume, join with me #nostr #lume</p>
<a
href="https://lume.nu"
className="font-normal text-fuchsia-500 no-underline hover:text-fuchsia-600"
target="_blank"
rel="noreferrer"
>
https://lume.nu
</a>
</div>
</div>
</div>
<div className="mt-4 flex w-full flex-col gap-2">
<button
type="button"
onClick={() => submit()}
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-white hover:bg-fuchsia-600"
>
{loading ? (
<>
<span className="w-5" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
<span className="w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Spread</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
<Link
to="/"
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-zinc-800 px-6 font-medium text-zinc-300 hover:bg-zinc-900"
>
Skip
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { Outlet } from 'react-router-dom';
export function OnboardingScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />
</div>
);
}

View File

@ -0,0 +1,123 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
import { updateAccount } from '@libs/storage';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { useAccount } from '@utils/hooks/useAccount';
import { usePublish } from '@utils/hooks/usePublish';
import { arrayToNIP02 } from '@utils/transform';
export function OnboardStep1Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { publish } = usePublish();
const { account } = useAccount();
const { status, data } = useQuery(['trending-profiles'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) {
throw new Error('Error');
}
return res.json();
});
const [loading, setLoading] = useState(false);
const [follows, setFollows] = useState([]);
// toggle follow state
const toggleFollow = (pubkey: string) => {
const arr = follows.includes(pubkey)
? follows.filter((i) => i !== pubkey)
: [...follows, pubkey];
setFollows(arr);
};
const submit = async () => {
try {
setLoading(true);
const tags = arrayToNIP02([...follows, account.pubkey]);
const event = await publish({ content: '', kind: 3, tags: tags });
await updateAccount('follows', follows);
// redirect to next step
if (event) {
setTimeout(() => {
queryClient.invalidateQueries(['currentAccount']);
navigate('/auth/onboarding/step-2', { replace: true });
}, 1000);
}
} catch {
console.log('error');
}
};
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-white">Enrich your network</h1>
<p className="text-sm text-white/50">Choose account you want to follow</p>
</div>
<div className="flex flex-col gap-4">
<div className="scrollbar-hide flex h-[500px] w-full flex-col overflow-y-auto rounded-xl bg-white/10">
{status === 'loading' ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
) : (
data?.profiles.map(
(item: { pubkey: string; profile: { content: string } }) => (
<button
key={item.pubkey}
type="button"
onClick={() => toggleFollow(item.pubkey)}
className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 hover:bg-white/20"
>
<User pubkey={item.pubkey} fallback={item.profile?.content} />
{follows.includes(item.pubkey) && (
<div>
<CheckCircleIcon className="h-4 w-4 text-green-400" />
</div>
)}
</button>
)
)
)}
</div>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={submit}
disabled={loading || follows.length === 0}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
>
{loading ? (
<>
<span className="w-5" />
<span>Creating...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Follow {follows.length} accounts & Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
<Link
to="/auth/onboarding/step-2"
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white hover:bg-white/10 focus:outline-none"
>
Skip, you can add later
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,117 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { createBlock } from '@libs/storage';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { BLOCK_KINDS } from '@stores/constants';
const data = [
{ hashtag: '#bitcoin' },
{ hashtag: '#nostr' },
{ hashtag: '#zap' },
{ hashtag: '#LFG' },
{ hashtag: '#zapchain' },
{ hashtag: '#plebchain' },
{ hashtag: '#nodes' },
{ hashtag: '#hodl' },
{ hashtag: '#stacksats' },
{ hashtag: '#nokyc' },
{ hashtag: '#anime' },
{ hashtag: '#waifu' },
{ hashtag: '#manga' },
{ hashtag: '#nostriches' },
{ hashtag: '#dev' },
];
export function OnboardStep2Screen() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [tags, setTags] = useState(new Set<string>());
const toggleTag = (tag: string) => {
if (tags.has(tag)) {
setTags((prev) => {
prev.delete(tag);
return new Set(prev);
});
} else {
if (tags.size >= 3) return;
setTags((prev) => new Set(prev.add(tag)));
}
};
const submit = async () => {
try {
setLoading(true);
for (const tag of tags) {
await createBlock(BLOCK_KINDS.hashtag, tag, tag.replace('#', ''));
}
setTimeout(() => navigate('/auth/onboarding/step-3', { replace: true }), 1000);
} catch {
console.log('error');
}
};
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-white">
Choose {tags.size}/3 your favorite tags
</h1>
<p className="text-sm text-white/50">Customize your space which hashtag widget</p>
</div>
<div className="flex flex-col gap-4">
<div className="scrollbar-hide flex h-[500px] w-full flex-col overflow-y-auto rounded-xl bg-white/10">
{data.map((item: { hashtag: string }) => (
<button
key={item.hashtag}
type="button"
onClick={() => toggleTag(item.hashtag)}
className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 hover:bg-white/20"
>
<p className="text-white">{item.hashtag}</p>
{tags.has(item.hashtag) && (
<div>
<CheckCircleIcon className="h-4 w-4 text-green-400" />
</div>
)}
</button>
))}
</div>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={submit}
disabled={loading || tags.size === 0 || tags.size > 3}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
>
{loading ? (
<>
<span className="w-5" />
<span>Creating...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Add {tags.size} tags & Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
<Link
to="/auth/onboarding/step-3"
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white hover:bg-white/10 focus:outline-none"
>
Skip, you can add later
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,172 @@
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { UserRelay } from '@app/auth/components/userRelay';
import { useNDK } from '@libs/ndk/provider';
import { createRelay } from '@libs/storage';
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { FULL_RELAYS } from '@stores/constants';
import { useAccount } from '@utils/hooks/useAccount';
import { usePublish } from '@utils/hooks/usePublish';
export function OnboardStep3Screen() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [relays, setRelays] = useState(new Set<string>());
const { publish } = usePublish();
const { account } = useAccount();
const { fetcher, relayUrls } = useNDK();
const { status, data } = useQuery(
['relays'],
async () => {
const tmp = new Map<string, string>();
const events = await fetcher.fetchAllEvents(
relayUrls,
{ kinds: [10002], authors: account.follows },
{ since: 0 }
);
if (events) {
events.forEach((event) => {
event.tags.forEach((tag) => {
tmp.set(tag[1], event.pubkey);
});
});
}
return tmp;
},
{
enabled: account ? true : false,
}
);
const toggleRelay = (relay: string) => {
if (relays.has(relay)) {
setRelays((prev) => {
prev.delete(relay);
return new Set(prev);
});
} else {
setRelays((prev) => new Set(prev.add(relay)));
}
};
const submit = async (skip?: boolean) => {
setLoading(true);
try {
if (!skip) {
for (const relay of relays) {
await createRelay(relay);
}
const tags = Array.from(relays).map((relay) => ['r', relay.replace(/\/+$/, '')]);
await publish({ content: '', kind: 10002, tags: tags });
} else {
for (const relay of FULL_RELAYS) {
await createRelay(relay);
}
}
setTimeout(() => {
navigate('/', { replace: true });
}, 1000);
} catch (e) {
setLoading(false);
console.log('error: ', e);
}
};
const relaysAsArray = Array.from(data?.keys() || []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-white">Relay discovery</h1>
<p className="text-sm text-white/50">
You can add relay which is using by who you&apos;re following to easier reach
their content. Learn more about relay{' '}
<a
href="https://nostr.com/relays"
target="_blank"
rel="noreferrer"
className="text-fuchsia-500 underline"
>
here (nostr.com)
</a>
</p>
</div>
<div className="flex flex-col gap-4">
<div className="scrollbar-hide relative flex h-[500px] w-full flex-col divide-y divide-white/10 overflow-y-auto rounded-xl bg-white/10">
{status === 'loading' ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
) : (
relaysAsArray.map((item, index) => (
<button
key={item + index}
type="button"
onClick={() => toggleRelay(item)}
className="inline-flex transform items-start justify-between bg-white/10 px-4 py-2 hover:bg-white/20"
>
<div className="flex flex-col items-start gap-1">
{item.replace(/\/+$/, '')}
<UserRelay pubkey={data.get(item)} />
</div>
{relays.has(item) && (
<div className="pt-1.5">
<CheckCircleIcon className="h-4 w-4 text-green-400" />
</div>
)}
</button>
))
)}
{relays.size > 5 && (
<div className="sticky bottom-0 left-0 inline-flex w-full items-center justify-center bg-white/10 px-4 py-2 backdrop-blur-2xl">
<p className="text-sm text-orange-400">
Using too much relay can cause high resource usage
</p>
</div>
)}
</div>
<div className="flex flex-col gap-2">
<button
type="button"
disabled={loading}
onClick={() => submit()}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
>
{loading ? (
<>
<span className="w-5" />
<span>Creating...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Add {relays.size} relays & Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
<button
type="button"
onClick={() => submit(true)}
className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white hover:bg-white/10 focus:outline-none"
>
Skip, use default relays
</button>
</div>
</div>
</div>
);
}

View File

@ -1,30 +1,38 @@
// source: https://github.com/nostr-dev-kit/ndk-react/
import NDK, { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
import NDK from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
import { NostrFetcher } from 'nostr-fetch';
import { useEffect, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
import { getSetting } from '@libs/storage';
import { getExplicitRelayUrls } from '@libs/storage';
const setting = await getSetting('relays');
const relays = normalizeRelayUrlSet(JSON.parse(setting));
import { FULL_RELAYS } from '@stores/constants';
export const NDKInstance = () => {
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
const [relayUrls, setRelayUrls] = useState<string[]>(relays);
const [relayUrls, setRelayUrls] = useState<string[]>([]);
const [fetcher, setFetcher] = useState<NostrFetcher>(undefined);
const [cacheAdapter] = useState(new TauriAdapter());
useEffect(() => {
const cacheAdapter = new TauriAdapter();
loadNdk(relays, cacheAdapter);
loadNdk();
return () => {
cacheAdapter.save();
};
}, [relays]);
}, []);
async function loadNdk() {
let explicitRelayUrls: string[];
const explicitRelayUrlsFromDB = await getExplicitRelayUrls();
if (explicitRelayUrlsFromDB) {
explicitRelayUrls = explicitRelayUrlsFromDB;
} else {
explicitRelayUrls = FULL_RELAYS;
}
async function loadNdk(explicitRelayUrls: string[], cacheAdapter: NDKCacheAdapter) {
const ndkInstance = new NDK({ explicitRelayUrls, cacheAdapter });
try {

View File

@ -9,7 +9,7 @@ interface NDKContext {
ndk: NDK;
relayUrls: string[];
fetcher: NostrFetcher;
loadNdk: (_: string[]) => void;
loadNdk: () => void;
}
const NDKContext = createContext<NDKContext>({

View File

@ -3,7 +3,15 @@ import destr from 'destr';
import { parser } from '@utils/parser';
import { getParentID } from '@utils/transform';
import { Account, Block, Chats, LumeEvent, Profile, Settings } from '@utils/types';
import {
Account,
Block,
Chats,
LumeEvent,
Profile,
Relays,
Settings,
} from '@utils/types';
let db: null | Database = null;
@ -507,3 +515,39 @@ export async function removePrivkey() {
`UPDATE accounts SET privkey = "privkey is stored in secure storage" WHERE id = "${activeAccount.id}";`
);
}
// get relays
export async function getRelays() {
const db = await connect();
const activeAccount = await getActiveAccount();
return (await db.select(
`SELECT * FROM relays WHERE account_id = "${activeAccount.id}";`
)) as Relays[];
}
// get relays
export async function getExplicitRelayUrls() {
const db = await connect();
const activeAccount = await getActiveAccount();
const result: Relays[] = await db.select(
`SELECT * FROM relays WHERE account_id = "${activeAccount.id}";`
);
if (result.length > 0) return result.map((el) => el.relay);
return null;
}
// create relay
export async function createRelay(relay: string, purpose?: string) {
const db = await connect();
const activeAccount = await getActiveAccount();
return await db.execute(
'INSERT OR IGNORE INTO blocks (account_id, relay, purpose) VALUES (?, ?, ?);',
[activeAccount.id, relay, purpose || '']
);
}
// remove relay
export async function removeRelay(relay: string) {
const db = await connect();
return await db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
}

View File

@ -48,4 +48,5 @@ export * from './thread';
export * from './strangers';
export * from './download';
export * from './horizontalDots';
export * from './arrowRightCircle';
// @endindex

View File

@ -66,8 +66,6 @@ export const OPENGRAPH = {
export const FULL_RELAYS = [
'wss://relayable.org',
'wss://relay.damus.io',
'wss://relay.nostrgraph.net',
'wss://relay.nostr.band/all',
'wss://nostr.mutinywallet.com',
];

View File

@ -55,3 +55,10 @@ export interface Settings {
key: string;
value: string;
}
export interface Relays {
id?: string;
account_id?: number;
relay: string;
purpose?: string;
}