feat: improve account management

This commit is contained in:
reya
2024-08-13 10:33:21 +07:00
parent be16d5c21d
commit 4cb49d44c7
25 changed files with 1006 additions and 785 deletions

View File

@@ -48,49 +48,41 @@ async saveBootstrapRelays(relays: string) : Promise<Result<null, string>> {
async getAccounts() : Promise<string[]> {
return await TAURI_INVOKE("get_accounts");
},
async createAccount() : Promise<Result<Account, string>> {
async createAccount(name: string, about: string, picture: string, password: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("create_account") };
return { status: "ok", data: await TAURI_INVOKE("create_account", { name, about, picture, password }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async saveAccount(nsec: string, password: string) : Promise<Result<string, string>> {
async importAccount(key: string, password: string | null) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("save_account", { nsec, password }) };
return { status: "ok", data: await TAURI_INVOKE("import_account", { key, password }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getEncryptedKey(npub: string, password: string) : Promise<Result<string, string>> {
async connectAccount(uri: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_encrypted_key", { npub, password }) };
return { status: "ok", data: await TAURI_INVOKE("connect_account", { uri }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getPrivateKey(npub: string) : Promise<Result<string, string>> {
async deleteAccount(id: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_private_key", { npub }) };
return { status: "ok", data: await TAURI_INVOKE("delete_account", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async connectRemoteAccount(uri: string) : Promise<Result<string, string>> {
async login(account: string, password: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async loadAccount(npub: string, bunker: string | null) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("load_account", { npub, bunker }) };
return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -461,7 +453,6 @@ async setBadge(count: number) : Promise<void> {
/** user-defined types **/
export type Account = { npub: string; nsec: string }
export type Column = { label: string; url: string; x: number; y: number; width: number; height: number }
export type Meta = { content: string; images: string[]; videos: string[]; events: string[]; mentions: string[]; hashtags: string[] }
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }

20
src/components/back.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { cn } from "@/commons";
import { useRouter } from "@tanstack/react-router";
import type { ReactNode } from "react";
export function GoBack({
children,
className,
}: { children: ReactNode | ReactNode[]; className?: string }) {
const { history } = useRouter();
return (
<button
type="button"
onClick={() => history.go(-1)}
className={cn(className)}
>
{children}
</button>
);
}

27
src/components/frame.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { cn } from "@/commons";
import { useRouteContext } from "@tanstack/react-router";
import type { ReactNode } from "react";
export function Frame({
children,
shadow,
className,
}: { children: ReactNode; shadow?: boolean; className?: string }) {
const { platform } = useRouteContext({ strict: false });
return (
<div
className={cn(
className,
platform === "linux"
? "bg-white dark:bg-neutral-950"
: "bg-white dark:bg-white/10",
shadow
? "shadow-lg shadow-neutral-500/10 dark:shadow-none dark:ring-1 dark:ring-white/20"
: "",
)}
>
{children}
</div>
);
}

View File

@@ -1,4 +1,6 @@
export * from "./container";
export * from "./frame";
export * from "./back";
export * from "./box";
export * from "./spinner";
export * from "./quote";

View File

@@ -26,11 +26,11 @@ import { Route as CreateTopicImport } from './routes/create-topic'
import { Route as CreateNewsfeedImport } from './routes/create-newsfeed'
import { Route as CreateGroupImport } from './routes/create-group'
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
import { Route as AccountImport } from './routes/$account'
import { Route as IndexImport } from './routes/index'
import { Route as EditorIndexImport } from './routes/editor/index'
import { Route as AccountIndexImport } from './routes/$account/index'
import { Route as ZapIdImport } from './routes/zap.$id'
import { Route as UsersPubkeyImport } from './routes/users/$pubkey'
import { Route as UsersIdImport } from './routes/users.$id'
import { Route as TrendingUsersImport } from './routes/trending.users'
import { Route as TrendingNotesImport } from './routes/trending.notes'
import { Route as SettingsWalletImport } from './routes/settings/wallet'
@@ -44,29 +44,23 @@ import { Route as SearchNotesImport } from './routes/search.notes'
import { Route as EventsIdImport } from './routes/events/$id'
import { Route as CreateNewsfeedUsersImport } from './routes/create-newsfeed.users'
import { Route as CreateNewsfeedF2fImport } from './routes/create-newsfeed.f2f'
import { Route as AuthCreateProfileImport } from './routes/auth/create-profile'
import { Route as AccountPanelImport } from './routes/$account/panel'
import { Route as AccountHomeImport } from './routes/$account/home'
import { Route as AuthAccountBackupImport } from './routes/auth/$account.backup'
import { Route as AccountBackupImport } from './routes/$account/backup'
// Create Virtual Routes
const LandingLazyImport = createFileRoute('/landing')()
const AuthLazyImport = createFileRoute('/auth')()
const AuthRemoteLazyImport = createFileRoute('/auth/remote')()
const NewLazyImport = createFileRoute('/new')()
const AuthNewLazyImport = createFileRoute('/auth/new')()
const AuthImportLazyImport = createFileRoute('/auth/import')()
const AuthConnectLazyImport = createFileRoute('/auth/connect')()
const AccountPanelLazyImport = createFileRoute('/$account/panel')()
// Create/Update Routes
const LandingLazyRoute = LandingLazyImport.update({
path: '/landing',
const NewLazyRoute = NewLazyImport.update({
path: '/new',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/landing.lazy').then((d) => d.Route))
const AuthLazyRoute = AuthLazyImport.update({
path: '/auth',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/auth.lazy').then((d) => d.Route))
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
const TrendingRoute = TrendingImport.update({
path: '/trending',
@@ -133,40 +127,50 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
getParentRoute: () => rootRoute,
} as any)
const AccountRoute = AccountImport.update({
path: '/$account',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/$account.lazy').then((d) => d.Route))
const IndexRoute = IndexImport.update({
path: '/',
getParentRoute: () => rootRoute,
} as any)
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
const EditorIndexRoute = EditorIndexImport.update({
path: '/editor/',
getParentRoute: () => rootRoute,
} as any)
const AccountIndexRoute = AccountIndexImport.update({
path: '/$account/',
const AuthNewLazyRoute = AuthNewLazyImport.update({
path: '/auth/new',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/$account/index.lazy').then((d) => d.Route),
)
const AuthRemoteLazyRoute = AuthRemoteLazyImport.update({
path: '/remote',
getParentRoute: () => AuthLazyRoute,
} as any).lazy(() => import('./routes/auth/remote.lazy').then((d) => d.Route))
} as any).lazy(() => import('./routes/auth/new.lazy').then((d) => d.Route))
const AuthImportLazyRoute = AuthImportLazyImport.update({
path: '/import',
getParentRoute: () => AuthLazyRoute,
path: '/auth/import',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/auth/import.lazy').then((d) => d.Route))
const AuthConnectLazyRoute = AuthConnectLazyImport.update({
path: '/auth/connect',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/auth/connect.lazy').then((d) => d.Route))
const AccountPanelLazyRoute = AccountPanelLazyImport.update({
path: '/panel',
getParentRoute: () => AccountRoute,
} as any).lazy(() =>
import('./routes/$account/panel.lazy').then((d) => d.Route),
)
const ZapIdRoute = ZapIdImport.update({
path: '/zap/$id',
getParentRoute: () => rootRoute,
} as any)
const UsersPubkeyRoute = UsersPubkeyImport.update({
path: '/users/$pubkey',
const UsersIdRoute = UsersIdImport.update({
path: '/users/$id',
getParentRoute: () => rootRoute,
} as any)
@@ -235,26 +239,14 @@ const CreateNewsfeedF2fRoute = CreateNewsfeedF2fImport.update({
getParentRoute: () => CreateNewsfeedRoute,
} as any)
const AuthCreateProfileRoute = AuthCreateProfileImport.update({
path: '/create-profile',
getParentRoute: () => AuthLazyRoute,
} as any)
const AccountPanelRoute = AccountPanelImport.update({
path: '/$account/panel',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/$account/panel.lazy').then((d) => d.Route),
)
const AccountHomeRoute = AccountHomeImport.update({
path: '/$account/home',
getParentRoute: () => rootRoute,
path: '/home',
getParentRoute: () => AccountRoute,
} as any).lazy(() => import('./routes/$account/home.lazy').then((d) => d.Route))
const AuthAccountBackupRoute = AuthAccountBackupImport.update({
path: '/$account/backup',
getParentRoute: () => AuthLazyRoute,
const AccountBackupRoute = AccountBackupImport.update({
path: '/backup',
getParentRoute: () => AccountRoute,
} as any)
// Populate the FileRoutesByPath interface
@@ -268,6 +260,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/$account': {
id: '/$account'
path: '/$account'
fullPath: '/$account'
preLoaderRoute: typeof AccountImport
parentRoute: typeof rootRoute
}
'/bootstrap-relays': {
id: '/bootstrap-relays'
path: '/bootstrap-relays'
@@ -359,40 +358,26 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TrendingImport
parentRoute: typeof rootRoute
}
'/auth': {
id: '/auth'
path: '/auth'
fullPath: '/auth'
preLoaderRoute: typeof AuthLazyImport
'/new': {
id: '/new'
path: '/new'
fullPath: '/new'
preLoaderRoute: typeof NewLazyImport
parentRoute: typeof rootRoute
}
'/landing': {
id: '/landing'
path: '/landing'
fullPath: '/landing'
preLoaderRoute: typeof LandingLazyImport
parentRoute: typeof rootRoute
'/$account/backup': {
id: '/$account/backup'
path: '/backup'
fullPath: '/$account/backup'
preLoaderRoute: typeof AccountBackupImport
parentRoute: typeof AccountImport
}
'/$account/home': {
id: '/$account/home'
path: '/$account/home'
path: '/home'
fullPath: '/$account/home'
preLoaderRoute: typeof AccountHomeImport
parentRoute: typeof rootRoute
}
'/$account/panel': {
id: '/$account/panel'
path: '/$account/panel'
fullPath: '/$account/panel'
preLoaderRoute: typeof AccountPanelImport
parentRoute: typeof rootRoute
}
'/auth/create-profile': {
id: '/auth/create-profile'
path: '/create-profile'
fullPath: '/auth/create-profile'
preLoaderRoute: typeof AuthCreateProfileImport
parentRoute: typeof AuthLazyImport
parentRoute: typeof AccountImport
}
'/create-newsfeed/f2f': {
id: '/create-newsfeed/f2f'
@@ -485,11 +470,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TrendingUsersImport
parentRoute: typeof TrendingImport
}
'/users/$pubkey': {
id: '/users/$pubkey'
path: '/users/$pubkey'
fullPath: '/users/$pubkey'
preLoaderRoute: typeof UsersPubkeyImport
'/users/$id': {
id: '/users/$id'
path: '/users/$id'
fullPath: '/users/$id'
preLoaderRoute: typeof UsersIdImport
parentRoute: typeof rootRoute
}
'/zap/$id': {
@@ -499,25 +484,32 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ZapIdImport
parentRoute: typeof rootRoute
}
'/$account/panel': {
id: '/$account/panel'
path: '/panel'
fullPath: '/$account/panel'
preLoaderRoute: typeof AccountPanelLazyImport
parentRoute: typeof AccountImport
}
'/auth/connect': {
id: '/auth/connect'
path: '/auth/connect'
fullPath: '/auth/connect'
preLoaderRoute: typeof AuthConnectLazyImport
parentRoute: typeof rootRoute
}
'/auth/import': {
id: '/auth/import'
path: '/import'
path: '/auth/import'
fullPath: '/auth/import'
preLoaderRoute: typeof AuthImportLazyImport
parentRoute: typeof AuthLazyImport
parentRoute: typeof rootRoute
}
'/auth/remote': {
id: '/auth/remote'
path: '/remote'
fullPath: '/auth/remote'
preLoaderRoute: typeof AuthRemoteLazyImport
parentRoute: typeof AuthLazyImport
}
'/$account/': {
id: '/$account/'
path: '/$account'
fullPath: '/$account'
preLoaderRoute: typeof AccountIndexImport
'/auth/new': {
id: '/auth/new'
path: '/auth/new'
fullPath: '/auth/new'
preLoaderRoute: typeof AuthNewLazyImport
parentRoute: typeof rootRoute
}
'/editor/': {
@@ -527,13 +519,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof EditorIndexImport
parentRoute: typeof rootRoute
}
'/auth/$account/backup': {
id: '/auth/$account/backup'
path: '/$account/backup'
fullPath: '/auth/$account/backup'
preLoaderRoute: typeof AuthAccountBackupImport
parentRoute: typeof AuthLazyImport
}
}
}
@@ -541,6 +526,11 @@ declare module '@tanstack/react-router' {
export const routeTree = rootRoute.addChildren({
IndexRoute,
AccountRoute: AccountRoute.addChildren({
AccountBackupRoute,
AccountHomeRoute,
AccountPanelLazyRoute,
}),
BootstrapRelaysRoute,
CreateGroupRoute,
CreateNewsfeedRoute: CreateNewsfeedRoute.addChildren({
@@ -567,19 +557,13 @@ export const routeTree = rootRoute.addChildren({
TrendingNotesRoute,
TrendingUsersRoute,
}),
AuthLazyRoute: AuthLazyRoute.addChildren({
AuthCreateProfileRoute,
AuthImportLazyRoute,
AuthRemoteLazyRoute,
AuthAccountBackupRoute,
}),
LandingLazyRoute,
AccountHomeRoute,
AccountPanelRoute,
NewLazyRoute,
EventsIdRoute,
UsersPubkeyRoute,
UsersIdRoute,
ZapIdRoute,
AccountIndexRoute,
AuthConnectLazyRoute,
AuthImportLazyRoute,
AuthNewLazyRoute,
EditorIndexRoute,
})
@@ -592,6 +576,7 @@ export const routeTree = rootRoute.addChildren({
"filePath": "__root.tsx",
"children": [
"/",
"/$account",
"/bootstrap-relays",
"/create-group",
"/create-newsfeed",
@@ -605,20 +590,27 @@ export const routeTree = rootRoute.addChildren({
"/store",
"/topic",
"/trending",
"/auth",
"/landing",
"/$account/home",
"/$account/panel",
"/new",
"/events/$id",
"/users/$pubkey",
"/users/$id",
"/zap/$id",
"/$account/",
"/auth/connect",
"/auth/import",
"/auth/new",
"/editor/"
]
},
"/": {
"filePath": "index.tsx"
},
"/$account": {
"filePath": "$account.tsx",
"children": [
"/$account/backup",
"/$account/home",
"/$account/panel"
]
},
"/bootstrap-relays": {
"filePath": "bootstrap-relays.tsx"
},
@@ -678,27 +670,16 @@ export const routeTree = rootRoute.addChildren({
"/trending/users"
]
},
"/auth": {
"filePath": "auth.lazy.tsx",
"children": [
"/auth/create-profile",
"/auth/import",
"/auth/remote",
"/auth/$account/backup"
]
"/new": {
"filePath": "new.lazy.tsx"
},
"/landing": {
"filePath": "landing.lazy.tsx"
"/$account/backup": {
"filePath": "$account/backup.tsx",
"parent": "/$account"
},
"/$account/home": {
"filePath": "$account/home.tsx"
},
"/$account/panel": {
"filePath": "$account/panel.tsx"
},
"/auth/create-profile": {
"filePath": "auth/create-profile.tsx",
"parent": "/auth"
"filePath": "$account/home.tsx",
"parent": "/$account"
},
"/create-newsfeed/f2f": {
"filePath": "create-newsfeed.f2f.tsx",
@@ -751,29 +732,27 @@ export const routeTree = rootRoute.addChildren({
"filePath": "trending.users.tsx",
"parent": "/trending"
},
"/users/$pubkey": {
"filePath": "users/$pubkey.tsx"
"/users/$id": {
"filePath": "users.$id.tsx"
},
"/zap/$id": {
"filePath": "zap.$id.tsx"
},
"/$account/panel": {
"filePath": "$account/panel.lazy.tsx",
"parent": "/$account"
},
"/auth/connect": {
"filePath": "auth/connect.lazy.tsx"
},
"/auth/import": {
"filePath": "auth/import.lazy.tsx",
"parent": "/auth"
"filePath": "auth/import.lazy.tsx"
},
"/auth/remote": {
"filePath": "auth/remote.lazy.tsx",
"parent": "/auth"
},
"/$account/": {
"filePath": "$account/index.tsx"
"/auth/new": {
"filePath": "auth/new.lazy.tsx"
},
"/editor/": {
"filePath": "editor/index.tsx"
},
"/auth/$account/backup": {
"filePath": "auth/$account.backup.tsx",
"parent": "/auth"
}
}
}

View File

@@ -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,
});

View File

@@ -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();

View File

@@ -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,
});

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);

View 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>
);
}

View File

@@ -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
View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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
View 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>
);
}

View File

@@ -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">