feat: update create account flow

This commit is contained in:
reya 2024-01-07 16:39:05 +07:00
parent 7554a35c31
commit 87099c6388
14 changed files with 486 additions and 833 deletions

View File

@ -59,6 +59,7 @@
"react-router-dom": "^6.21.1", "react-router-dom": "^6.21.1",
"smol-toml": "^1.1.3", "smol-toml": "^1.1.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"unique-names-generator": "^4.7.1",
"virtua": "^0.18.1" "virtua": "^0.18.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,7 +1,6 @@
import { useArk, useStorage } from "@lume/ark"; import { useArk, useStorage } from "@lume/ark";
import { LoaderIcon } from "@lume/icons"; import { LoaderIcon } from "@lume/icons";
import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui"; import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { fetch } from "@tauri-apps/plugin-http"; import { fetch } from "@tauri-apps/plugin-http";
import { import {
RouterProvider, RouterProvider,
@ -24,7 +23,7 @@ export default function Router() {
element: <HomeLayout />, element: <HomeLayout />,
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
loader: async () => { loader: async () => {
if (!storage.account) return redirect("auth/welcome"); if (!storage.account) return redirect("auth");
return null; return null;
}, },
children: [ children: [
@ -171,7 +170,7 @@ export default function Router() {
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
children: [ children: [
{ {
path: "welcome", index: true,
async lazy() { async lazy() {
const { WelcomeScreen } = await import("./routes/auth/welcome"); const { WelcomeScreen } = await import("./routes/auth/welcome");
return { Component: WelcomeScreen }; return { Component: WelcomeScreen };
@ -180,23 +179,7 @@ export default function Router() {
{ {
path: "create", path: "create",
loader: async () => { loader: async () => {
const trusted: NDKEvent[] = []; return await ark.getOAuthServices();
const services = await ark.ndk.fetchEvents({
kinds: [NDKKind.AppHandler],
"#k": ["24133"],
});
for (const service of services) {
const nip05 = JSON.parse(service.content).nip05;
const validate = await ark.validateNIP05({
pubkey: service.pubkey,
nip05,
});
if (validate) trusted.push(service);
}
return trusted;
}, },
async lazy() { async lazy() {
const { CreateAccountScreen } = await import( const { CreateAccountScreen } = await import(
@ -205,15 +188,6 @@ export default function Router() {
return { Component: CreateAccountScreen }; return { Component: CreateAccountScreen };
}, },
}, },
{
path: "create-profile",
async lazy() {
const { CreateProfileScreen } = await import(
"./routes/auth/create-profile"
);
return { Component: CreateProfileScreen };
},
},
{ {
path: "import", path: "import",
async lazy() { async lazy() {
@ -232,20 +206,6 @@ export default function Router() {
return { Component: OnboardingScreen }; return { Component: OnboardingScreen };
}, },
}, },
{
path: "follow",
async lazy() {
const { FollowScreen } = await import("./routes/auth/follow");
return { Component: FollowScreen };
},
},
{
path: "finish",
async lazy() {
const { FinishScreen } = await import("./routes/auth/finish");
return { Component: FinishScreen };
},
},
{ {
path: "tutorials/note", path: "tutorials/note",
async lazy() { async lazy() {

View File

@ -1,5 +0,0 @@
export function CreateProfileScreen() {
return (
<div className="flex items-center justify-center w-full h-full">WIP</div>
);
}

View File

@ -1,12 +1,17 @@
import { useStorage } from "@lume/ark"; import { useArk, useStorage } from "@lume/ark";
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons"; import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
import NDK, { import NDK, {
NDKEvent, NDKEvent,
NDKKind,
NDKNip46Signer, NDKNip46Signer,
NDKPrivateKeySigner, NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk"; } from "@nostr-dev-kit/ndk";
import * as Select from "@radix-ui/react-select"; import * as Select from "@radix-ui/react-select";
import { downloadDir } from "@tauri-apps/api/path";
import { Window } from "@tauri-apps/api/window"; import { Window } from "@tauri-apps/api/window";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { getPublicKey, nip19 } from "nostr-tools";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useLoaderData, useNavigate } from "react-router-dom"; import { useLoaderData, useNavigate } from "react-router-dom";
@ -29,17 +34,18 @@ const Item = ({ event }: { event: NDKEvent }) => {
}; };
export function CreateAccountScreen() { export function CreateAccountScreen() {
const ark = useArk();
const storage = useStorage(); const storage = useStorage();
const services = useLoaderData() as NDKEvent[];
const navigate = useNavigate(); const navigate = useNavigate();
const services = useLoaderData() as NDKEvent[];
const [serviceId, setServiceId] = useState(services[0].id); const [serviceId, setServiceId] = useState(services?.[0]?.id);
const [loading, setIsLoading] = useState(false); const [loading, setIsLoading] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { isDirty, isValid }, formState: { isValid },
} = useForm(); } = useForm();
const getDomainName = (id: string) => { const getDomainName = (id: string) => {
@ -47,6 +53,31 @@ export function CreateAccountScreen() {
return JSON.parse(event.content).nip05.replace("_@", "") as string; return JSON.parse(event.content).nip05.replace("_@", "") as string;
}; };
const generateNostrKeys = async () => {
const signer = NDKPrivateKeySigner.generate();
const pubkey = getPublicKey(signer.privateKey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(signer.privateKey);
ark.updateNostrSigner({ signer });
const downloadPath = await downloadDir();
const fileName = `nostr_keys_${new Date().getTime().toString(36)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
if (filePath) {
await writeTextFile(
filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`,
);
} // else { user cancel action }
return navigate("/auth/onboarding");
};
const onSubmit = async (data: { username: string; email: string }) => { const onSubmit = async (data: { username: string; email: string }) => {
setIsLoading(true); setIsLoading(true);
@ -71,19 +102,18 @@ export function CreateAccountScreen() {
localSigner, localSigner,
); );
let authWindow: Window;
remoteSigner.addListener("authUrl", (authUrl: string) => { remoteSigner.addListener("authUrl", (authUrl: string) => {
const authWindow = new Window("auth", { authWindow = new Window(`auth-${serviceId}`, {
url: authUrl, url: authUrl,
title: domain, title: domain,
titleBarStyle: "overlay", titleBarStyle: "overlay",
width: 415, width: 415,
height: 600, height: 600,
center: true center: true,
closable: false,
}); });
authWindow.listen(
"tauri://close-requested",
async () => await authWindow.close(),
);
}); });
const account = await remoteSigner.createAccount( const account = await remoteSigner.createAccount(
@ -93,7 +123,9 @@ export function CreateAccountScreen() {
); );
if (!account) { if (!account) {
authWindow.close();
setIsLoading(false); setIsLoading(false);
return toast.error("Failed to create new account, try again later"); return toast.error("Failed to create new account, try again later");
} }
@ -103,15 +135,19 @@ export function CreateAccountScreen() {
privkey: localSigner.privateKey, privkey: localSigner.privateKey,
}); });
ark.updateNostrSigner({ signer: remoteSigner });
authWindow.close();
setIsLoading(false); setIsLoading(false);
return navigate("/auth/create-profile");
return navigate("/auth/onboarding");
}; };
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold text-center"> <h1 className="text-2xl font-semibold">
Let's get you set up on Nostr. Let's get you set up on Nostr.
</h1> </h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500"> <p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
@ -119,101 +155,109 @@ export function CreateAccountScreen() {
you want. you want.
</p> </p>
</div> </div>
<div className="flex flex-col gap-8"> {!services ? (
<form <div className="flex items-center justify-center w-full">
onSubmit={handleSubmit(onSubmit)} <LoaderIcon className="size-5 animate-spin" />
className="flex flex-col gap-3 mb-0" </div>
> ) : (
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl"> <div className="flex flex-col gap-6">
<div className="flex flex-col gap-2"> <form
<label onSubmit={handleSubmit(onSubmit)}
htmlFor="username" className="flex flex-col gap-3 mb-0"
className="text-sm font-semibold uppercase text-neutral-600" >
> <div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
Username * <div className="flex flex-col gap-2">
</label> <label
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl"> htmlFor="username"
className="text-sm font-semibold uppercase text-neutral-600"
>
Username *
</label>
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
<input
type={"text"}
{...register("username", {
required: true,
minLength: 1,
})}
spellCheck={false}
placeholder="satoshi"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root value={serviceId} onValueChange={setServiceId}>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="border rounded-lg bg-neutral-950 border-neutral-900">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Public handles
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (Optional)
</label>
<input <input
type={"text"} type={"email"}
{...register("username", { {...register("email", { required: false })}
required: true,
minLength: 1,
})}
spellCheck={false} spellCheck={false}
placeholder="satoshi" autoCapitalize="none"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600" autoCorrect="none"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/> />
<Select.Root value={serviceId} onValueChange={setServiceId}>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="border rounded-lg bg-neutral-900 border-neutral-800">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 px-7 text-neutral-600">
Public services
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <button
<label type="submit"
htmlFor="email" disabled={!isValid}
className="text-sm font-semibold uppercase text-neutral-600" className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
> >
Backup Email (Optional) {loading ? (
</label> <LoaderIcon className="size-5 animate-spin" />
<input ) : (
type={"email"} "Create Account"
{...register("email", { required: false })} )}
spellCheck={false} </button>
autoCapitalize="none" </form>
autoCorrect="none" <div className="flex flex-col gap-6">
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800" <div className="relative">
/> <div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-neutral-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-600">
Or
</span>
</div>
</div> </div>
<button
type="button"
onClick={generateNostrKeys}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Generate Nostr keys
</button>
</div> </div>
<button
type="submit"
className="inline-flex items-center justify-center w-full h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Create Account"
)}
</button>
</form>
<div className="flex flex-col gap-3">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-neutral-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-600">
Or
</span>
</div>
</div>
<button
type="submit"
className="inline-flex items-center justify-center w-full h-12 font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Generate nostr keys
</button>
</div> </div>
</div> )}
</div> </div>
</div> </div>
); );

View File

@ -1,313 +0,0 @@
import { useArk, useStorage } from "@lume/ark";
import { ArrowLeftIcon, InfoIcon, LoaderIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { NDKKind, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { downloadDir } from "@tauri-apps/api/path";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { save } from "@tauri-apps/plugin-dialog";
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 { AvatarUploader } from "./components/avatarUploader";
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;
}>(null);
const {
register,
handleSubmit,
formState: { isDirty, isValid },
} = useForm();
const ark = useArk();
const storage = useStorage();
const navigate = useNavigate();
const svgURI = `data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon("lume new account", 90, 50),
)}`;
const onSubmit = async (data: { name: string; about: string }) => {
try {
setLoading(true);
const profile = {
...data,
name: data.name,
display_name: data.name,
bio: data.about,
picture: picture,
avatar: picture,
};
const userPrivkey = generatePrivateKey();
const userPubkey = getPublicKey(userPrivkey);
const userNpub = nip19.npubEncode(userPubkey);
const userNsec = nip19.nsecEncode(userPrivkey);
const signer = new NDKPrivateKeySigner(userPrivkey);
ark.updateNostrSigner({ signer });
const publish = await ark.createEvent({
content: JSON.stringify(profile),
kind: NDKKind.Metadata,
tags: [],
});
if (publish) {
await storage.createAccount({
id: userNpub,
pubkey: userPubkey,
privkey: userPrivkey,
});
setKeys({ npub: userNpub, nsec: userNsec });
setLoading(false);
} else {
toast.error("Cannot publish user profile, please try again later.");
setLoading(false);
}
} catch (e) {
return toast.error(e);
}
};
const copyNsec = async () => {
await writeText(keys.nsec);
};
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,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`,
);
setDownloaded(true);
} // else { user cancel action }
} catch (e) {
return toast.error(e);
}
};
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="absolute left-[8px] top-2">
{!keys ? (
<button
type="button"
onClick={() => navigate(-1)}
className="group 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 group-hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-200 dark:group-hover:bg-neutral-700">
<ArrowLeftIcon className="h-4 w-4" />
</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">
Let&apos;s set up your account.
</h1>
<div className="flex flex-col gap-3">
{!keys ? (
<div className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950">
<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="flex h-36 w-full flex-col items-center justify-center gap-3 rounded-lg bg-neutral-100 dark:bg-neutral-900">
{picture.length > 0 ? (
<img
src={picture}
alt="user's avatar"
className="h-14 w-14 rounded-xl object-cover"
/>
) : (
<img
src={svgURI}
alt="user's avatar"
className="h-14 w-14 rounded-xl bg-black dark:bg-white"
/>
)}
<AvatarUploader setPicture={setPicture} />
</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 border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="about" className="font-semibold">
Bio
</label>
<textarea
{...register("about")}
spellCheck={false}
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 rounded-lg bg-blue-100 p-3 text-sm text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<InfoIcon className="h-8 w-8" />
<p>
There are many more settings you can configure from the
&quot;Settings&quot; screen. Be sure to visit it later.
</p>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 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-4 w-4 animate-spin" />
) : (
"Create and Continue"
)}
</button>
</div>
</div>
</form>
</div>
) : (
<>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950"
>
<User pubkey={keys.npub} variant="simple" />
</motion.div>
<motion.div
initial={{ opacity: 0, y: 80 }}
animate={{
opacity: 1,
y: 0,
}}
className="rounded-xl bg-neutral-50 p-3 dark:bg-neutral-950"
>
<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>
<div className="relative w-full">
<input
readOnly
value={`${keys.nsec.substring(
0,
10,
)}**************************`}
className="h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
<button
type="button"
onClick={copyNsec}
className="rounded-md bg-neutral-200 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
>
Copy
</button>
</div>
</div>
</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 w-full rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
</div>
{!downloaded ? (
<button
type="button"
onClick={() => download()}
className="mt-1 inline-flex h-11 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-11 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")}
>
Finish
</motion.button>
) : null}
</div>
</div>
</div>
);
}

View File

@ -1,34 +0,0 @@
import { Link } from 'react-router-dom';
export function FinishScreen() {
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 className="text-center">
<img src="/icon.png" alt="Lume's logo" className="mx-auto mb-1 h-auto w-16" />
<h1 className="text-2xl font-light">
Yo, you&apos;re ready to use <span className="font-bold">Lume</span>
</h1>
</div>
<div className="flex flex-col gap-2">
<Link
to="/auth/tutorials/note"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
>
Start tutorial
</Link>
<Link
to="/"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-neutral-100 font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Skip
</Link>
<p className="text-center text-sm font-medium text-neutral-500 dark:text-neutral-600">
You need to restart app to make changes in previous step take effect or you
can continue with Lume default settings
</p>
</div>
</div>
</div>
);
}

View File

@ -1,277 +0,0 @@
import { useArk } from "@lume/ark";
import {
ArrowLeftIcon,
ArrowRightIcon,
CancelIcon,
ChevronDownIcon,
LoaderIcon,
PlusIcon,
} from "@lume/icons";
import { User } from "@lume/ui";
import * as Accordion from "@radix-ui/react-accordion";
import { useQuery } from "@tanstack/react-query";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { twMerge } from "tailwind-merge";
const POPULAR_USERS = [
"npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6",
"npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m",
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
"npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
"npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8",
"npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a",
"npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc",
"npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza",
"npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424",
"npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac",
"npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv",
"npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk",
];
const LUME_USERS = [
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445",
];
export function FollowScreen() {
const ark = useArk();
const navigate = useNavigate();
const { status, data } = useQuery({
queryKey: ["trending-profiles-widget"],
queryFn: 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<string[]>([]);
// 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);
if (!follows.length) return navigate("/auth/finish");
const publish = await ark.newContactList({
tags: follows.map((item) => {
if (item.startsWith("npub1"))
return ["p", nip19.decode(item).data as string];
return ["p", item];
}),
});
if (publish) {
setLoading(false);
return navigate("/auth/finish");
}
} catch (e) {
setLoading(false);
toast.error(e);
}
};
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 font-semibold">Dive into the nostrverse</h1>
<h2 className="text-neutral-700 dark:text-neutral-300">
Try following some users that interest you
<br />
to build up your timeline.
</h2>
</div>
<Accordion.Root type="single" defaultValue="recommended" collapsible>
<Accordion.Item
value="recommended"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-12 w-full items-center justify-between rounded-t-xl bg-neutral-100 px-3 font-medium dark:bg-neutral-900">
Popular users
<ChevronDownIcon className="h-4 w-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex h-[420px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl bg-neutral-50 p-3 dark:bg-neutral-950">
{POPULAR_USERS.map((pubkey) => (
<div
key={pubkey}
className="flex h-max w-full shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-100 bg-white dark:border-neutral-900 dark:bg-black"
>
<div className="p-3">
<User pubkey={pubkey} variant="large" />
</div>
<div className="border-t border-neutral-100 px-3 py-4 dark:border-neutral-900">
<button
type="button"
onClick={() => toggleFollow(pubkey)}
className={twMerge(
"inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
follows.includes(pubkey)
? "bg-red-500 hover:bg-red-600"
: "bg-blue-500 hover:bg-blue-600",
)}
>
{follows.includes(pubkey) ? (
<>
<CancelIcon className="h-4 w-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="h-4 w-4" />
Follow
</>
)}
</button>
</div>
</div>
))}
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item
value="trending"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-12 w-full items-center justify-between rounded-t-xl bg-neutral-100 px-3 font-medium dark:bg-neutral-900">
Trending users
<ChevronDownIcon className="h-4 w-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex h-[420px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl bg-neutral-50 p-3 dark:bg-neutral-950">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin" />
</div>
) : (
data?.profiles.map(
(item: {
pubkey: string;
profile: { content: string };
}) => (
<div
key={item.pubkey}
className="flex h-max w-full shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-100 bg-white dark:border-neutral-900 dark:bg-black"
>
<div className="p-3">
<User pubkey={item.pubkey} variant="large" />
</div>
<div className="border-t border-neutral-100 px-3 py-4 dark:border-neutral-900">
<button
type="button"
onClick={() => toggleFollow(item.pubkey)}
className={twMerge(
"inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
follows.includes(item.pubkey)
? "bg-red-500 hover:bg-red-600"
: "bg-blue-500 hover:bg-blue-600",
)}
>
{follows.includes(item.pubkey) ? (
<>
<CancelIcon className="h-4 w-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="h-4 w-4" />
Follow
</>
)}
</button>
</div>
</div>
),
)
)}
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item
value="lume"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-12 w-full items-center justify-between rounded-t-xl bg-neutral-100 px-3 font-medium dark:bg-neutral-900">
Lume team
<ChevronDownIcon className="h-4 w-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex h-[420px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl bg-neutral-50 p-3 dark:bg-neutral-950">
{LUME_USERS.map((pubkey) => (
<div
key={pubkey}
className="flex h-max w-full shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-100 bg-white dark:border-neutral-900 dark:bg-black"
>
<div className="p-3">
<User pubkey={pubkey} variant="large" />
</div>
<div className="border-t border-neutral-100 px-3 py-4 dark:border-neutral-900">
<button
type="button"
onClick={() => toggleFollow(pubkey)}
className={twMerge(
"inline-flex h-9 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
follows.includes(pubkey)
? "bg-red-500 hover:bg-red-600"
: "bg-blue-500 hover:bg-blue-600",
)}
>
{follows.includes(pubkey) ? (
<>
<CancelIcon className="h-4 w-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="h-4 w-4" />
Follow
</>
)}
</button>
</div>
</div>
))}
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</div>
<div className="absolute bottom-3 right-3 flex w-full items-center justify-end gap-2">
<button
type="button"
onClick={() => navigate(-1)}
className="inline-flex h-11 w-max items-center justify-center gap-2 rounded-lg bg-neutral-100 px-3 font-semibold hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-blue-800"
>
<ArrowLeftIcon className="h-4 w-4" />
Back
</button>
<button
type="button"
onClick={submit}
disabled={loading}
className="inline-flex h-11 w-max items-center justify-center gap-2 rounded-lg bg-blue-500 px-3 font-semibold text-white hover:bg-blue-600"
>
Continue
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin" />
) : (
<ArrowRightIcon className="h-4 w-4" />
)}
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,260 @@
import { useArk } from "@lume/ark";
import {
ArrowLeftIcon,
ArrowRightIcon,
CancelIcon,
ChevronDownIcon,
LoaderIcon,
PlusIcon,
} from "@lume/icons";
import { User } from "@lume/ui";
import * as Accordion from "@radix-ui/react-accordion";
import { useQuery } from "@tanstack/react-query";
import { nip19 } from "nostr-tools";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { twMerge } from "tailwind-merge";
const POPULAR_USERS = [
"npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6",
"npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m",
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
"npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
"npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8",
"npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a",
"npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc",
"npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza",
"npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424",
"npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac",
"npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv",
"npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk",
];
const LUME_USERS = [
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445",
];
export function FollowsScreen() {
const ark = useArk();
const navigate = useNavigate();
const { status, data } = useQuery({
queryKey: ["trending-profiles-widget"],
queryFn: 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<string[]>([]);
// 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);
if (!follows.length) return navigate("/auth/finish");
const publish = await ark.newContactList({
tags: follows.map((item) => {
if (item.startsWith("npub1"))
return ["p", nip19.decode(item).data as string];
return ["p", item];
}),
});
if (publish) {
setLoading(false);
return navigate("/auth/finish");
}
} catch (e) {
setLoading(false);
toast.error(e);
}
};
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="flex flex-col gap-3">
<h1 className="text-2xl font-semibold text-center">
Dive into the nostrverse
</h1>
<p className="text-lg font-medium leading-snug text-center text-neutral-600 dark:text-neutral-500">
Try following some users that interest you
<br />
to build up your timeline.
</p>
</div>
<Accordion.Root type="single" defaultValue="recommended" collapsible>
<Accordion.Item
value="recommended"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-14 w-full items-center justify-between rounded-t-xl px-3 font-medium bg-neutral-950">
Popular users
<ChevronDownIcon className="size-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex h-[400px] w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-950">
{POPULAR_USERS.map((pubkey) => (
<div
key={pubkey}
className="flex h-max w-full shrink-0 flex-col mb-3 p-3 gap-4 overflow-hidden rounded-lg bg-neutral-900"
>
<User pubkey={pubkey} variant="large" />
<button
type="button"
onClick={() => toggleFollow(pubkey)}
className={twMerge(
"inline-flex h-9 shrink-0 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
follows.includes(pubkey)
? "bg-red-600 hover:bg-red-500"
: "bg-blue-600 hover:bg-blue-500",
)}
>
{follows.includes(pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div>
))}
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item
value="trending"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-14 w-full items-center justify-between rounded-t-xl px-3 font-medium bg-neutral-950">
Trending users
<ChevronDownIcon className="size-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex h-[400px] w-full flex-col overflow-y-auto rounded-b-xl px-3 bg-neutral-950">
{status === "pending" ? (
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" />
</div>
) : (
data?.profiles.map(
(item: {
pubkey: string;
profile: { content: string };
}) => (
<div
key={item.pubkey}
className="flex h-max w-full mb-3 p-3 gap-4 shrink-0 flex-col overflow-hidden rounded-lg bg-neutral-900"
>
<User pubkey={item.pubkey} variant="large" />
<button
type="button"
onClick={() => toggleFollow(item.pubkey)}
className={twMerge(
"inline-flex h-9 shrink-0 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
follows.includes(item.pubkey)
? "bg-red-600 hover:bg-red-500"
: "bg-blue-600 hover:bg-blue-500",
)}
>
{follows.includes(item.pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div>
),
)
)}
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item
value="lume"
className="mb-3 overflow-hidden rounded-xl"
>
<Accordion.Trigger className="flex h-14 w-full items-center justify-between rounded-t-xl px-3 font-medium bg-neutral-950">
Lume HQ
<ChevronDownIcon className="size-4" />
</Accordion.Trigger>
<Accordion.Content>
<div className="flex h-[400px] w-full flex-col gap-3 overflow-y-auto rounded-b-xl p-3 bg-neutral-950">
{LUME_USERS.map((pubkey) => (
<div
key={pubkey}
className="flex h-max w-full mb-3 p-3 gap-4 shrink-0 flex-col overflow-hidden rounded-lg bg-neutral-900"
>
<User pubkey={pubkey} variant="large" />
<button
type="button"
onClick={() => toggleFollow(pubkey)}
className={twMerge(
"inline-flex h-9 shrink-0 w-full items-center justify-center gap-1 rounded-lg font-medium text-white",
follows.includes(pubkey)
? "bg-red-600 hover:bg-red-500"
: "bg-blue-600 hover:bg-blue-500",
)}
>
{follows.includes(pubkey) ? (
<>
<CancelIcon className="size-4" />
Unfollow
</>
) : (
<>
<PlusIcon className="size-4" />
Follow
</>
)}
</button>
</div>
))}
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</div>
<div className="absolute bottom-4 right-4 flex w-full items-center justify-end gap-2">
<button
type="button"
onClick={submit}
disabled={loading}
className="inline-flex h-12 w-max items-center justify-center gap-2 rounded-xl bg-blue-500 px-3 font-semibold text-white hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
<ArrowRightIcon className="size-4" />
)}
Continue
</button>
</div>
</div>
);
}

View File

@ -14,7 +14,6 @@ export function OnboardingScreen() {
const [settings, setSettings] = useState({ const [settings, setSettings] = useState({
autoupdate: false, autoupdate: false,
outbox: false,
notification: false, notification: false,
}); });
@ -23,12 +22,6 @@ export function OnboardingScreen() {
return navigate("/auth/finish"); return navigate("/auth/finish");
}; };
const toggleOutbox = async () => {
await storage.createSetting("outbox", String(+!settings.outbox));
// update state
setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
};
const toggleAutoupdate = async () => { const toggleAutoupdate = async () => {
await storage.createSetting("autoupdate", String(+!settings.autoupdate)); await storage.createSetting("autoupdate", String(+!settings.autoupdate));
// update state // update state
@ -55,12 +48,6 @@ export function OnboardingScreen() {
...prev, ...prev,
autoupdate: !!parseInt(item.value), autoupdate: !!parseInt(item.value),
})); }));
if (item.key === "outbox")
setSettings((prev) => ({
...prev,
outbox: !!parseInt(item.value),
}));
} }
} }
@ -70,65 +57,51 @@ export function OnboardingScreen() {
return ( return (
<div className="relative flex h-full w-full items-center justify-center"> <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="mx-auto flex w-full max-w-md flex-col gap-10">
<div className="text-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-light text-neutral-900 dark:text-neutral-100"> <h1 className="text-2xl font-semibold">
You&apos;re almost ready to use Lume. You&apos;re almost ready to use Lume.
</h1> </h1>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100"> <p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Let&apos;s start personalizing your experience. Let&apos;s start personalizing your experience.
</h2> </p>
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900"> <div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
<Switch.Root <Switch.Root
checked={settings.autoupdate} checked={settings.autoupdate}
onClick={() => toggleAutoupdate()} onClick={() => toggleAutoupdate()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
> >
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" /> <Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root> </Switch.Root>
<div> <div>
<h3 className="font-semibold">Auto check for update on Login</h3> <h3 className="text-lg font-semibold">
<p className="text-sm"> Auto check for update on Login
</h3>
<p className="text-neutral-500">
Keep Lume up to date with latest version, always have new Keep Lume up to date with latest version, always have new
features and bug free. features and bug free.
</p> </p>
</div> </div>
</div> </div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900"> <div className="flex w-full items-start justify-between gap-4 rounded-xl px-5 py-4 bg-neutral-950">
<Switch.Root <Switch.Root
checked={settings.notification} checked={settings.notification}
onClick={() => toggleNofitication()} onClick={() => toggleNofitication()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800" className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
> >
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" /> <Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-neutral-50 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root> </Switch.Root>
<div> <div>
<h3 className="font-semibold">Push notification</h3> <h3 className="font-semibold text-lg">Push notification</h3>
<p className="text-sm"> <p className="text-neutral-500">
Enabling push notifications will allow you to receive Enabling push notifications will allow you to receive
notifications from Lume directly on your device. notifications from Lume.
</p> </p>
</div> </div>
</div> </div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900"> <div className="flex items-center gap-2 rounded-xl px-5 py-3 text-sm bg-blue-950 text-blue-300">
<Switch.Root <InfoIcon className="size-8" />
checked={settings.outbox}
onClick={() => toggleOutbox()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
<div>
<h3 className="font-semibold">Use Gossip model (recommended)</h3>
<p className="text-sm">
Automatically discover relays to connect based on the
preferences of each author.
</p>
</div>
</div>
<div className="flex items-center gap-2 rounded-lg bg-blue-100 p-3 text-sm text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<InfoIcon className="h-8 w-8" />
<p> <p>
There are many more settings you can configure from the There are many more settings you can configure from the
&quot;Settings&quot; screen. Be sure to visit it later. &quot;Settings&quot; screen. Be sure to visit it later.
@ -137,7 +110,7 @@ export function OnboardingScreen() {
<button <button
type="button" type="button"
onClick={next} onClick={next}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
> >
Continue Continue
</button> </button>

View File

@ -1,7 +1,16 @@
import { SettingsIcon } from "@lume/icons"; import { LoaderIcon } from "@lume/icons";
import { Link } from "react-router-dom"; import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
export function WelcomeScreen() { export function WelcomeScreen() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const gotoCreateAccount = () => {
setLoading(true);
navigate("/auth/create");
};
return ( return (
<div className="flex flex-col items-center justify-between w-full h-full"> <div className="flex flex-col items-center justify-between w-full h-full">
<div /> <div />
@ -20,15 +29,20 @@ export function WelcomeScreen() {
</p> </p>
</div> </div>
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto"> <div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
<Link <button
to="/auth/create" type="button"
className="inline-flex items-center justify-center w-full h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600" onClick={gotoCreateAccount}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
> >
Create New Account {loading ? (
</Link> <LoaderIcon className="size-5 animate-spin" />
) : (
"Create New Account"
)}
</button>
<Link <Link
to="/auth/import" to="/auth/import"
className="inline-flex items-center justify-center w-full h-12 font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
> >
Login Login
</Link> </Link>

View File

@ -1,7 +1,6 @@
import { LumeStorage } from "@lume/storage"; import { LumeStorage } from "@lume/storage";
import { type NDKEventWithReplies, type NIP05 } from "@lume/types"; import { type NDKEventWithReplies, type NIP05 } from "@lume/types";
import NDK, { import NDK, {
NDKAppHandlerEvent,
NDKEvent, NDKEvent,
NDKFilter, NDKFilter,
NDKKind, NDKKind,
@ -513,7 +512,6 @@ export class Ark {
const data: NIP05 = await res.json(); const data: NIP05 = await res.json();
if (!data.names) return false; if (!data.names) return false;
if (data.names[localPath.toLowerCase()] === pubkey) return true; if (data.names[localPath.toLowerCase()] === pubkey) return true;
if (data.names[localPath] === pubkey) return true; if (data.names[localPath] === pubkey) return true;
@ -542,4 +540,28 @@ export class Ark {
return null; return null;
} }
public async getOAuthServices() {
const trusted: NDKEvent[] = [];
const services = await this.ndk.fetchEvents({
kinds: [NDKKind.AppHandler],
"#k": ["24133"],
});
for (const service of services) {
const nip05 = JSON.parse(service.content).nip05 as string;
try {
const validate = await this.validateNIP05({
pubkey: service.pubkey,
nip05,
});
if (validate) trusted.push(service);
} catch (e) {
console.log(e);
}
}
return trusted;
}
} }

View File

@ -7,7 +7,7 @@ export function AuthLayout({ platform }: { platform: Platform }) {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const canGoBack = location.key === "default"; const canGoBack = location.pathname.length > 6;
return ( return (
<div className="flex flex-col w-screen h-screen bg-black text-neutral-50"> <div className="flex flex-col w-screen h-screen bg-black text-neutral-50">

View File

@ -182,11 +182,11 @@ export const User = memo(function User({
/> />
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
<div className="flex flex-col items-start text-start"> <div className="flex flex-col items-start text-start gap-1">
<p className="max-w-[15rem] truncate text-lg font-semibold"> <p className="max-w-[15rem] truncate text-lg font-semibold leadning-tight">
{user?.name || user?.display_name || user?.displayName} {user?.name || user?.display_name || user?.displayName}
</p> </p>
<p className="break-p prose prose-neutral max-w-none select-text whitespace-pre-line leading-normal dark:prose-invert"> <p className="break-p text-neutral-500 max-w-none select-text whitespace-pre-line">
{user?.about || user?.bio || "No bio"} {user?.about || user?.bio || "No bio"}
</p> </p>
</div> </div>

View File

@ -176,6 +176,9 @@ importers:
sonner: sonner:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1(react-dom@18.2.0)(react@18.2.0) version: 1.3.1(react-dom@18.2.0)(react@18.2.0)
unique-names-generator:
specifier: ^4.7.1
version: 4.7.1
virtua: virtua:
specifier: ^0.18.1 specifier: ^0.18.1
version: 0.18.1(react-dom@18.2.0)(react@18.2.0) version: 0.18.1(react-dom@18.2.0)(react@18.2.0)
@ -6034,6 +6037,11 @@ packages:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true dev: true
/unique-names-generator@4.7.1:
resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==}
engines: {node: '>=8'}
dev: false
/unpipe@1.0.0: /unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}