mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
wip: add relay discover to onboarding
This commit is contained in:
parent
9c7b58ee99
commit
e6c6793f6e
10
package.json
10
package.json
@ -21,7 +21,7 @@
|
|||||||
"@noble/ciphers": "^0.2.0",
|
"@noble/ciphers": "^0.2.0",
|
||||||
"@noble/curves": "^1.1.0",
|
"@noble/curves": "^1.1.0",
|
||||||
"@noble/hashes": "^1.3.1",
|
"@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",
|
"@nostr-fetch/adapter-ndk": "^0.11.0",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
@ -57,7 +57,7 @@
|
|||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
"destr": "^1.2.2",
|
"destr": "^1.2.2",
|
||||||
"framer-motion": "^10.15.0",
|
"framer-motion": "^10.15.1",
|
||||||
"get-urls": "^11.0.0",
|
"get-urls": "^11.0.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"tauri-controls": "^0.0.5",
|
"tauri-controls": "^0.0.5",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"zustand": "^4.4.0"
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
@ -87,7 +87,7 @@
|
|||||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||||
"@types/html-to-text": "^9.0.1",
|
"@types/html-to-text": "^9.0.1",
|
||||||
"@types/node": "^18.17.3",
|
"@types/node": "^18.17.3",
|
||||||
"@types/react": "^18.2.18",
|
"@types/react": "^18.2.19",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
@ -112,7 +112,7 @@
|
|||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.4.8",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
"vite-plugin-top-level-await": "^1.3.1",
|
||||||
"vite-tsconfig-paths": "^4.2.0"
|
"vite-tsconfig-paths": "^4.2.0"
|
||||||
}
|
}
|
||||||
|
806
pnpm-lock.yaml
806
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
10
src-tauri/migrations/20230808085847_add_relays_table.sql
Normal file
10
src-tauri/migrations/20230808085847_add_relays_table.sql
Normal 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)
|
||||||
|
);
|
@ -105,6 +105,12 @@ fn main() {
|
|||||||
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
|
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
|
||||||
kind: MigrationKind::Up,
|
kind: MigrationKind::Up,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230808085847,
|
||||||
|
description: "add relays",
|
||||||
|
sql: include_str!("../migrations/20230808085847_add_relays_table.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
13
src/app.tsx
13
src/app.tsx
@ -10,6 +10,9 @@ import { ImportStep2Screen } from '@app/auth/import/step-2';
|
|||||||
import { ImportStep3Screen } from '@app/auth/import/step-3';
|
import { ImportStep3Screen } from '@app/auth/import/step-3';
|
||||||
import { MigrateScreen } from '@app/auth/migrate';
|
import { MigrateScreen } from '@app/auth/migrate';
|
||||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
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 { ResetScreen } from '@app/auth/reset';
|
||||||
import { UnlockScreen } from '@app/auth/unlock';
|
import { UnlockScreen } from '@app/auth/unlock';
|
||||||
import { WelcomeScreen } from '@app/auth/welcome';
|
import { WelcomeScreen } from '@app/auth/welcome';
|
||||||
@ -77,7 +80,6 @@ const router = createBrowserRouter([
|
|||||||
element: <AuthLayout />,
|
element: <AuthLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ path: 'welcome', element: <WelcomeScreen /> },
|
{ path: 'welcome', element: <WelcomeScreen /> },
|
||||||
{ path: 'onboarding', element: <OnboardingScreen /> },
|
|
||||||
{
|
{
|
||||||
path: 'import',
|
path: 'import',
|
||||||
element: <AuthImportScreen />,
|
element: <AuthImportScreen />,
|
||||||
@ -96,6 +98,15 @@ const router = createBrowserRouter([
|
|||||||
{ path: 'step-3', element: <CreateStep3Screen /> },
|
{ 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: 'unlock', element: <UnlockScreen /> },
|
||||||
{ path: 'migrate', element: <MigrateScreen /> },
|
{ path: 'migrate', element: <MigrateScreen /> },
|
||||||
{ path: 'reset', element: <ResetScreen /> },
|
{ path: 'reset', element: <ResetScreen /> },
|
||||||
|
@ -3,7 +3,7 @@ import { Image } from '@shared/image';
|
|||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
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 }) {
|
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
|
||||||
const { status, user } = useProfile(pubkey, fallback);
|
const { status, user } = useProfile(pubkey, fallback);
|
||||||
@ -32,10 +32,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
<span className="truncate font-medium leading-tight text-white">
|
<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>
|
||||||
<span className="max-w-[15rem] truncate text-base leading-tight text-white/50">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
39
src/app/auth/components/userRelay.tsx
Normal file
39
src/app/auth/components/userRelay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -14,7 +15,9 @@ import { usePublish } from '@utils/hooks/usePublish';
|
|||||||
|
|
||||||
export function CreateStep3Screen() {
|
export function CreateStep3Screen() {
|
||||||
const { publish } = usePublish();
|
const { publish } = usePublish();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
||||||
@ -43,8 +46,10 @@ export function CreateStep3Screen() {
|
|||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(['currentAccount']);
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
setTimeout(() => navigate('/auth/create/step-4', { replace: true }), 1000);
|
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1000);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error: ', e);
|
console.log('error: ', e);
|
||||||
|
@ -6,8 +6,7 @@ import { User } from '@app/auth/components/user';
|
|||||||
|
|
||||||
import { updateLastLogin } from '@libs/storage';
|
import { updateLastLogin } from '@libs/storage';
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
@ -33,7 +32,7 @@ export function ImportStep3Screen() {
|
|||||||
|
|
||||||
queryClient.invalidateQueries(['currentAccount']);
|
queryClient.invalidateQueries(['currentAccount']);
|
||||||
|
|
||||||
navigate('/', { replace: true });
|
navigate('/auth/onboarding/step-2', { replace: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error: ', e);
|
console.log('error: ', e);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -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'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'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>
|
|
||||||
);
|
|
||||||
}
|
|
9
src/app/auth/onboarding/index.tsx
Normal file
9
src/app/auth/onboarding/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
123
src/app/auth/onboarding/step-1.tsx
Normal file
123
src/app/auth/onboarding/step-1.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
117
src/app/auth/onboarding/step-2.tsx
Normal file
117
src/app/auth/onboarding/step-2.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
172
src/app/auth/onboarding/step-3.tsx
Normal file
172
src/app/auth/onboarding/step-3.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,30 +1,38 @@
|
|||||||
// source: https://github.com/nostr-dev-kit/ndk-react/
|
// 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 { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||||
import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
|
import { NostrFetcher } from 'nostr-fetch';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import TauriAdapter from '@libs/ndk/cache';
|
import TauriAdapter from '@libs/ndk/cache';
|
||||||
import { getSetting } from '@libs/storage';
|
import { getExplicitRelayUrls } from '@libs/storage';
|
||||||
|
|
||||||
const setting = await getSetting('relays');
|
import { FULL_RELAYS } from '@stores/constants';
|
||||||
const relays = normalizeRelayUrlSet(JSON.parse(setting));
|
|
||||||
|
|
||||||
export const NDKInstance = () => {
|
export const NDKInstance = () => {
|
||||||
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
|
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 [fetcher, setFetcher] = useState<NostrFetcher>(undefined);
|
||||||
|
const [cacheAdapter] = useState(new TauriAdapter());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cacheAdapter = new TauriAdapter();
|
loadNdk();
|
||||||
loadNdk(relays, cacheAdapter);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cacheAdapter.save();
|
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 });
|
const ndkInstance = new NDK({ explicitRelayUrls, cacheAdapter });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -9,7 +9,7 @@ interface NDKContext {
|
|||||||
ndk: NDK;
|
ndk: NDK;
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
fetcher: NostrFetcher;
|
fetcher: NostrFetcher;
|
||||||
loadNdk: (_: string[]) => void;
|
loadNdk: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NDKContext = createContext<NDKContext>({
|
const NDKContext = createContext<NDKContext>({
|
||||||
|
@ -3,7 +3,15 @@ import destr from 'destr';
|
|||||||
|
|
||||||
import { parser } from '@utils/parser';
|
import { parser } from '@utils/parser';
|
||||||
import { getParentID } from '@utils/transform';
|
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;
|
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}";`
|
`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}";`);
|
||||||
|
}
|
||||||
|
@ -48,4 +48,5 @@ export * from './thread';
|
|||||||
export * from './strangers';
|
export * from './strangers';
|
||||||
export * from './download';
|
export * from './download';
|
||||||
export * from './horizontalDots';
|
export * from './horizontalDots';
|
||||||
|
export * from './arrowRightCircle';
|
||||||
// @endindex
|
// @endindex
|
||||||
|
@ -66,8 +66,6 @@ export const OPENGRAPH = {
|
|||||||
export const FULL_RELAYS = [
|
export const FULL_RELAYS = [
|
||||||
'wss://relayable.org',
|
'wss://relayable.org',
|
||||||
'wss://relay.damus.io',
|
'wss://relay.damus.io',
|
||||||
'wss://relay.nostrgraph.net',
|
|
||||||
'wss://relay.nostr.band/all',
|
|
||||||
'wss://nostr.mutinywallet.com',
|
'wss://nostr.mutinywallet.com',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
7
src/utils/types.d.ts
vendored
7
src/utils/types.d.ts
vendored
@ -55,3 +55,10 @@ export interface Settings {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Relays {
|
||||||
|
id?: string;
|
||||||
|
account_id?: number;
|
||||||
|
relay: string;
|
||||||
|
purpose?: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user