mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-30 00:41:00 +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",
|
"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": {
|
||||||
|
@ -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() {
|
||||||
|
@ -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 { 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,7 +155,12 @@ export function CreateAccountScreen() {
|
|||||||
you want.
|
you want.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex flex-col gap-3 mb-0"
|
className="flex flex-col gap-3 mb-0"
|
||||||
@ -151,11 +192,11 @@ export function CreateAccountScreen() {
|
|||||||
</Select.Icon>
|
</Select.Icon>
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Portal>
|
<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.Viewport className="p-3">
|
||||||
<Select.Group>
|
<Select.Group>
|
||||||
<Select.Label className="mb-2 px-7 text-neutral-600">
|
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
|
||||||
Public services
|
Public handles
|
||||||
</Select.Label>
|
</Select.Label>
|
||||||
{services.map((service) => (
|
{services.map((service) => (
|
||||||
<Item key={service.id} event={service} />
|
<Item key={service.id} event={service} />
|
||||||
@ -186,7 +227,8 @@ export function CreateAccountScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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 ? (
|
{loading ? (
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
@ -195,7 +237,7 @@ export function CreateAccountScreen() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<div className="w-full border-t border-neutral-900" />
|
<div className="w-full border-t border-neutral-900" />
|
||||||
@ -207,13 +249,15 @@ export function CreateAccountScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</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({
|
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're almost ready to use Lume.
|
You'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's start personalizing your experience.
|
Let'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
|
||||||
"Settings" screen. Be sure to visit it later.
|
"Settings" 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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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'}
|
||||||
|
Loading…
Reference in New Issue
Block a user