diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 68a391f..6f4d7cb 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -48,9 +48,9 @@ pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result, state: State<'_, Nostr>, -) -> Result<(), String> { +) -> Result { let client = &state.client; let keys = Keys::generate(); 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; // Update metadata - let url = Url::parse(&picture).map_err(|e| e.to_string())?; - let metadata = Metadata::new().display_name(name).picture(url); + let url = match picture { + 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 { - Ok(_) => Ok(()), + Ok(_) => Ok(npub), Err(e) => Err(e.to_string()), } } @@ -150,6 +157,51 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, () Ok(list) } +#[tauri::command] +#[specta::specta] +pub async fn get_inbox(id: String, state: State<'_, Nostr>) -> Result, 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::>(); + + Ok(urls) + } else { + Ok(Vec::new()) + } + } + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn set_inbox(relays: Vec, state: State<'_, Nostr>) -> Result<(), String> { + let client = &state.client; + + let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::>(); + 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] #[specta::specta] pub async fn login( @@ -222,6 +274,8 @@ pub async fn login( let mut inbox_relays = state.inbox_relays.lock().await; inbox_relays.insert(public_key, urls); + } else { + return Err("404".into()); } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 128522b..3e57c5c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,6 +30,8 @@ fn main() { get_contact_list, get_chats, get_chat_messages, + get_inbox, + set_inbox, connect_inbox, disconnect_inbox, send_message, diff --git a/src/commands.ts b/src/commands.ts index c9c031c..e64481e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -12,7 +12,7 @@ try { else return { status: "error", error: e as any }; } }, -async createAccount(name: string, picture: string) : Promise> { +async createAccount(name: string, picture: string | null) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("create_account", { name, picture }) }; } catch (e) { @@ -71,6 +71,22 @@ try { else return { status: "error", error: e as any }; } }, +async getInbox(id: string) : Promise> { +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> { +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> { try { return { status: "ok", data: await TAURI_INVOKE("connect_inbox", { id }) }; diff --git a/src/components/back.tsx b/src/components/back.tsx new file mode 100644 index 0000000..b15c5c7 --- /dev/null +++ b/src/components/back.tsx @@ -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 ( + + ); +} diff --git a/src/routes.gen.ts b/src/routes.gen.ts index 03679bd..d8d4fa1 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -23,6 +23,7 @@ const NostrConnectLazyImport = createFileRoute('/nostr-connect')() const NewLazyImport = createFileRoute('/new')() const ImportKeyLazyImport = createFileRoute('/import-key')() const CreateAccountLazyImport = createFileRoute('/create-account')() +const AccountRelaysLazyImport = createFileRoute('/$account/relays')() const AccountChatsLazyImport = createFileRoute('/$account/chats')() const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')() @@ -53,7 +54,14 @@ const CreateAccountLazyRoute = CreateAccountLazyImport.update({ const IndexRoute = IndexImport.update({ path: '/', 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({ path: '/$account/chats', @@ -136,6 +144,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AccountChatsLazyImport parentRoute: typeof rootRoute } + '/$account/relays': { + id: '/$account/relays' + path: '/$account/relays' + fullPath: '/$account/relays' + preLoaderRoute: typeof AccountRelaysLazyImport + parentRoute: typeof rootRoute + } '/$account/chats/$id': { id: '/$account/chats/$id' path: '/$id' @@ -166,6 +181,7 @@ export const routeTree = rootRoute.addChildren({ AccountChatsIdRoute, AccountChatsNewLazyRoute, }), + AccountRelaysLazyRoute, }) /* prettier-ignore-end */ @@ -182,7 +198,8 @@ export const routeTree = rootRoute.addChildren({ "/new", "/nostr-connect", "/$account/contacts", - "/$account/chats" + "/$account/chats", + "/$account/relays" ] }, "/": { @@ -210,6 +227,9 @@ export const routeTree = rootRoute.addChildren({ "/$account/chats/new" ] }, + "/$account/relays": { + "filePath": "$account.relays.lazy.tsx" + }, "/$account/chats/$id": { "filePath": "$account.chats.$id.tsx", "parent": "/$account/chats" diff --git a/src/routes/$account.chats.lazy.tsx b/src/routes/$account.chats.lazy.tsx index cf4d07a..7524331 100644 --- a/src/routes/$account.chats.lazy.tsx +++ b/src/routes/$account.chats.lazy.tsx @@ -2,7 +2,13 @@ import { commands } from "@/commands"; import { ago, cn } from "@/commons"; import { Spinner } from "@/components/spinner"; 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 ScrollArea from "@radix-ui/react-scroll-area"; import { useQuery } from "@tanstack/react-query"; @@ -48,7 +54,7 @@ function Header() { data-tauri-drag-region className={cn( "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", )} > @@ -153,6 +159,12 @@ function ChatList() { ))} + ) : !data?.length ? ( +
+
+ No chats. +
+
) : ( data.map((item) => ( 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" > - + + ); } diff --git a/src/routes/$account.chats.new.lazy.tsx b/src/routes/$account.chats.new.lazy.tsx index 066bbd1..7e7f8d3 100644 --- a/src/routes/$account.chats.new.lazy.tsx +++ b/src/routes/$account.chats.new.lazy.tsx @@ -1,5 +1,5 @@ -import { createLazyFileRoute } from "@tanstack/react-router"; import { CoopIcon } from "@/icons/coop"; +import { createLazyFileRoute } from "@tanstack/react-router"; export const Route = createLazyFileRoute("/$account/chats/new")({ component: Screen, @@ -7,8 +7,14 @@ export const Route = createLazyFileRoute("/$account/chats/new")({ function Screen() { return ( -
+
+

+ coop on nostr. +

); } diff --git a/src/routes/$account.relays.lazy.tsx b/src/routes/$account.relays.lazy.tsx new file mode 100644 index 0000000..9c282c2 --- /dev/null +++ b/src/routes/$account.relays.lazy.tsx @@ -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([]); + 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 ( +
+
+
+

Inbox Relays

+

+ Inbox Relay is used to receive message from others +

+
+
+ +
+

+ You need to set at least 1 inbox relay in order to receive + message from others. +

+

+ If you don't know which relay to add, you can use{" "} + setNewRelay("wss://auth.nostr1.com")} + onKeyDown={() => setNewRelay("wss://auth.nostr1.com")} + className="font-semibold" + > + auth.nostr1.com + +

+
+
+ 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" + /> + +
+
+ {relays.map((relay) => ( +
+
{relay}
+
+ +
+
+ ))} +
+ +
+ +
+
+
+
+ ); +} diff --git a/src/routes/create-account.lazy.tsx b/src/routes/create-account.lazy.tsx index 7a9af5c..6222531 100644 --- a/src/routes/create-account.lazy.tsx +++ b/src/routes/create-account.lazy.tsx @@ -1,4 +1,5 @@ import { commands } from "@/commands"; +import { GoBack } from "@/components/back"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; import { createLazyFileRoute } from "@tanstack/react-router"; @@ -12,7 +13,7 @@ export const Route = createLazyFileRoute("/create-account")({ function Screen() { const navigate = Route.useNavigate(); - const [picture, setPicture] = useState(""); + const [picture, setPicture] = useState(null); const [name, setName] = useState(""); const [isPending, startTransition] = useTransition(); @@ -21,7 +22,11 @@ function Screen() { const res = await commands.createAccount(name, picture); if (res.status === "ok") { - navigate({ to: "/", replace: true }); + navigate({ + to: "/$account/relays", + params: { account: res.data }, + replace: true, + }); } else { await message(res.error, { title: "New Identity", @@ -36,9 +41,7 @@ function Screen() {
-

- Import Private Key -

+

New Identity

{isPending ? : "Continue"} + + Back +
diff --git a/src/routes/import-key.lazy.tsx b/src/routes/import-key.lazy.tsx index dfafd97..86260d2 100644 --- a/src/routes/import-key.lazy.tsx +++ b/src/routes/import-key.lazy.tsx @@ -1,7 +1,8 @@ import { commands } from "@/commands"; +import { GoBack } from "@/components/back"; import { Frame } from "@/components/frame"; 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 { useState, useTransition } from "react"; @@ -94,6 +95,9 @@ function Screen() { > {isPending ? : "Continue"} + + Back +
diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx new file mode 100644 index 0000000..643cbd2 --- /dev/null +++ b/src/routes/index.lazy.tsx @@ -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 ( +
+
+
+

+ {currentDate} +

+

Welcome back!

+
+ + {context.accounts.map((account) => ( +
loginWith(account)} + onKeyDown={() => loginWith(account)} + className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5" + > + + + +
+ + + {npub(account, 16)} + +
+
+
+
+ {value === account && isPending ? : null} +
+
+ ))} + +
+
+ +
+ + Add an account + +
+ + +
+
+ ); +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d4b563d..f258624 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,11 +1,5 @@ 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, createFileRoute, redirect } from "@tanstack/react-router"; -import { useMemo, useState, useTransition } from "react"; +import { createFileRoute, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ beforeLoad: async () => { @@ -20,94 +14,4 @@ export const Route = createFileRoute("/")({ 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 ( -
-
-
-

- {currentDate} -

-

Welcome back!

-
- - {context.accounts.map((account) => ( -
loginWith(account)} - onKeyDown={() => loginWith(account)} - className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5" - > - - - -
- - - {npub(account, 16)} - -
-
-
-
- {value === account && isPending ? : null} -
-
- ))} - -
-
- -
- - Add an account - -
- - -
-
- ); -} diff --git a/src/routes/new.lazy.tsx b/src/routes/new.lazy.tsx index d053a77..b5473f7 100644 --- a/src/routes/new.lazy.tsx +++ b/src/routes/new.lazy.tsx @@ -1,4 +1,4 @@ -import { createLazyFileRoute, Link } from "@tanstack/react-router"; +import { Link, createLazyFileRoute } from "@tanstack/react-router"; export const Route = createLazyFileRoute("/new")({ component: Screen, @@ -16,13 +16,13 @@ function Screen() {
Create a new identity Login with Nostr Connect diff --git a/src/routes/nostr-connect.lazy.tsx b/src/routes/nostr-connect.lazy.tsx index 487e704..e30646b 100644 --- a/src/routes/nostr-connect.lazy.tsx +++ b/src/routes/nostr-connect.lazy.tsx @@ -1,4 +1,5 @@ import { commands } from "@/commands"; +import { GoBack } from "@/components/back"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; import { createLazyFileRoute } from "@tanstack/react-router"; @@ -47,9 +48,7 @@ function Screen() {
-

- Nostr Connect. -

+

Nostr Connect

) : null} + + Back +