feat: improve account management
This commit is contained in:
@@ -13,7 +13,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { memo, useCallback, useState } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/$account/")({
|
||||
export const Route = createLazyFileRoute("/$account")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NostrAccount, NostrQuery } from "@/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/$account/")({
|
||||
export const Route = createFileRoute("/$account")({
|
||||
beforeLoad: async ({ params }) => {
|
||||
const settings = await NostrQuery.getUserSettings();
|
||||
const accounts = await NostrAccount.getAccounts();
|
||||
@@ -8,7 +8,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/auth/$account/backup")({
|
||||
export const Route = createFileRoute("/$account/backup")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Container } from "@/components";
|
||||
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return (
|
||||
<Container withDrag>
|
||||
<div className="max-w-sm mx-auto size-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
105
src/routes/auth/connect.lazy.tsx
Normal file
105
src/routes/auth/connect.lazy.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Frame, GoBack, Spinner } from "@/components";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth/connect")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const navigate = Route.useNavigate();
|
||||
|
||||
const [uri, setUri] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const pasteFromClipboard = async () => {
|
||||
const val = await readText();
|
||||
setUri(val);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
if (!uri.startsWith("bunker://")) {
|
||||
await message(
|
||||
"You need to enter a valid Connect URI starts with bunker://",
|
||||
{ title: "Nostr Connect", kind: "info" },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await commands.connectAccount(uri);
|
||||
|
||||
if (res.status === "ok") {
|
||||
navigate({ to: "/", replace: true });
|
||||
} else {
|
||||
await message(res.error, { title: "Nostr Connect", kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="size-full flex items-center justify-center"
|
||||
>
|
||||
<div className="w-[320px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h1 className="leading-tight text-xl font-semibold">Nostr Connect</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Frame
|
||||
className="flex flex-col gap-1 p-3 rounded-xl overflow-hidden"
|
||||
shadow
|
||||
>
|
||||
<label
|
||||
htmlFor="uri"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Connection String
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
name="uri"
|
||||
type="text"
|
||||
placeholder="bunker://..."
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pasteFromClipboard()}
|
||||
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
</div>
|
||||
</Frame>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner /> : "Continue"}
|
||||
</button>
|
||||
{isPending ? (
|
||||
<p className="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400">
|
||||
Waiting confirmation...
|
||||
</p>
|
||||
) : (
|
||||
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||
Go back to previous screen
|
||||
</GoBack>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Spinner } from "@/components";
|
||||
import { PlusIcon } from "@/components";
|
||||
import { AvatarUploader } from "@/components/avatarUploader";
|
||||
import { NostrAccount } from "@/system";
|
||||
import type { Metadata } from "@/types";
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const Route = createFileRoute("/auth/create-profile")({
|
||||
loader: async () => {
|
||||
const account = await NostrAccount.createAccount();
|
||||
return account;
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const account = Route.useLoaderData();
|
||||
const navigate = useNavigate();
|
||||
const { register, handleSubmit } = useForm();
|
||||
|
||||
const [picture, setPicture] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (data: {
|
||||
name: string;
|
||||
about: string;
|
||||
website: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Save account keys
|
||||
const save = await NostrAccount.saveAccount(account.nsec);
|
||||
|
||||
// Then create profile
|
||||
if (save) {
|
||||
const profile: Metadata = { ...data, picture };
|
||||
const eventId = await NostrAccount.createProfile(profile);
|
||||
|
||||
if (eventId) {
|
||||
navigate({
|
||||
to: "/auth/$account/backup",
|
||||
params: { account: account.npub },
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
await message(String(e), { title: "Create Profile", kind: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold">Let's set up your profile.</h3>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full mb-0">
|
||||
<div className="flex flex-col gap-3 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
<div className="self-center relative rounded-full size-20 bg-neutral-200 dark:bg-white/70 my-3">
|
||||
{picture ? (
|
||||
<img
|
||||
src={picture}
|
||||
alt="avatar"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
<AvatarUploader
|
||||
setPicture={setPicture}
|
||||
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full text-white rounded-full dark:text-black bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<PlusIcon className="size-8" />
|
||||
</AvatarUploader>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="display_name" className="font-medium">
|
||||
Display Name *
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("display_name", { required: true, minLength: 1 })}
|
||||
placeholder="e.g. Alice in Nostrland"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("name")}
|
||||
placeholder="e.g. alice"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
||||
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-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
{...register("website")}
|
||||
placeholder="e.g. https://alice.me"
|
||||
spellCheck={false}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Spinner /> : "Continue"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Spinner } from "@/components";
|
||||
import { NostrAccount } from "@/system";
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Frame, GoBack } from "@/components";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth/import")({
|
||||
component: Screen,
|
||||
@@ -13,77 +15,118 @@ function Screen() {
|
||||
|
||||
const [key, setKey] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const submit = async () => {
|
||||
if (!key.startsWith("nsec1")) {
|
||||
return await message(
|
||||
"You need to enter a valid private key starts with nsec or ncryptsec",
|
||||
{ title: "Import Key", kind: "info" },
|
||||
);
|
||||
}
|
||||
const pasteFromClipboard = async () => {
|
||||
const val = await readText();
|
||||
setKey(val);
|
||||
};
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const npub = await NostrAccount.saveAccount(key, password);
|
||||
|
||||
if (npub) {
|
||||
navigate({ to: "/", replace: true });
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
if (!key.startsWith("nsec1") && !key.startsWith("ncryptsec")) {
|
||||
await message(
|
||||
"You need to enter a valid private key starts with nsec or ncryptsec",
|
||||
{ title: "Login", kind: "info" },
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
await message(String(e), { title: "Import Key", kind: "error" });
|
||||
}
|
||||
|
||||
if (key.startsWith("nsec1") && !password.length) {
|
||||
await message("You must set password to secure your key", {
|
||||
title: "Login",
|
||||
kind: "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await commands.importAccount(key, password);
|
||||
|
||||
if (res.status === "ok") {
|
||||
navigate({ to: "/", replace: true });
|
||||
} else {
|
||||
await message(res.error, {
|
||||
title: "Import Private Ket",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold">Continue with Private Key</h3>
|
||||
</div>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col gap-3 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="size-full flex items-center justify-center"
|
||||
>
|
||||
<div className="w-[320px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h1 className="leading-tight text-xl font-semibold">
|
||||
Import Private Key
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Frame
|
||||
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||
shadow
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Private Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
name="key"
|
||||
type="password"
|
||||
placeholder="nsec or ncryptsec..."
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pasteFromClipboard()}
|
||||
className="absolute uppercase top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{key.length && !key.startsWith("ncryptsec") ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Set password to secure your key
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</Frame>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
Private Key
|
||||
</label>
|
||||
<input
|
||||
name="key"
|
||||
type="text"
|
||||
placeholder="nsec or ncryptsec..."
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Password (Optional)
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
{isPending ? <Spinner /> : "Continue"}
|
||||
</button>
|
||||
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||
Go back to previous screen
|
||||
</GoBack>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Spinner /> : "Login"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
166
src/routes/auth/new.lazy.tsx
Normal file
166
src/routes/auth/new.lazy.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { Frame, GoBack, Spinner } from "@/components";
|
||||
import { NostrQuery } from "@/system";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth/new")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const navigate = Route.useNavigate();
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [picture, setPicture] = useState<string>("");
|
||||
const [name, setName] = useState("");
|
||||
const [about, setAbout] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
const file = await NostrQuery.upload();
|
||||
|
||||
if (file) {
|
||||
setPicture(file);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
startTransition(async () => {
|
||||
if (!name.length) {
|
||||
await message("Please add your name", {
|
||||
title: "New Identity",
|
||||
kind: "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.length) {
|
||||
await message("You must set password to secure your account", {
|
||||
title: "New Identity",
|
||||
kind: "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await commands.createAccount(name, picture, about, password);
|
||||
|
||||
if (res.status === "ok") {
|
||||
navigate({
|
||||
to: "/",
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
await message(res.error, {
|
||||
title: "New Identity",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="size-full flex items-center justify-center"
|
||||
>
|
||||
<div className="w-[320px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h1 className="leading-tight text-xl font-semibold">New Identity</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Frame
|
||||
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||
shadow
|
||||
>
|
||||
<div className="self-center relative rounded-full size-20 bg-neutral-100 dark:bg-neutral-900 my-3">
|
||||
{picture.length ? (
|
||||
<img
|
||||
src={picture}
|
||||
alt="avatar"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Alice"
|
||||
spellCheck={false}
|
||||
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="about"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
About
|
||||
</label>
|
||||
<textarea
|
||||
name="about"
|
||||
value={about}
|
||||
onChange={(e) => setAbout(e.target.value)}
|
||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
||||
spellCheck={false}
|
||||
className="px-3 py-1.5 rounded-lg min-h-16 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-px w-full mt-2 bg-neutral-100 dark:bg-neutral-900" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Set password to secure your account *
|
||||
</label>
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
</Frame>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPending ? <Spinner /> : "Continue"}
|
||||
</button>
|
||||
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||
Go back to previous screen
|
||||
</GoBack>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Spinner } from "@/components";
|
||||
import { NostrAccount } from "@/system";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/auth/remote")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const navigate = Route.useNavigate();
|
||||
|
||||
const [uri, setUri] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
if (!uri.startsWith("bunker://")) {
|
||||
return await message(
|
||||
"You need to enter a valid Connect URI starts with bunker://",
|
||||
{ title: "Nostr Connect", kind: "info" },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const remoteAccount = await NostrAccount.connectRemoteAccount(uri);
|
||||
|
||||
if (remoteAccount?.length) {
|
||||
navigate({ to: "/", replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
await message(String(e), { title: "Nostr Connect", kind: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold">Continue with Nostr Connect</h3>
|
||||
</div>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col gap-1 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
<label
|
||||
htmlFor="uri"
|
||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
Connect URI
|
||||
</label>
|
||||
<input
|
||||
name="uri"
|
||||
type="text"
|
||||
placeholder="bunker://..."
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Spinner /> : "Login"}
|
||||
</button>
|
||||
{loading ? (
|
||||
<p className="text-sm text-center text-neutral-600 dark:text-neutral-400">
|
||||
Waiting confirmation...
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
211
src/routes/index.lazy.tsx
Normal file
211
src/routes/index.lazy.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { commands } from "@/commands.gen";
|
||||
import { displayNpub } from "@/commons";
|
||||
import { Frame, Spinner, User } from "@/components";
|
||||
import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react";
|
||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const context = Route.useRouteContext();
|
||||
const navigate = Route.useNavigate();
|
||||
|
||||
const currentDate = useMemo(
|
||||
() =>
|
||||
new Date().toLocaleString("default", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [value, setValue] = useState("");
|
||||
const [autoLogin, setAutoLogin] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const deleteAccount = async (account: string) => {
|
||||
const res = await commands.deleteAccount(account);
|
||||
|
||||
if (res.status === "ok") {
|
||||
setAccounts((prev) => prev.filter((item) => item !== account));
|
||||
}
|
||||
};
|
||||
|
||||
const selectAccount = (account: string) => {
|
||||
setValue(account);
|
||||
|
||||
if (account.includes("_nostrconnect")) {
|
||||
setAutoLogin(true);
|
||||
}
|
||||
};
|
||||
|
||||
const loginWith = () => {
|
||||
startTransition(async () => {
|
||||
const res = await commands.login(value, password);
|
||||
|
||||
if (res.status === "ok") {
|
||||
navigate({
|
||||
to: "/$account/home",
|
||||
params: { account: res.data },
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
await message(res.error, { title: "Login", kind: "error" });
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const showContextMenu = useCallback(
|
||||
async (e: React.MouseEvent, account: string) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const menuItems = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "Delete account",
|
||||
action: async () => await deleteAccount(account),
|
||||
}),
|
||||
]);
|
||||
|
||||
const menu = await Menu.new({
|
||||
items: menuItems,
|
||||
});
|
||||
|
||||
await menu.popup().catch((e) => console.error(e));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoLogin) {
|
||||
loginWith();
|
||||
}
|
||||
}, [autoLogin, value]);
|
||||
|
||||
useEffect(() => {
|
||||
setAccounts(context.accounts);
|
||||
}, [context.accounts]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative size-full flex items-center justify-center"
|
||||
>
|
||||
<div className="w-[320px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h3 className="leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
{currentDate}
|
||||
</h3>
|
||||
<h1 className="leading-tight text-xl font-semibold">Welcome back!</h1>
|
||||
</div>
|
||||
<Frame
|
||||
className="flex flex-col w-full divide-y divide-neutral-100 dark:divide-white/5 rounded-xl overflow-hidden"
|
||||
shadow
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<div
|
||||
key={account}
|
||||
onClick={() => selectAccount(account)}
|
||||
onKeyDown={() => selectAccount(account)}
|
||||
className="group flex items-center gap-2 hover:bg-black/5 dark:hover:bg-white/5 p-3"
|
||||
>
|
||||
<User.Provider pubkey={account.replace("_nostrconnect", "")}>
|
||||
<User.Root className="flex-1 flex items-center gap-2.5">
|
||||
<User.Avatar className="rounded-full size-10" />
|
||||
{value === account && !value.includes("_nostrconnect") ? (
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") loginWith();
|
||||
}}
|
||||
placeholder="Password"
|
||||
className="px-3 rounded-full w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex flex-col items-start">
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
||||
{account.includes("_nostrconnect") ? (
|
||||
<div className="text-[8px] border border-blue-500 text-blue-500 px-1.5 rounded-full">
|
||||
Nostr Connect
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{displayNpub(account.replace("_nostrconnect", ""), 16)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="inline-flex items-center justify-center size-8 shrink-0">
|
||||
{value === account ? (
|
||||
isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loginWith()}
|
||||
className="rounded-full size-10 inline-flex items-center justify-center"
|
||||
>
|
||||
<ArrowRight className="size-5" />
|
||||
</button>
|
||||
)
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => showContextMenu(e, account)}
|
||||
className="rounded-full size-10 hidden group-hover:inline-flex items-center justify-center"
|
||||
>
|
||||
<DotsThree className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Link
|
||||
to="/new"
|
||||
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 p-3">
|
||||
<div className="inline-flex items-center justify-center rounded-full size-10 bg-neutral-200 dark:bg-white/10">
|
||||
<Plus className="size-5" />
|
||||
</div>
|
||||
<span className="truncate text-sm font-medium leading-tight">
|
||||
New account
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</Frame>
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<Link
|
||||
to="/bootstrap-relays"
|
||||
className="h-8 w-max text-xs px-3 inline-flex items-center justify-center gap-1.5 bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-full"
|
||||
>
|
||||
<GearSix className="size-4" />
|
||||
Manage Relays
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,140 +1,22 @@
|
||||
import { checkForAppUpdates, displayNpub } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { PlusIcon, RelayIcon } from "@/components";
|
||||
import { User } from "@/components/user";
|
||||
import { checkForAppUpdates } from "@/commons";
|
||||
import { NostrAccount } from "@/system";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
beforeLoad: async () => {
|
||||
// Check for app updates
|
||||
// TODO: move this function to rust
|
||||
await checkForAppUpdates(true);
|
||||
|
||||
// Get all accounts
|
||||
// TODO: use emit & listen
|
||||
const accounts = await NostrAccount.getAccounts();
|
||||
|
||||
if (accounts.length < 1) {
|
||||
throw redirect({
|
||||
to: "/landing",
|
||||
to: "/new",
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { accounts };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const navigate = Route.useNavigate();
|
||||
const context = Route.useRouteContext();
|
||||
|
||||
const [loading, setLoading] = useState({ npub: "", status: false });
|
||||
|
||||
const select = async (npub: string) => {
|
||||
try {
|
||||
setLoading({ npub, status: true });
|
||||
|
||||
const status = await NostrAccount.loadAccount(npub);
|
||||
|
||||
if (status) {
|
||||
return navigate({
|
||||
to: "/$account/home",
|
||||
params: { account: npub },
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading({ npub: "", status: false });
|
||||
await message(String(e), {
|
||||
title: "Account",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const currentDate = new Date().toLocaleString("default", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative flex flex-col items-center justify-between w-full h-full"
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="absolute top-0 left-0 h-14 w-full"
|
||||
/>
|
||||
<div className="flex items-end justify-center flex-1 w-full px-4 pb-10">
|
||||
<div className="text-center">
|
||||
<h2 className="mb-1 text-lg text-neutral-700 dark:text-neutral-300">
|
||||
{currentDate}
|
||||
</h2>
|
||||
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center flex-1 w-full gap-3">
|
||||
<div className="flex flex-col w-full max-w-sm mx-auto overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
||||
{context.accounts.map((account) => (
|
||||
<div
|
||||
key={account}
|
||||
onClick={() => select(account)}
|
||||
onKeyDown={() => select(account)}
|
||||
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
||||
>
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root className="flex items-center gap-2.5 p-3">
|
||||
<User.Avatar className="rounded-full size-10" />
|
||||
<div className="inline-flex flex-col items-start">
|
||||
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
||||
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{displayNpub(account, 16)}
|
||||
</span>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="inline-flex items-center justify-center size-10">
|
||||
{loading.npub === account ? (
|
||||
loading.status ? (
|
||||
<Spinner />
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Link
|
||||
to="/landing"
|
||||
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 p-3">
|
||||
<div className="inline-flex items-center justify-center rounded-full size-10 bg-neutral-200 dark:bg-white/10">
|
||||
<PlusIcon className="size-5" />
|
||||
</div>
|
||||
<span className="max-w-[6rem] truncate text-sm font-medium leading-tight">
|
||||
Add account
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="w-full max-w-sm mx-auto">
|
||||
<Link
|
||||
to="/bootstrap-relays"
|
||||
className="inline-flex items-center justify-center w-full h-8 gap-2 px-2 text-xs font-medium rounded-lg bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 text-neutral-700 dark:text-white/40"
|
||||
>
|
||||
<RelayIcon className="size-4" />
|
||||
Custom Bootstrap Relays
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { KeyIcon, RemoteIcon } from "@/components";
|
||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/landing")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex flex-col items-center justify-center w-screen h-screen"
|
||||
>
|
||||
<div className="w-full max-w-xs mx-auto lg:max-w-md">
|
||||
<div className="flex flex-col w-full gap-2 px-2 bg-white rounded-xl shadow-primary dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
|
||||
<div className="flex items-center h-20 border-b border-neutral-100 dark:border-white/5">
|
||||
<Link
|
||||
to="/auth/create-profile"
|
||||
className="flex items-center justify-center w-full gap-2 px-2 rounded-lg h-14 hover:bg-neutral-100 dark:hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex items-center justify-center rounded-full size-9 shrink-0">
|
||||
<img
|
||||
src="/icon.jpeg"
|
||||
alt="App Icon"
|
||||
className="object-cover rounded-full size-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="inline-flex flex-col flex-1">
|
||||
<span className="font-semibold leading-tight">
|
||||
Create new account
|
||||
</span>
|
||||
<span className="text-sm leading-tight text-neutral-500">
|
||||
Use everywhere
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 pb-2.5">
|
||||
<Link
|
||||
to="/auth/import"
|
||||
className="inline-flex items-center w-full gap-2 px-2 rounded-lg h-11 hover:bg-neutral-100 dark:hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex items-center justify-center size-9">
|
||||
<KeyIcon className="size-5 text-neutral-600 dark:text-neutral-400" />
|
||||
</div>
|
||||
Login with Private Key
|
||||
</Link>
|
||||
<Link
|
||||
to="/auth/remote"
|
||||
className="inline-flex items-center w-full gap-2 px-2 rounded-lg h-11 hover:bg-neutral-100 dark:hover:bg-white/10"
|
||||
>
|
||||
<div className="inline-flex items-center justify-center size-9">
|
||||
<RemoteIcon className="size-5 text-neutral-600 dark:text-neutral-400" />
|
||||
</div>
|
||||
Nostr Connect
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/routes/new.lazy.tsx
Normal file
45
src/routes/new.lazy.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/new")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="size-full flex items-center justify-center"
|
||||
>
|
||||
<div className="w-[320px] flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h1 className="leading-tight text-xl font-semibold">
|
||||
Welcome to Nostr.
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link
|
||||
to="/auth/new"
|
||||
className="w-full h-10 bg-blue-500 font-medium hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
||||
>
|
||||
Create a new identity
|
||||
</Link>
|
||||
<div className="w-full h-px bg-black/5 dark:bg-white/5" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
to="/auth/connect"
|
||||
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-100 dark:bg-white dark:text-black rounded-lg inline-flex items-center justify-center"
|
||||
>
|
||||
Login with Nostr Connect
|
||||
</Link>
|
||||
<Link
|
||||
to="/auth/import"
|
||||
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-100 dark:bg-white dark:text-black rounded-lg inline-flex items-center justify-center"
|
||||
>
|
||||
Login with Private Key
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,19 +11,19 @@ import { Await } from "@tanstack/react-router";
|
||||
import { Suspense, useCallback } from "react";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/users/$pubkey")({
|
||||
export const Route = createFileRoute("/users/$id")({
|
||||
beforeLoad: async () => {
|
||||
const settings = await NostrQuery.getUserSettings();
|
||||
return { settings };
|
||||
},
|
||||
loader: async ({ params }) => {
|
||||
return { data: defer(NostrQuery.getUserEvents(params.pubkey)) };
|
||||
return { data: defer(NostrQuery.getUserEvents(params.id)) };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { pubkey } = Route.useParams();
|
||||
const { id } = Route.useParams();
|
||||
const { data } = Route.useLoaderData();
|
||||
|
||||
const renderItem = useCallback(
|
||||
@@ -52,7 +52,7 @@ function Screen() {
|
||||
<Container withDrag>
|
||||
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5">
|
||||
<WindowVirtualizer>
|
||||
<User.Provider pubkey={pubkey}>
|
||||
<User.Provider pubkey={id}>
|
||||
<User.Root>
|
||||
<User.Cover className="object-cover w-full h-44" />
|
||||
<div className="relative flex flex-col px-3 -mt-8">
|
||||
Reference in New Issue
Block a user