feat: add set up inbox relay screen
This commit is contained in:
@@ -48,9 +48,9 @@ pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result<String,
|
|||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn create_account(
|
pub async fn create_account(
|
||||||
name: String,
|
name: String,
|
||||||
picture: String,
|
picture: Option<String>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<(), String> {
|
) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
||||||
@@ -66,11 +66,18 @@ pub async fn create_account(
|
|||||||
client.set_signer(Some(signer)).await;
|
client.set_signer(Some(signer)).await;
|
||||||
|
|
||||||
// Update metadata
|
// Update metadata
|
||||||
let url = Url::parse(&picture).map_err(|e| e.to_string())?;
|
let url = match picture {
|
||||||
let metadata = Metadata::new().display_name(name).picture(url);
|
Some(p) => Some(Url::parse(&p).map_err(|e| e.to_string())?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = match url {
|
||||||
|
Some(picture) => Metadata::new().display_name(name).picture(picture),
|
||||||
|
None => Metadata::new().display_name(name),
|
||||||
|
};
|
||||||
|
|
||||||
match client.set_metadata(&metadata).await {
|
match client.set_metadata(&metadata).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(npub),
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,51 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, ()
|
|||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_inbox(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let public_key = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||||
|
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||||
|
|
||||||
|
match client.get_events_of(vec![inbox], None).await {
|
||||||
|
Ok(events) => {
|
||||||
|
if let Some(event) = events.into_iter().next() {
|
||||||
|
let urls = event
|
||||||
|
.tags()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|tag| {
|
||||||
|
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||||
|
Some(relay.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(urls)
|
||||||
|
} else {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn set_inbox(relays: Vec<String>, state: State<'_, Nostr>) -> Result<(), String> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::<Vec<_>>();
|
||||||
|
let event = EventBuilder::new(Kind::Custom(10050), "", tags);
|
||||||
|
|
||||||
|
match client.send_event_builder(event).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
@@ -222,6 +274,8 @@ pub async fn login(
|
|||||||
|
|
||||||
let mut inbox_relays = state.inbox_relays.lock().await;
|
let mut inbox_relays = state.inbox_relays.lock().await;
|
||||||
inbox_relays.insert(public_key, urls);
|
inbox_relays.insert(public_key, urls);
|
||||||
|
} else {
|
||||||
|
return Err("404".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ fn main() {
|
|||||||
get_contact_list,
|
get_contact_list,
|
||||||
get_chats,
|
get_chats,
|
||||||
get_chat_messages,
|
get_chat_messages,
|
||||||
|
get_inbox,
|
||||||
|
set_inbox,
|
||||||
connect_inbox,
|
connect_inbox,
|
||||||
disconnect_inbox,
|
disconnect_inbox,
|
||||||
send_message,
|
send_message,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ try {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async createAccount(name: string, picture: string) : Promise<Result<null, string>> {
|
async createAccount(name: string, picture: string | null) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("create_account", { name, picture }) };
|
return { status: "ok", data: await TAURI_INVOKE("create_account", { name, picture }) };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -71,6 +71,22 @@ try {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async getInbox(id: string) : Promise<Result<string[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_inbox", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setInbox(relays: string[]) : Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("set_inbox", { relays }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
async connectInbox(id: string) : Promise<Result<string[], string>> {
|
async connectInbox(id: string) : Promise<Result<string[], string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("connect_inbox", { id }) };
|
return { status: "ok", data: await TAURI_INVOKE("connect_inbox", { id }) };
|
||||||
|
|||||||
20
src/components/back.tsx
Normal file
20
src/components/back.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ const NostrConnectLazyImport = createFileRoute('/nostr-connect')()
|
|||||||
const NewLazyImport = createFileRoute('/new')()
|
const NewLazyImport = createFileRoute('/new')()
|
||||||
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
||||||
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
||||||
|
const AccountRelaysLazyImport = createFileRoute('/$account/relays')()
|
||||||
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
||||||
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
||||||
|
|
||||||
@@ -53,7 +54,14 @@ const CreateAccountLazyRoute = CreateAccountLazyImport.update({
|
|||||||
const IndexRoute = IndexImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const AccountRelaysLazyRoute = AccountRelaysLazyImport.update({
|
||||||
|
path: '/$account/relays',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/$account.relays.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
||||||
path: '/$account/chats',
|
path: '/$account/chats',
|
||||||
@@ -136,6 +144,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AccountChatsLazyImport
|
preLoaderRoute: typeof AccountChatsLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/$account/relays': {
|
||||||
|
id: '/$account/relays'
|
||||||
|
path: '/$account/relays'
|
||||||
|
fullPath: '/$account/relays'
|
||||||
|
preLoaderRoute: typeof AccountRelaysLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/$account/chats/$id': {
|
'/$account/chats/$id': {
|
||||||
id: '/$account/chats/$id'
|
id: '/$account/chats/$id'
|
||||||
path: '/$id'
|
path: '/$id'
|
||||||
@@ -166,6 +181,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
AccountChatsIdRoute,
|
AccountChatsIdRoute,
|
||||||
AccountChatsNewLazyRoute,
|
AccountChatsNewLazyRoute,
|
||||||
}),
|
}),
|
||||||
|
AccountRelaysLazyRoute,
|
||||||
})
|
})
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
@@ -182,7 +198,8 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/new",
|
"/new",
|
||||||
"/nostr-connect",
|
"/nostr-connect",
|
||||||
"/$account/contacts",
|
"/$account/contacts",
|
||||||
"/$account/chats"
|
"/$account/chats",
|
||||||
|
"/$account/relays"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@@ -210,6 +227,9 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/$account/chats/new"
|
"/$account/chats/new"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"/$account/relays": {
|
||||||
|
"filePath": "$account.relays.lazy.tsx"
|
||||||
|
},
|
||||||
"/$account/chats/$id": {
|
"/$account/chats/$id": {
|
||||||
"filePath": "$account.chats.$id.tsx",
|
"filePath": "$account.chats.$id.tsx",
|
||||||
"parent": "/$account/chats"
|
"parent": "/$account/chats"
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { commands } from "@/commands";
|
|||||||
import { ago, cn } from "@/commons";
|
import { ago, cn } from "@/commons";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { ArrowRight, CirclesFour, Plus, X } from "@phosphor-icons/react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
CaretDown,
|
||||||
|
CirclesFour,
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -48,7 +54,7 @@ function Header() {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 h-12 flex items-center justify-between",
|
"shrink-0 h-12 flex items-center justify-between",
|
||||||
platform === "macos" ? "pl-24 pr-3.5" : "px-3.5",
|
platform === "macos" ? "pl-[78px] pr-3.5" : "px-3.5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CurrentUser />
|
<CurrentUser />
|
||||||
@@ -153,6 +159,12 @@ function ChatList() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : !data?.length ? (
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="px-2 h-12 w-full rounded-lg bg-black/5 dark:bg-white/5 flex items-center justify-center text-sm">
|
||||||
|
No chats.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((item) => (
|
data.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -380,13 +392,14 @@ function CurrentUser() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => showContextMenu(e)}
|
onClick={(e) => showContextMenu(e)}
|
||||||
className="shrink-0 size-8 flex items-center justify-center rounded-full ring-1 ring-teal-500"
|
className="h-8 inline-flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={params.account}>
|
<User.Provider pubkey={params.account}>
|
||||||
<User.Root className="shrink-0">
|
<User.Root className="shrink-0">
|
||||||
<User.Avatar className="size-7 rounded-full" />
|
<User.Avatar className="size-8 rounded-full" />
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
|
<CaretDown className="size-3 text-neutral-600 dark:text-neutral-400" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { CoopIcon } from "@/icons/coop";
|
import { CoopIcon } from "@/icons/coop";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/chats/new")({
|
export const Route = createLazyFileRoute("/$account/chats/new")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -7,8 +7,14 @@ export const Route = createLazyFileRoute("/$account/chats/new")({
|
|||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
return (
|
return (
|
||||||
<div className="size-full flex items-center justify-center">
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="size-full flex flex-col gap-3 items-center justify-center"
|
||||||
|
>
|
||||||
<CoopIcon className="size-10 text-neutral-200 dark:text-neutral-800" />
|
<CoopIcon className="size-10 text-neutral-200 dark:text-neutral-800" />
|
||||||
|
<h1 className="text-center font-bold text-neutral-300 dark:text-neutral-700">
|
||||||
|
coop on nostr.
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
166
src/routes/$account.relays.lazy.tsx
Normal file
166
src/routes/$account.relays.lazy.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { Frame } from "@/components/frame";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { Plus, X } from "@phosphor-icons/react";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/$account/relays")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
|
const [newRelay, setNewRelay] = useState("");
|
||||||
|
const [relays, setRelays] = useState<string[]>([]);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const add = () => {
|
||||||
|
try {
|
||||||
|
let url = newRelay;
|
||||||
|
|
||||||
|
if (relays.length >= 3) {
|
||||||
|
return message("You should keep relay lists small (1 - 3 relays).", {
|
||||||
|
kind: "info",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.startsWith("wss://")) {
|
||||||
|
url = `wss://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
const relay = new URL(url);
|
||||||
|
|
||||||
|
// Update
|
||||||
|
setRelays((prev) => [...prev, relay.toString()]);
|
||||||
|
setNewRelay("");
|
||||||
|
} catch {
|
||||||
|
message("URL is not valid.", { kind: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
if (!relays.length) {
|
||||||
|
await message("You need to add at least 1 relay", { kind: "info" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await commands.setInbox(relays);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({
|
||||||
|
to: "/",
|
||||||
|
params: { account },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await message(res.error, {
|
||||||
|
title: "Inbox Relays",
|
||||||
|
kind: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getRelays() {
|
||||||
|
const res = await commands.getInbox(account);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
setRelays((prev) => [...prev, ...res.data]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRelays();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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">Inbox Relays</h1>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
Inbox Relay is used to receive message from others
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Frame
|
||||||
|
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
<p className="mb-1.5">
|
||||||
|
You need to set at least 1 inbox relay in order to receive
|
||||||
|
message from others.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you don't know which relay to add, you can use{" "}
|
||||||
|
<span
|
||||||
|
onClick={() => setNewRelay("wss://auth.nostr1.com")}
|
||||||
|
onKeyDown={() => setNewRelay("wss://auth.nostr1.com")}
|
||||||
|
className="font-semibold"
|
||||||
|
>
|
||||||
|
auth.nostr1.com
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
name="relay"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex: relay.nostr.net, ..."
|
||||||
|
value={newRelay}
|
||||||
|
onChange={(e) => setNewRelay(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") add();
|
||||||
|
}}
|
||||||
|
className="flex-1 px-3 rounded-lg h-9 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => add()}
|
||||||
|
className="inline-flex items-center justify-center size-9 rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<Plus className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{relays.map((relay) => (
|
||||||
|
<div
|
||||||
|
key={relay}
|
||||||
|
className="flex items-center justify-between h-9 px-2 rounded-lg bg-neutral-100 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{relay}</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center rounded-md size-7 text-neutral-700 dark:text-white/20 hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<X className="size-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Frame>
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
disabled={isPending || !relays.length}
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
|
import { GoBack } from "@/components/back";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
@@ -12,7 +13,7 @@ export const Route = createLazyFileRoute("/create-account")({
|
|||||||
function Screen() {
|
function Screen() {
|
||||||
const navigate = Route.useNavigate();
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
const [picture, setPicture] = useState("");
|
const [picture, setPicture] = useState(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
@@ -21,7 +22,11 @@ function Screen() {
|
|||||||
const res = await commands.createAccount(name, picture);
|
const res = await commands.createAccount(name, picture);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
navigate({ to: "/", replace: true });
|
navigate({
|
||||||
|
to: "/$account/relays",
|
||||||
|
params: { account: res.data },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await message(res.error, {
|
await message(res.error, {
|
||||||
title: "New Identity",
|
title: "New Identity",
|
||||||
@@ -36,9 +41,7 @@ function Screen() {
|
|||||||
<div className="size-full flex items-center justify-center">
|
<div className="size-full flex items-center justify-center">
|
||||||
<div className="w-[320px] flex flex-col gap-8">
|
<div className="w-[320px] flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<h1 className="leading-tight text-xl font-semibold">
|
<h1 className="leading-tight text-xl font-semibold">New Identity</h1>
|
||||||
Import Private Key
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Frame
|
<Frame
|
||||||
@@ -86,6 +89,9 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
{isPending ? <Spinner /> : "Continue"}
|
{isPending ? <Spinner /> : "Continue"}
|
||||||
</button>
|
</button>
|
||||||
|
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||||
|
Back
|
||||||
|
</GoBack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
|
import { GoBack } from "@/components/back";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
@@ -94,6 +95,9 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
{isPending ? <Spinner /> : "Continue"}
|
{isPending ? <Spinner /> : "Continue"}
|
||||||
</button>
|
</button>
|
||||||
|
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||||
|
Back
|
||||||
|
</GoBack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
109
src/routes/index.lazy.tsx
Normal file
109
src/routes/index.lazy.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { npub } from "@/commons";
|
||||||
|
import { Frame } from "@/components/frame";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { User } from "@/components/user";
|
||||||
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { 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 [value, setValue] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const loginWith = async (npub: string) => {
|
||||||
|
setValue(npub);
|
||||||
|
startTransition(async () => {
|
||||||
|
const bunker: string = localStorage.getItem(`${npub}_bunker`);
|
||||||
|
const verifyBunker = bunker?.length && bunker?.startsWith("bunker://");
|
||||||
|
const res = await commands.login(npub, verifyBunker ? bunker : null);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({
|
||||||
|
to: "/$account/chats/new",
|
||||||
|
params: { account: res.data },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (res.error === "404") {
|
||||||
|
navigate({
|
||||||
|
to: "/$account/relays",
|
||||||
|
params: { account: npub },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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">
|
||||||
|
<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
|
||||||
|
>
|
||||||
|
{context.accounts.map((account) => (
|
||||||
|
<div
|
||||||
|
key={account}
|
||||||
|
onClick={() => loginWith(account)}
|
||||||
|
onKeyDown={() => loginWith(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">
|
||||||
|
{npub(account, 16)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<div className="inline-flex items-center justify-center size-10">
|
||||||
|
{value === account && isPending ? <Spinner /> : null}
|
||||||
|
</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">
|
||||||
|
Add an account
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</Frame>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
import { npub } from "@/commons";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { Frame } from "@/components/frame";
|
|
||||||
import { Spinner } from "@/components/spinner";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { Plus } from "@phosphor-icons/react";
|
|
||||||
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
|
|
||||||
import { useMemo, useState, useTransition } from "react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
@@ -20,94 +14,4 @@ export const Route = createFileRoute("/")({
|
|||||||
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
},
|
},
|
||||||
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 [value, setValue] = useState("");
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const loginWith = async (npub: string) => {
|
|
||||||
setValue(npub);
|
|
||||||
startTransition(async () => {
|
|
||||||
const bunker: string = localStorage.getItem(`${npub}_bunker`);
|
|
||||||
const verifyBunker = bunker?.length && bunker?.startsWith("bunker://");
|
|
||||||
const res = await commands.login(npub, verifyBunker ? bunker : null);
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
navigate({
|
|
||||||
to: "/$account/chats/new",
|
|
||||||
params: { account: res.data },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div 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">
|
|
||||||
<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
|
|
||||||
>
|
|
||||||
{context.accounts.map((account) => (
|
|
||||||
<div
|
|
||||||
key={account}
|
|
||||||
onClick={() => loginWith(account)}
|
|
||||||
onKeyDown={() => loginWith(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">
|
|
||||||
{npub(account, 16)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="inline-flex items-center justify-center size-10">
|
|
||||||
{value === account && isPending ? <Spinner /> : null}
|
|
||||||
</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">
|
|
||||||
Add an account
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</Frame>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createLazyFileRoute, Link } from "@tanstack/react-router";
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/new")({
|
export const Route = createLazyFileRoute("/new")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -16,13 +16,13 @@ function Screen() {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Link
|
<Link
|
||||||
to="/create-account"
|
to="/create-account"
|
||||||
className="w-full h-9 bg-blue-500 hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
className="w-full h-10 bg-blue-500 hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
||||||
>
|
>
|
||||||
Create a new identity
|
Create a new identity
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/nostr-connect"
|
to="/nostr-connect"
|
||||||
className="w-full h-9 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-950 dark:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
|
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-950 dark:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
|
||||||
>
|
>
|
||||||
Login with Nostr Connect
|
Login with Nostr Connect
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
|
import { GoBack } from "@/components/back";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
@@ -47,9 +48,7 @@ function Screen() {
|
|||||||
<div className="size-full flex items-center justify-center">
|
<div className="size-full flex items-center justify-center">
|
||||||
<div className="w-[320px] flex flex-col gap-8">
|
<div className="w-[320px] flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<h1 className="leading-tight text-xl font-semibold">
|
<h1 className="leading-tight text-xl font-semibold">Nostr Connect</h1>
|
||||||
Nostr Connect.
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Frame
|
<Frame
|
||||||
@@ -85,6 +84,9 @@ function Screen() {
|
|||||||
Waiting confirmation...
|
Waiting confirmation...
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||||
|
Back
|
||||||
|
</GoBack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user