mirror of
https://github.com/lumehq/lume.git
synced 2025-03-18 05:41:53 +01:00
wip: new onboarding
This commit is contained in:
parent
cd3b9ada5a
commit
3aa4f294f9
@ -19,6 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@evilmartians/harmony": "^1.1.0",
|
||||
"@formkit/auto-animate": "^0.8.0",
|
||||
"@getalby/sdk": "^2.4.0",
|
||||
"@nostr-dev-kit/ndk": "^2.0.2",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.2",
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ dependencies:
|
||||
'@evilmartians/harmony':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@formkit/auto-animate':
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0
|
||||
'@getalby/sdk':
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
@ -838,6 +841,10 @@ packages:
|
||||
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
|
||||
dev: false
|
||||
|
||||
/@formkit/auto-animate@0.8.0:
|
||||
resolution: {integrity: sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw==}
|
||||
dev: false
|
||||
|
||||
/@getalby/sdk@2.4.0:
|
||||
resolution: {integrity: sha512-aIGNwLRF9coj6koxfq7P4GtFZbFjQbnIheix39x9176PwFw4dXOdGXHPXnqioJTmeq80y+vX1yd+u/f03YGoeg==}
|
||||
engines: {node: '>=14'}
|
||||
|
@ -115,7 +115,7 @@ fn main() {
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::default()
|
||||
.add_migrations(
|
||||
"sqlite:lume.db",
|
||||
"sqlite:lume_v2.db",
|
||||
vec![
|
||||
Migration {
|
||||
version: 20230418013219,
|
||||
|
44
src/app.tsx
44
src/app.tsx
@ -3,7 +3,6 @@ import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import { CreateAccountScreen } from '@app/auth/create';
|
||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
||||
import { ChatsScreen } from '@app/chats';
|
||||
import { ErrorScreen } from '@app/error';
|
||||
@ -24,17 +23,10 @@ export default function App() {
|
||||
|
||||
const accountLoader = async () => {
|
||||
try {
|
||||
const totalAccount = await db.checkAccount();
|
||||
|
||||
const onboarding = localStorage.getItem('onboarding');
|
||||
const step = onboarding ? JSON.parse(onboarding).state.step : null;
|
||||
|
||||
// redirect to welcome screen if none user exist
|
||||
const totalAccount = await db.checkAccount();
|
||||
if (totalAccount === 0) return redirect('/auth/welcome');
|
||||
|
||||
// restart onboarding process
|
||||
if (step) return redirect(step);
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
|
||||
@ -169,8 +161,10 @@ export default function App() {
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
element: <CreateAccountScreen />,
|
||||
errorElement: <ErrorScreen />,
|
||||
async lazy() {
|
||||
const { CreateAccountScreen } = await import('@app/auth/create');
|
||||
return { Component: CreateAccountScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'import',
|
||||
@ -179,6 +173,13 @@ export default function App() {
|
||||
return { Component: ImportAccountScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'complete',
|
||||
async lazy() {
|
||||
const { CompleteScreen } = await import('@app/auth/complete');
|
||||
return { Component: CompleteScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'onboarding',
|
||||
element: <OnboardingScreen />,
|
||||
@ -187,30 +188,23 @@ export default function App() {
|
||||
{
|
||||
path: '',
|
||||
async lazy() {
|
||||
const { OnboardStep1Screen } = await import(
|
||||
'@app/auth/onboarding/step-1'
|
||||
const { OnboardingListScreen } = await import(
|
||||
'@app/auth/onboarding/list'
|
||||
);
|
||||
return { Component: OnboardStep1Screen };
|
||||
return { Component: OnboardingListScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step-2',
|
||||
path: 'enrich',
|
||||
async lazy() {
|
||||
const { OnboardStep2Screen } = await import(
|
||||
'@app/auth/onboarding/step-2'
|
||||
const { OnboardEnrichScreen } = await import(
|
||||
'@app/auth/onboarding/enrich'
|
||||
);
|
||||
return { Component: OnboardStep2Screen };
|
||||
return { Component: OnboardEnrichScreen };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'complete',
|
||||
async lazy() {
|
||||
const { CompleteScreen } = await import('@app/auth/complete');
|
||||
return { Component: CompleteScreen };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
21
src/app/auth/components/features/customRelay.tsx
Normal file
21
src/app/auth/components/features/customRelay.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export function CustomRelay() {
|
||||
return (
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-semibold">Personalize relay list</h5>
|
||||
<p className="text-sm">
|
||||
Lume offers some default relays for users who are not familiar with Nostr, but
|
||||
you can consider adding more relays to discover more content.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
21
src/app/auth/components/features/favoriteHashtag.tsx
Normal file
21
src/app/auth/components/features/favoriteHashtag.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export function FavoriteHashtag() {
|
||||
return (
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-semibold">Favorite hashtag</h5>
|
||||
<p className="text-sm">
|
||||
By adding favorite hashtag, Lume will display all contents related to this
|
||||
hashtag as a column
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
3
src/app/auth/components/features/followList.tsx
Normal file
3
src/app/auth/components/features/followList.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function FollowList() {
|
||||
return <div></div>;
|
||||
}
|
21
src/app/auth/components/features/linkList.tsx
Normal file
21
src/app/auth/components/features/linkList.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export function LinkList() {
|
||||
return (
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-semibold">Enable Links</h5>
|
||||
<p className="text-sm">
|
||||
Beside newsfeed from your follows, you will see more content from all people
|
||||
that followed by your follows.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
21
src/app/auth/components/features/nip04.tsx
Normal file
21
src/app/auth/components/features/nip04.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export function NIP04() {
|
||||
return (
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-semibold">Enable direct message (Deprecated)</h5>
|
||||
<p className="text-sm">
|
||||
Send direct message to other user (NIP-04), all messages will be encrypted,
|
||||
but your metadata will be leaked.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1 h-9 w-24 shrink-0 rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
src/app/auth/components/features/suggestFollow.tsx
Normal file
23
src/app/auth/components/features/suggestFollow.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function SuggestFollow() {
|
||||
return (
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-semibold">Enrich your network</h5>
|
||||
<p className="text-sm">
|
||||
Follow more people to stay up to date with everything happening around the
|
||||
world.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/auth/onboarding/enrich"
|
||||
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Check
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
281
src/app/auth/create.tsx
Normal file
281
src/app/auth/create.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import { downloadDir } from '@tauri-apps/api/path';
|
||||
import { message, save } from '@tauri-apps/plugin-dialog';
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { motion } from 'framer-motion';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useState } from 'react';
|
||||
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 { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function CreateAccountScreen() {
|
||||
const [picture, setPicture] = useState('');
|
||||
const [downloaded, setDownloaded] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keys, setKeys] = useState<null | {
|
||||
npub: string;
|
||||
nsec: string;
|
||||
pubkey: string;
|
||||
privkey: string;
|
||||
}>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const svgURI =
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon('lume new account', 90, 50));
|
||||
|
||||
const onSubmit = async (data: {
|
||||
name: string;
|
||||
display_name: string;
|
||||
about: string;
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const profile = {
|
||||
...data,
|
||||
name: data.name,
|
||||
display_name: data.display_name,
|
||||
bio: data.about,
|
||||
};
|
||||
|
||||
const userPrivkey = generatePrivateKey();
|
||||
const userPubkey = getPublicKey(userPrivkey);
|
||||
const userNpub = nip19.npubEncode(userPubkey);
|
||||
const userNsec = nip19.nsecEncode(userPrivkey);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
const signer = new NDKPrivateKeySigner(userPrivkey);
|
||||
|
||||
event.content = JSON.stringify(profile);
|
||||
event.kind = NDKKind.Metadata;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = userPubkey;
|
||||
event.tags = [];
|
||||
|
||||
await event.sign(signer);
|
||||
const publish = await event.publish();
|
||||
|
||||
if (publish) {
|
||||
await db.createAccount(userNpub, userPubkey);
|
||||
await db.secureSave(userPubkey, userPrivkey);
|
||||
setKeys({
|
||||
npub: userNpub,
|
||||
nsec: userNsec,
|
||||
pubkey: userPubkey,
|
||||
privkey: userPrivkey,
|
||||
});
|
||||
setLoading(false);
|
||||
} else {
|
||||
toast('Create account failed');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
return toast(e);
|
||||
}
|
||||
};
|
||||
|
||||
const download = async () => {
|
||||
try {
|
||||
const downloadPath = await downloadDir();
|
||||
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
|
||||
const filePath = await save({
|
||||
defaultPath: downloadPath + '/' + fileName,
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
await writeTextFile(
|
||||
filePath,
|
||||
`Generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`
|
||||
);
|
||||
|
||||
setDownloaded(true);
|
||||
} // else { user cancel action }
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Cannot download account keys', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="absolute left-[8px] top-4">
|
||||
{!keys ? (
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Let's set up your Nostr account.
|
||||
</h1>
|
||||
<div className="flex flex-col gap-3">
|
||||
{!keys ? (
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-semibold">Avatar</span>
|
||||
<div className="relative flex h-36 w-full items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
|
||||
<img
|
||||
src={picture || svgURI}
|
||||
alt="user's avatar"
|
||||
className="h-14 w-14 rounded-lg bg-black object-cover dark:bg-white"
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-semibold">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 1,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-semibold">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-neutral-200 px-3 py-2 !outline-none placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-5 w-4 animate-spin" />
|
||||
) : (
|
||||
'Create and Continue'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
||||
>
|
||||
<User pubkey={keys.pubkey} variant="simple" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 80 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold">Backup account</h5>
|
||||
<div>
|
||||
<p className="mb-2 select-text text-sm text-neutral-800 dark:text-neutral-200">
|
||||
Your private key is your password. If you lose this key, you will
|
||||
lose access to your account! Copy it and keep it in a safe place.{' '}
|
||||
<span className="text-red-500">
|
||||
There is no way to reset your private key.
|
||||
</span>
|
||||
</p>
|
||||
<p className="select-text text-sm text-neutral-800 dark:text-neutral-200">
|
||||
Public key is used for sharing with other people so that they can
|
||||
find you using the public key.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="nsec" className="text-sm font-semibold">
|
||||
Private key
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={keys.nsec}
|
||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="nsec" className="text-sm font-semibold">
|
||||
Public key
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={keys.npub}
|
||||
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!downloaded ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => download()}
|
||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
>
|
||||
Download account keys
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
{downloaded ? (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
type="button"
|
||||
onClick={() => navigate('/auth/onboarding', { state: { newuser: true } })}
|
||||
>
|
||||
Finish
|
||||
</motion.button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function CreateAccountScreen() {
|
||||
return <Outlet />;
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
import { downloadDir } from '@tauri-apps/api/path';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { message, save } from '@tauri-apps/plugin-dialog';
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CopyIcon } from '@shared/icons';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
|
||||
export function CreateStep1Screen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [downloaded, setDownloaded] = useState(false);
|
||||
|
||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nsec = nip19.nsecEncode(privkey);
|
||||
|
||||
const copyPrivkey = async () => {
|
||||
try {
|
||||
await writeText(nsec);
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Cannot copy private key', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const download = async () => {
|
||||
try {
|
||||
const downloadPath = await downloadDir();
|
||||
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
|
||||
const filePath = await save({
|
||||
defaultPath: downloadPath + '/' + fileName,
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
await writeTextFile(
|
||||
filePath,
|
||||
`Generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`
|
||||
);
|
||||
|
||||
setDownloaded(true);
|
||||
} // else { user cancel action }
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Cannot download account keys', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setPubkey(pubkey);
|
||||
|
||||
// save privkey
|
||||
await db.secureSave(privkey, pubkey);
|
||||
|
||||
// save to database
|
||||
await db.createAccount(npub, pubkey);
|
||||
|
||||
// redirect to next step
|
||||
navigate('/auth/create/step-2', { replace: true });
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Something went wrong!', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||
<div>
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
This is your new Nostr account
|
||||
</h1>
|
||||
<p className="mb-2 select-text text-neutral-600 dark:text-neutral-300">
|
||||
Your private key is your password. If you lose this key, you will lose access
|
||||
to your account! Copy it and keep it in a safe place.{' '}
|
||||
<span className="text-red-500">
|
||||
There is no way to reset your private key.
|
||||
</span>
|
||||
</p>
|
||||
<p className="select-text text-neutral-600 dark:text-neutral-300">
|
||||
Public key is used for sharing with other people so that they can find you
|
||||
using the public key.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="nsec"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Private Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
readOnly
|
||||
name="nsec"
|
||||
value={nsec.substring(0, 5) + '**************************************'}
|
||||
className="relative h-12 w-full rounded-lg bg-neutral-200 py-1 pl-3.5 pr-11 text-neutral-900 !outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyPrivkey()}
|
||||
className="group absolute right-2 top-1/2 inline-flex h-7 -translate-y-1/2 transform items-center gap-1.5 rounded-md bg-neutral-300 px-2.5 text-sm text-neutral-800 hover:bg-neutral-400 hover:text-neutral-900 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-600 dark:hover:text-neutral-100"
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="npub"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Public Key
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
name="npub"
|
||||
value={npub}
|
||||
className="relative h-12 w-full rounded-lg bg-neutral-200 px-3.5 py-1 text-neutral-900 !outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => download()}
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{downloaded ? 'Downloaded' : 'Download account keys'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-neutral-200 px-6 font-medium leading-none text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
||||
>
|
||||
{loading ? 'Creating...' : 'Continue'}
|
||||
</button>
|
||||
<span className="select-text text-center text-sm text-neutral-400 dark:text-neutral-600">
|
||||
By clicking 'Continue', you are ensuring that your keys are saved
|
||||
in a safe place. You cannot recover these keys if they are lost.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { BannerUploader } from '@shared/bannerUploader';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function CreateStep2Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
const [banner, setBanner] = useState('');
|
||||
|
||||
const { db } = useStorage();
|
||||
const { publish } = useNostr();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data: { name: string; about: string; website: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profile = {
|
||||
...data,
|
||||
name: data.name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
website: data.website,
|
||||
};
|
||||
|
||||
const event = await publish({
|
||||
content: JSON.stringify(profile),
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// create default widget
|
||||
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||
|
||||
if (event) {
|
||||
navigate('/auth/onboarding', { replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error: ', e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create/step-3');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 border-b border-white/10 pb-4">
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||
Personalize your Nostr profile
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Nostr profile is synchronous across all Nostr clients. If you create a profile
|
||||
on Lume, it will also work well with other Nostr clients. If you update your
|
||||
profile on another Nostr client, it will also sync to Lume.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||
<div className="relative">
|
||||
<div className="relative h-36 w-full bg-white/10 backdrop-blur-xl">
|
||||
{banner ? (
|
||||
<Image
|
||||
src={banner}
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-white/20" />
|
||||
)}
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 px-4">
|
||||
<div className="relative z-10 -mt-8 h-16 w-16">
|
||||
<Image
|
||||
src={picture}
|
||||
alt="user's avatar"
|
||||
className="h-16 w-16 rounded-lg object-cover ring-2 ring-white/20"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium text-white">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 1,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium text-white">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-white/20 px-3 py-2 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium text-white">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
{...register('website', {
|
||||
required: false,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Creating...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Continue</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -7,6 +7,7 @@ import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowLeftIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function ImportAccountScreen() {
|
||||
@ -58,24 +59,34 @@ export function ImportAccountScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const finish = async () => {
|
||||
navigate('/auth/onboarding');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="absolute left-[8px] top-4">
|
||||
{!created ? (
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Import your Nostr account
|
||||
Import your Nostr account.
|
||||
</h1>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="npub" className="font-semibold">
|
||||
Enter your nostr npub:
|
||||
Enter your npub:
|
||||
</label>
|
||||
<div className="inline-flex w-full items-center gap-2">
|
||||
<input
|
||||
name="npub"
|
||||
type="text"
|
||||
value={npub}
|
||||
onChange={(e) => setNpub(e.target.value)}
|
||||
@ -105,7 +116,6 @@ export function ImportAccountScreen() {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{ y: { velocity: -100 } }}
|
||||
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
||||
>
|
||||
<h5 className="mb-1.5 font-semibold">Account found</h5>
|
||||
@ -142,15 +152,15 @@ export function ImportAccountScreen() {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{ y: { velocity: -100 } }}
|
||||
className="rounded-lg bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="npub" className="font-semibold">
|
||||
Enter your nostr nsec (optional):
|
||||
<label htmlFor="nsec" className="font-semibold">
|
||||
Enter your nsec (optional):
|
||||
</label>
|
||||
<div className="inline-flex w-full items-center gap-2">
|
||||
<input
|
||||
name="nsec"
|
||||
type="text"
|
||||
value={nsec}
|
||||
onChange={(e) => setNsec(e.target.value)}
|
||||
@ -204,15 +214,16 @@ export function ImportAccountScreen() {
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={finish}
|
||||
initial={{ opacity: 0, y: 80 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{ y: { velocity: -130 } }}
|
||||
className="h-9 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
navigate('/auth/onboarding', { state: { newuser: false } })
|
||||
}
|
||||
>
|
||||
Finish
|
||||
</motion.button>
|
||||
|
@ -1,21 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { ArrowLeftIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { arrayToNIP02 } from '@utils/transform';
|
||||
|
||||
export function OnboardStep1Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
export function OnboardEnrichScreen() {
|
||||
const { publish, fetchUserData } = useNostr();
|
||||
const { db } = useStorage();
|
||||
const { status, data } = useQuery(['trending-profiles-widget'], async () => {
|
||||
@ -29,6 +24,8 @@ export function OnboardStep1Screen() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [follows, setFollows] = useState([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// toggle follow state
|
||||
const toggleFollow = (pubkey: string) => {
|
||||
const arr = follows.includes(pubkey)
|
||||
@ -59,27 +56,28 @@ export function OnboardStep1Screen() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/onboarding');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-center">
|
||||
<div className="mx-auto mb-4 w-full max-w-md border-b border-white/10 pb-4">
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||
<div className="relative flex h-full w-full flex-col justify-center">
|
||||
<div className="absolute left-[8px] top-4">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="inline-flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div className="mx-auto mb-8 w-full max-w-md px-3">
|
||||
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{loading ? 'Loading...' : 'Enrich your network'}
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Choose the account you want to follow. These accounts are trending in the last
|
||||
24 hours. If none of the accounts interest you, you can explore more options and
|
||||
add them later.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-nowrap items-center gap-4 overflow-x-auto px-4 scrollbar-none">
|
||||
{status === 'loading' ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
|
||||
</div>
|
||||
) : (
|
||||
data?.profiles.map((item: { pubkey: string; profile: { content: string } }) => (
|
||||
@ -87,7 +85,7 @@ export function OnboardStep1Screen() {
|
||||
key={item.pubkey}
|
||||
type="button"
|
||||
onClick={() => toggleFollow(item.pubkey)}
|
||||
className="relative h-[300px] shrink-0 grow-0 basis-[250px] rounded-lg border-t border-white/10 bg-white/20 px-4 py-4 hover:bg-white/30"
|
||||
className="relative h-[300px] shrink-0 grow-0 basis-[250px] overflow-hidden rounded-lg bg-neutral-200 px-4 py-4 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<User
|
||||
pubkey={item.pubkey}
|
||||
@ -96,42 +94,29 @@ export function OnboardStep1Screen() {
|
||||
/>
|
||||
{follows.includes(item.pubkey) && (
|
||||
<div className="absolute right-2 top-2">
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-400" />
|
||||
<CheckCircleIcon className="h-5 w-5 text-teal-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-auto mt-4 w-full max-w-md">
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={loading || follows.length === 0}
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>It might take a bit, please patient...</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-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
|
||||
>
|
||||
Skip, you can add later
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mx-auto mt-8 w-full max-w-md px-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={loading || follows.length === 0}
|
||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
<span>It might take a bit, please patient...</span>
|
||||
</>
|
||||
) : (
|
||||
<span>Follow {follows.length} accounts & Continue</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -1,12 +1,11 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
const data = [
|
||||
@ -32,15 +31,13 @@ const data = [
|
||||
{ hashtag: '#dev' },
|
||||
];
|
||||
|
||||
export function OnboardStep2Screen() {
|
||||
export function OnboardHashtagScreen() {
|
||||
const { db } = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [setStep, clearStep] = useOnboarding((state) => [state.setStep, state.clearStep]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tags, setTags] = useState(new Set<string>());
|
||||
|
||||
const { db } = useStorage();
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
if (tags.has(tag)) {
|
||||
setTags((prev) => {
|
||||
@ -57,9 +54,6 @@ export function OnboardStep2Screen() {
|
||||
// update last login
|
||||
await db.updateLastLogin();
|
||||
|
||||
// clear local storage
|
||||
clearStep();
|
||||
|
||||
navigate('/auth/complete', { replace: true });
|
||||
};
|
||||
|
||||
@ -74,9 +68,6 @@ export function OnboardStep2Screen() {
|
||||
// update last login
|
||||
await db.updateLastLogin();
|
||||
|
||||
// clear local storage
|
||||
clearStep();
|
||||
|
||||
navigate('/auth/complete', { replace: true });
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
@ -84,11 +75,6 @@ export function OnboardStep2Screen() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/onboarding/step-2');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 border-b border-white/10 pb-4">
|
@ -1,9 +1,5 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function OnboardingScreen() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
41
src/app/auth/onboarding/list.tsx
Normal file
41
src/app/auth/onboarding/list.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { CustomRelay } from '@app/auth/components/features/customRelay';
|
||||
import { FavoriteHashtag } from '@app/auth/components/features/favoriteHashtag';
|
||||
import { FollowList } from '@app/auth/components/features/followList';
|
||||
import { LinkList } from '@app/auth/components/features/linkList';
|
||||
import { NIP04 } from '@app/auth/components/features/nip04';
|
||||
import { SuggestFollow } from '@app/auth/components/features/suggestFollow';
|
||||
|
||||
export function OnboardingListScreen() {
|
||||
const { state } = useLocation();
|
||||
const { newuser }: { newuser: boolean } = state;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl text-neutral-900 dark:text-neutral-100">
|
||||
You're almost ready to use Lume.
|
||||
</h1>
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Let's start personalizing your experience.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{newuser ? <SuggestFollow /> : <FollowList />}
|
||||
<FavoriteHashtag />
|
||||
<LinkList />
|
||||
<NIP04 />
|
||||
<CustomRelay />
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
>
|
||||
Continue
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
@ -9,14 +9,12 @@ import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { FULL_RELAYS } from '@stores/constants';
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function OnboardStep3Screen() {
|
||||
export function OnboardRelaysScreen() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [setStep, clearStep] = useOnboarding((state) => [state.setStep, state.clearStep]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [relays, setRelays] = useState(new Set<string>());
|
||||
|
||||
@ -81,7 +79,6 @@ export function OnboardStep3Screen() {
|
||||
// update last login
|
||||
await db.updateLastLogin();
|
||||
|
||||
clearStep();
|
||||
navigate('/', { replace: true });
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
@ -89,11 +86,6 @@ export function OnboardStep3Screen() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/onboarding/step-3');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
@ -28,12 +28,7 @@ const NDKProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
data-tauri-drag-region
|
||||
className="flex h-screen w-screen items-center justify-center bg-neutral-50 dark:bg-neutral-950"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<LoaderIcon className="h-6 w-6 animate-spin text-neutral-950 dark:text-neutral-50" />
|
||||
<h3 className="text-lg font-medium leading-none text-neutral-950 dark:text-neutral-50">
|
||||
Connecting...
|
||||
</h3>
|
||||
</div>
|
||||
<LoaderIcon className="h-8 w-8 animate-spin text-neutral-950 dark:text-neutral-50" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export class LumeStorage {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public async secureSave(value: string, key?: string) {
|
||||
public async secureSave(key: string, value: string) {
|
||||
return await invoke('secure_save', { key, value });
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
|
||||
const initLumeStorage = async () => {
|
||||
try {
|
||||
const sqlite = await Database.load('sqlite:lume.db');
|
||||
const sqlite = await Database.load('sqlite:lume_v2.db');
|
||||
const platformName = await platform();
|
||||
const dir = await appConfigDir();
|
||||
|
||||
|
@ -25,13 +25,14 @@ export function AvatarUploader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className="inline-flex h-full w-full items-center justify-center rounded-lg bg-black/50 hover:bg-black/60"
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-100 px-1.5 py-1 text-sm font-medium text-blue-500 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:bg-blue-800"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<PlusIcon className="h-6 w-6 text-white" />
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
)}
|
||||
Change avatar
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -25,14 +25,16 @@ export function BannerUploader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadBanner()}
|
||||
className="inline-flex h-full w-full flex-col items-center justify-center gap-1 bg-black/40 hover:bg-black/50"
|
||||
className="inline-flex h-full w-full flex-col items-center justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||
<LoaderIcon className="h-6 w-6 animate-spin text-neutral-900 dark:text-neutral-100" />
|
||||
) : (
|
||||
<PlusIcon className="h-6 w-6 text-white" />
|
||||
<PlusIcon className="h-6 w-6 text-neutral-900 dark:text-neutral-100" />
|
||||
)}
|
||||
<p className="text-sm font-medium text-white/70">Add a banner image</p>
|
||||
<p className="text-sm font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
Add cover
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,11 @@ export function AuthLayout() {
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-neutral-50 dark:bg-neutral-950">
|
||||
{db.platform !== 'macos' ? <WindowTitlebar /> : null}
|
||||
{db.platform !== 'macos' ? (
|
||||
<WindowTitlebar />
|
||||
) : (
|
||||
<div data-tauri-drag-region className="h-9" />
|
||||
)}
|
||||
<div className="h-full w-full">
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
|
@ -3,14 +3,7 @@ import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { ActiveAccount } from '@shared/accounts/active';
|
||||
import { ComposerModal } from '@shared/composer';
|
||||
import {
|
||||
ChatsIcon,
|
||||
CommunityIcon,
|
||||
ExploreIcon,
|
||||
HomeIcon,
|
||||
NwcIcon,
|
||||
RelayIcon,
|
||||
} from '@shared/icons';
|
||||
import { ChatsIcon, ExploreIcon, HomeIcon, NwcIcon, RelayIcon } from '@shared/icons';
|
||||
|
||||
export function Navigation() {
|
||||
return (
|
||||
@ -58,27 +51,6 @@ export function Navigation() {
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/communities"
|
||||
preventScrollReset={true}
|
||||
className="inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
'inline-flex aspect-square h-auto w-full items-center justify-center rounded-lg',
|
||||
isActive
|
||||
? 'bg-black/10 text-black dark:bg-white/10 dark:text-white'
|
||||
: 'text-black/50 dark:text-white/50'
|
||||
)}
|
||||
>
|
||||
<CommunityIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-sm text-black dark:text-white">Groups</div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/relays"
|
||||
preventScrollReset={true}
|
||||
|
@ -1,38 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
interface OnboardingState {
|
||||
step: null | string;
|
||||
pubkey: null | string;
|
||||
tempPrivkey: null | string;
|
||||
setPubkey: (pubkey: string) => void;
|
||||
setTempPrivkey: (privkey: string) => void;
|
||||
setStep: (url: string) => void;
|
||||
clearStep: () => void;
|
||||
}
|
||||
|
||||
export const useOnboarding = create<OnboardingState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
step: null,
|
||||
pubkey: null,
|
||||
tempPrivkey: null,
|
||||
setPubkey: (pubkey: string) => {
|
||||
set({ pubkey });
|
||||
},
|
||||
setTempPrivkey: (privkey: string) => {
|
||||
set({ tempPrivkey: privkey });
|
||||
},
|
||||
setStep: (url: string) => {
|
||||
set({ step: url });
|
||||
},
|
||||
clearStep: () => {
|
||||
set({ step: null, pubkey: null, tempPrivkey: null });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'onboarding',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user