mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: update create account flow
This commit is contained in:
parent
7554a35c31
commit
87099c6388
@ -59,6 +59,7 @@
|
||||
"react-router-dom": "^6.21.1",
|
||||
"smol-toml": "^1.1.3",
|
||||
"sonner": "^1.3.1",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"virtua": "^0.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useArk, useStorage } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import {
|
||||
RouterProvider,
|
||||
@ -24,7 +23,7 @@ export default function Router() {
|
||||
element: <HomeLayout />,
|
||||
errorElement: <ErrorScreen />,
|
||||
loader: async () => {
|
||||
if (!storage.account) return redirect("auth/welcome");
|
||||
if (!storage.account) return redirect("auth");
|
||||
return null;
|
||||
},
|
||||
children: [
|
||||
@ -171,7 +170,7 @@ export default function Router() {
|
||||
errorElement: <ErrorScreen />,
|
||||
children: [
|
||||
{
|
||||
path: "welcome",
|
||||
index: true,
|
||||
async lazy() {
|
||||
const { WelcomeScreen } = await import("./routes/auth/welcome");
|
||||
return { Component: WelcomeScreen };
|
||||
@ -180,23 +179,7 @@ export default function Router() {
|
||||
{
|
||||
path: "create",
|
||||
loader: async () => {
|
||||
const trusted: NDKEvent[] = [];
|
||||
|
||||
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;
|
||||
return await ark.getOAuthServices();
|
||||
},
|
||||
async lazy() {
|
||||
const { CreateAccountScreen } = await import(
|
||||
@ -205,15 +188,6 @@ export default function Router() {
|
||||
return { Component: CreateAccountScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "create-profile",
|
||||
async lazy() {
|
||||
const { CreateProfileScreen } = await import(
|
||||
"./routes/auth/create-profile"
|
||||
);
|
||||
return { Component: CreateProfileScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "import",
|
||||
async lazy() {
|
||||
@ -232,20 +206,6 @@ export default function Router() {
|
||||
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",
|
||||
async lazy() {
|
||||
|
@ -1,5 +0,0 @@
|
||||
export function CreateProfileScreen() {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">WIP</div>
|
||||
);
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
import { useStorage } from "@lume/ark";
|
||||
import { useArk, useStorage } from "@lume/ark";
|
||||
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKKind,
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import * as Select from "@radix-ui/react-select";
|
||||
import { downloadDir } from "@tauri-apps/api/path";
|
||||
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 { useForm } from "react-hook-form";
|
||||
import { useLoaderData, useNavigate } from "react-router-dom";
|
||||
@ -29,17 +34,18 @@ const Item = ({ event }: { event: NDKEvent }) => {
|
||||
};
|
||||
|
||||
export function CreateAccountScreen() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const services = useLoaderData() as NDKEvent[];
|
||||
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 {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
formState: { isValid },
|
||||
} = useForm();
|
||||
|
||||
const getDomainName = (id: string) => {
|
||||
@ -47,6 +53,31 @@ export function CreateAccountScreen() {
|
||||
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 }) => {
|
||||
setIsLoading(true);
|
||||
|
||||
@ -71,19 +102,18 @@ export function CreateAccountScreen() {
|
||||
localSigner,
|
||||
);
|
||||
|
||||
let authWindow: Window;
|
||||
|
||||
remoteSigner.addListener("authUrl", (authUrl: string) => {
|
||||
const authWindow = new Window("auth", {
|
||||
authWindow = new Window(`auth-${serviceId}`, {
|
||||
url: authUrl,
|
||||
title: domain,
|
||||
titleBarStyle: "overlay",
|
||||
width: 415,
|
||||
height: 600,
|
||||
center: true
|
||||
center: true,
|
||||
closable: false,
|
||||
});
|
||||
authWindow.listen(
|
||||
"tauri://close-requested",
|
||||
async () => await authWindow.close(),
|
||||
);
|
||||
});
|
||||
|
||||
const account = await remoteSigner.createAccount(
|
||||
@ -93,7 +123,9 @@ export function CreateAccountScreen() {
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
authWindow.close();
|
||||
setIsLoading(false);
|
||||
|
||||
return toast.error("Failed to create new account, try again later");
|
||||
}
|
||||
|
||||
@ -103,15 +135,19 @@ export function CreateAccountScreen() {
|
||||
privkey: localSigner.privateKey,
|
||||
});
|
||||
|
||||
ark.updateNostrSigner({ signer: remoteSigner });
|
||||
|
||||
authWindow.close();
|
||||
setIsLoading(false);
|
||||
return navigate("/auth/create-profile");
|
||||
|
||||
return navigate("/auth/onboarding");
|
||||
};
|
||||
|
||||
return (
|
||||
<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 gap-3">
|
||||
<h1 className="text-2xl font-semibold text-center">
|
||||
<div className="flex flex-col gap-1 text-center items-center">
|
||||
<h1 className="text-2xl font-semibold">
|
||||
Let's get you set up on Nostr.
|
||||
</h1>
|
||||
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
||||
@ -119,7 +155,12 @@ export function CreateAccountScreen() {
|
||||
you want.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
{!services ? (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-3 mb-0"
|
||||
@ -151,11 +192,11 @@ export function CreateAccountScreen() {
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content className="border rounded-lg bg-neutral-900 border-neutral-800">
|
||||
<Select.Content className="border rounded-lg bg-neutral-950 border-neutral-900">
|
||||
<Select.Viewport className="p-3">
|
||||
<Select.Group>
|
||||
<Select.Label className="mb-2 px-7 text-neutral-600">
|
||||
Public services
|
||||
<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} />
|
||||
@ -186,7 +227,8 @@ export function CreateAccountScreen() {
|
||||
</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"
|
||||
disabled={!isValid}
|
||||
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"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
@ -195,7 +237,7 @@ export function CreateAccountScreen() {
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-neutral-900" />
|
||||
@ -207,13 +249,15 @@ export function CreateAccountScreen() {
|
||||
</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"
|
||||
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
|
||||
Generate Nostr keys
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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'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
|
||||
"Settings" 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>
|
||||
);
|
||||
}
|
@ -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'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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
260
apps/desktop/src/routes/auth/follows.tsx
Normal file
260
apps/desktop/src/routes/auth/follows.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -14,7 +14,6 @@ export function OnboardingScreen() {
|
||||
|
||||
const [settings, setSettings] = useState({
|
||||
autoupdate: false,
|
||||
outbox: false,
|
||||
notification: false,
|
||||
});
|
||||
|
||||
@ -23,12 +22,6 @@ export function OnboardingScreen() {
|
||||
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 () => {
|
||||
await storage.createSetting("autoupdate", String(+!settings.autoupdate));
|
||||
// update state
|
||||
@ -55,12 +48,6 @@ export function OnboardingScreen() {
|
||||
...prev,
|
||||
autoupdate: !!parseInt(item.value),
|
||||
}));
|
||||
|
||||
if (item.key === "outbox")
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
outbox: !!parseInt(item.value),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,65 +57,51 @@ export function OnboardingScreen() {
|
||||
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-light text-neutral-900 dark:text-neutral-100">
|
||||
<div className="flex flex-col gap-1 text-center items-center">
|
||||
<h1 className="text-2xl font-semibold">
|
||||
You're almost ready to use Lume.
|
||||
</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's start personalizing your experience.
|
||||
</h2>
|
||||
</p>
|
||||
</div>
|
||||
<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
|
||||
checked={settings.autoupdate}
|
||||
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>
|
||||
<div>
|
||||
<h3 className="font-semibold">Auto check for update on Login</h3>
|
||||
<p className="text-sm">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Auto check for update on Login
|
||||
</h3>
|
||||
<p className="text-neutral-500">
|
||||
Keep Lume up to date with latest version, always have new
|
||||
features and bug free.
|
||||
</p>
|
||||
</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
|
||||
checked={settings.notification}
|
||||
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>
|
||||
<div>
|
||||
<h3 className="font-semibold">Push notification</h3>
|
||||
<p className="text-sm">
|
||||
<h3 className="font-semibold text-lg">Push notification</h3>
|
||||
<p className="text-neutral-500">
|
||||
Enabling push notifications will allow you to receive
|
||||
notifications from Lume directly on your device.
|
||||
notifications from Lume.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<Switch.Root
|
||||
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" />
|
||||
<div className="flex items-center gap-2 rounded-xl px-5 py-3 text-sm bg-blue-950 text-blue-300">
|
||||
<InfoIcon className="size-8" />
|
||||
<p>
|
||||
There are many more settings you can configure from the
|
||||
"Settings" screen. Be sure to visit it later.
|
||||
@ -137,7 +110,7 @@ export function OnboardingScreen() {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
@ -1,7 +1,16 @@
|
||||
import { SettingsIcon } from "@lume/icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export function WelcomeScreen() {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const gotoCreateAccount = () => {
|
||||
setLoading(true);
|
||||
navigate("/auth/create");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-between w-full h-full">
|
||||
<div />
|
||||
@ -20,15 +29,20 @@ export function WelcomeScreen() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
|
||||
<Link
|
||||
to="/auth/create"
|
||||
className="inline-flex items-center justify-center w-full h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</Link>
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
) : (
|
||||
"Create New Account"
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { LumeStorage } from "@lume/storage";
|
||||
import { type NDKEventWithReplies, type NIP05 } from "@lume/types";
|
||||
import NDK, {
|
||||
NDKAppHandlerEvent,
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKKind,
|
||||
@ -513,7 +512,6 @@ export class Ark {
|
||||
const data: NIP05 = await res.json();
|
||||
|
||||
if (!data.names) return false;
|
||||
|
||||
if (data.names[localPath.toLowerCase()] === pubkey) return true;
|
||||
if (data.names[localPath] === pubkey) return true;
|
||||
|
||||
@ -542,4 +540,28 @@ export class Ark {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export function AuthLayout({ platform }: { platform: Platform }) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const canGoBack = location.key === "default";
|
||||
const canGoBack = location.pathname.length > 6;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-screen h-screen bg-black text-neutral-50">
|
||||
|
@ -182,11 +182,11 @@ export const User = memo(function User({
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-col items-start text-start">
|
||||
<p className="max-w-[15rem] truncate text-lg font-semibold">
|
||||
<div className="flex flex-col items-start text-start gap-1">
|
||||
<p className="max-w-[15rem] truncate text-lg font-semibold leadning-tight">
|
||||
{user?.name || user?.display_name || user?.displayName}
|
||||
</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"}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -176,6 +176,9 @@ importers:
|
||||
sonner:
|
||||
specifier: ^1.3.1
|
||||
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:
|
||||
specifier: ^0.18.1
|
||||
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==}
|
||||
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:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
Loading…
Reference in New Issue
Block a user