From 0ff3c7c76f5847753e719e98b24a071bfadacfd0 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 18 Sep 2024 13:19:28 +0700 Subject: [PATCH] feat: ensure user have inbox relays --- src-tauri/src/commands/relay.rs | 23 +- src-tauri/src/main.rs | 3 +- src/commands.ts | 12 +- src/routes.gen.ts | 291 ++++++---- src/routes/$account.chats.$id.lazy.tsx | 377 ------------- src/routes/$account.chats.$id.tsx | 34 -- src/routes/$account.chats.lazy.tsx | 505 ------------------ src/routes/$account.chats.new.lazy.tsx | 20 - src/routes/$account.contacts.lazy.tsx | 63 --- src/routes/$account.contacts.tsx | 14 - src/routes/$account.relays.tsx | 14 - src/routes/$account/_layout.tsx | 16 + .../$account/_layout/chats.$id.lazy.tsx | 378 +++++++++++++ src/routes/$account/_layout/chats.$id.tsx | 34 ++ src/routes/$account/_layout/chats.lazy.tsx | 505 ++++++++++++++++++ .../$account/_layout/chats.new.lazy.tsx | 20 + src/routes/$account/_layout/contacts.lazy.tsx | 63 +++ src/routes/$account/_layout/contacts.tsx | 14 + ....relays.lazy.tsx => inbox-relays.lazy.tsx} | 67 ++- src/routes/inbox-relays.tsx | 15 + src/routes/index.tsx | 2 +- 21 files changed, 1315 insertions(+), 1155 deletions(-) delete mode 100644 src/routes/$account.chats.$id.lazy.tsx delete mode 100644 src/routes/$account.chats.$id.tsx delete mode 100644 src/routes/$account.chats.lazy.tsx delete mode 100644 src/routes/$account.chats.new.lazy.tsx delete mode 100644 src/routes/$account.contacts.lazy.tsx delete mode 100644 src/routes/$account.contacts.tsx delete mode 100644 src/routes/$account.relays.tsx create mode 100644 src/routes/$account/_layout.tsx create mode 100644 src/routes/$account/_layout/chats.$id.lazy.tsx create mode 100644 src/routes/$account/_layout/chats.$id.tsx create mode 100644 src/routes/$account/_layout/chats.lazy.tsx create mode 100644 src/routes/$account/_layout/chats.new.lazy.tsx create mode 100644 src/routes/$account/_layout/contacts.lazy.tsx create mode 100644 src/routes/$account/_layout/contacts.tsx rename src/routes/{$account.relays.lazy.tsx => inbox-relays.lazy.tsx} (78%) create mode 100644 src/routes/inbox-relays.tsx diff --git a/src-tauri/src/commands/relay.rs b/src-tauri/src/commands/relay.rs index 97cb269..7979212 100644 --- a/src-tauri/src/commands/relay.rs +++ b/src-tauri/src/commands/relay.rs @@ -36,7 +36,7 @@ pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), #[tauri::command] #[specta::specta] -pub async fn collect_inbox_relays( +pub async fn get_inbox_relays( user_id: String, state: State<'_, Nostr>, ) -> Result, String> { @@ -69,6 +69,27 @@ pub async fn collect_inbox_relays( } } +#[tauri::command] +#[specta::specta] +pub async fn ensure_inbox_relays( + user_id: String, + state: State<'_, Nostr>, +) -> Result, String> { + let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?; + let relays = state.inbox_relays.lock().await; + + match relays.get(&public_key) { + Some(relays) => { + if relays.is_empty() { + Err("404".into()) + } else { + Ok(relays.to_owned()) + } + } + None => Err("404".into()), + } +} + #[tauri::command] #[specta::specta] pub async fn set_inbox_relays(relays: Vec, state: State<'_, Nostr>) -> Result<(), String> { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5d75a54..3564774 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -35,8 +35,9 @@ fn main() { let builder = Builder::::new().commands(collect_commands![ get_bootstrap_relays, set_bootstrap_relays, - collect_inbox_relays, + get_inbox_relays, set_inbox_relays, + ensure_inbox_relays, connect_inbox_relays, disconnect_inbox_relays, login, diff --git a/src/commands.ts b/src/commands.ts index 8e0ebaf..7388938 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -21,9 +21,9 @@ async setBootstrapRelays(relays: string) : Promise> { else return { status: "error", error: e as any }; } }, -async collectInboxRelays(userId: string) : Promise> { +async getInboxRelays(userId: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("collect_inbox_relays", { userId }) }; + return { status: "ok", data: await TAURI_INVOKE("get_inbox_relays", { userId }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -37,6 +37,14 @@ async setInboxRelays(relays: string[]) : Promise> { else return { status: "error", error: e as any }; } }, +async ensureInboxRelays(userId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("ensure_inbox_relays", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async connectInboxRelays(userId: string, ignoreCache: boolean) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("connect_inbox_relays", { userId, ignoreCache }) }; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index bc6368b..968cd02 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -13,24 +13,35 @@ import { createFileRoute } from '@tanstack/react-router' // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as InboxRelaysImport } from './routes/inbox-relays' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' import { Route as IndexImport } from './routes/index' import { Route as AuthNewImport } from './routes/auth/new' import { Route as AuthImportImport } from './routes/auth/import' import { Route as AuthConnectImport } from './routes/auth/connect' -import { Route as AccountRelaysImport } from './routes/$account.relays' -import { Route as AccountContactsImport } from './routes/$account.contacts' -import { Route as AccountChatsIdImport } from './routes/$account.chats.$id' +import { Route as AccountLayoutImport } from './routes/$account/_layout' +import { Route as AccountLayoutContactsImport } from './routes/$account/_layout/contacts' +import { Route as AccountLayoutChatsIdImport } from './routes/$account/_layout/chats.$id' // Create Virtual Routes +const AccountImport = createFileRoute('/$account')() const ResetLazyImport = createFileRoute('/reset')() const NewLazyImport = createFileRoute('/new')() -const AccountChatsLazyImport = createFileRoute('/$account/chats')() -const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')() +const AccountLayoutChatsLazyImport = createFileRoute( + '/$account/_layout/chats', +)() +const AccountLayoutChatsNewLazyImport = createFileRoute( + '/$account/_layout/chats/new', +)() // Create/Update Routes +const AccountRoute = AccountImport.update({ + path: '/$account', + getParentRoute: () => rootRoute, +} as any) + const ResetLazyRoute = ResetLazyImport.update({ path: '/reset', getParentRoute: () => rootRoute, @@ -41,6 +52,11 @@ const NewLazyRoute = NewLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) +const InboxRelaysRoute = InboxRelaysImport.update({ + path: '/inbox-relays', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/inbox-relays.lazy').then((d) => d.Route)) + const BootstrapRelaysRoute = BootstrapRelaysImport.update({ path: '/bootstrap-relays', getParentRoute: () => rootRoute, @@ -53,13 +69,6 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) -const AccountChatsLazyRoute = AccountChatsLazyImport.update({ - path: '/$account/chats', - getParentRoute: () => rootRoute, -} as any).lazy(() => - import('./routes/$account.chats.lazy').then((d) => d.Route), -) - const AuthNewRoute = AuthNewImport.update({ path: '/auth/new', getParentRoute: () => rootRoute, @@ -75,32 +84,37 @@ const AuthConnectRoute = AuthConnectImport.update({ getParentRoute: () => rootRoute, } as any) -const AccountRelaysRoute = AccountRelaysImport.update({ - path: '/$account/relays', - getParentRoute: () => rootRoute, +const AccountLayoutRoute = AccountLayoutImport.update({ + id: '/_layout', + getParentRoute: () => AccountRoute, +} as any) + +const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({ + path: '/chats', + getParentRoute: () => AccountLayoutRoute, } as any).lazy(() => - import('./routes/$account.relays.lazy').then((d) => d.Route), + import('./routes/$account/_layout/chats.lazy').then((d) => d.Route), ) -const AccountContactsRoute = AccountContactsImport.update({ - path: '/$account/contacts', - getParentRoute: () => rootRoute, +const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({ + path: '/contacts', + getParentRoute: () => AccountLayoutRoute, } as any).lazy(() => - import('./routes/$account.contacts.lazy').then((d) => d.Route), + import('./routes/$account/_layout/contacts.lazy').then((d) => d.Route), ) -const AccountChatsNewLazyRoute = AccountChatsNewLazyImport.update({ +const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({ path: '/new', - getParentRoute: () => AccountChatsLazyRoute, + getParentRoute: () => AccountLayoutChatsLazyRoute, } as any).lazy(() => - import('./routes/$account.chats.new.lazy').then((d) => d.Route), + import('./routes/$account/_layout/chats.new.lazy').then((d) => d.Route), ) -const AccountChatsIdRoute = AccountChatsIdImport.update({ +const AccountLayoutChatsIdRoute = AccountLayoutChatsIdImport.update({ path: '/$id', - getParentRoute: () => AccountChatsLazyRoute, + getParentRoute: () => AccountLayoutChatsLazyRoute, } as any).lazy(() => - import('./routes/$account.chats.$id.lazy').then((d) => d.Route), + import('./routes/$account/_layout/chats.$id.lazy').then((d) => d.Route), ) // Populate the FileRoutesByPath interface @@ -121,6 +135,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof BootstrapRelaysImport parentRoute: typeof rootRoute } + '/inbox-relays': { + id: '/inbox-relays' + path: '/inbox-relays' + fullPath: '/inbox-relays' + preLoaderRoute: typeof InboxRelaysImport + parentRoute: typeof rootRoute + } '/new': { id: '/new' path: '/new' @@ -135,19 +156,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResetLazyImport parentRoute: typeof rootRoute } - '/$account/contacts': { - id: '/$account/contacts' - path: '/$account/contacts' - fullPath: '/$account/contacts' - preLoaderRoute: typeof AccountContactsImport + '/$account': { + id: '/$account' + path: '/$account' + fullPath: '/$account' + preLoaderRoute: typeof AccountImport parentRoute: typeof rootRoute } - '/$account/relays': { - id: '/$account/relays' - path: '/$account/relays' - fullPath: '/$account/relays' - preLoaderRoute: typeof AccountRelaysImport - parentRoute: typeof rootRoute + '/$account/_layout': { + id: '/$account/_layout' + path: '/$account' + fullPath: '/$account' + preLoaderRoute: typeof AccountLayoutImport + parentRoute: typeof AccountRoute } '/auth/connect': { id: '/auth/connect' @@ -170,89 +191,128 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthNewImport parentRoute: typeof rootRoute } - '/$account/chats': { - id: '/$account/chats' - path: '/$account/chats' - fullPath: '/$account/chats' - preLoaderRoute: typeof AccountChatsLazyImport - parentRoute: typeof rootRoute + '/$account/_layout/contacts': { + id: '/$account/_layout/contacts' + path: '/contacts' + fullPath: '/$account/contacts' + preLoaderRoute: typeof AccountLayoutContactsImport + parentRoute: typeof AccountLayoutImport } - '/$account/chats/$id': { - id: '/$account/chats/$id' + '/$account/_layout/chats': { + id: '/$account/_layout/chats' + path: '/chats' + fullPath: '/$account/chats' + preLoaderRoute: typeof AccountLayoutChatsLazyImport + parentRoute: typeof AccountLayoutImport + } + '/$account/_layout/chats/$id': { + id: '/$account/_layout/chats/$id' path: '/$id' fullPath: '/$account/chats/$id' - preLoaderRoute: typeof AccountChatsIdImport - parentRoute: typeof AccountChatsLazyImport + preLoaderRoute: typeof AccountLayoutChatsIdImport + parentRoute: typeof AccountLayoutChatsLazyImport } - '/$account/chats/new': { - id: '/$account/chats/new' + '/$account/_layout/chats/new': { + id: '/$account/_layout/chats/new' path: '/new' fullPath: '/$account/chats/new' - preLoaderRoute: typeof AccountChatsNewLazyImport - parentRoute: typeof AccountChatsLazyImport + preLoaderRoute: typeof AccountLayoutChatsNewLazyImport + parentRoute: typeof AccountLayoutChatsLazyImport } } } // Create and export the route tree -interface AccountChatsLazyRouteChildren { - AccountChatsIdRoute: typeof AccountChatsIdRoute - AccountChatsNewLazyRoute: typeof AccountChatsNewLazyRoute +interface AccountLayoutChatsLazyRouteChildren { + AccountLayoutChatsIdRoute: typeof AccountLayoutChatsIdRoute + AccountLayoutChatsNewLazyRoute: typeof AccountLayoutChatsNewLazyRoute } -const AccountChatsLazyRouteChildren: AccountChatsLazyRouteChildren = { - AccountChatsIdRoute: AccountChatsIdRoute, - AccountChatsNewLazyRoute: AccountChatsNewLazyRoute, +const AccountLayoutChatsLazyRouteChildren: AccountLayoutChatsLazyRouteChildren = + { + AccountLayoutChatsIdRoute: AccountLayoutChatsIdRoute, + AccountLayoutChatsNewLazyRoute: AccountLayoutChatsNewLazyRoute, + } + +const AccountLayoutChatsLazyRouteWithChildren = + AccountLayoutChatsLazyRoute._addFileChildren( + AccountLayoutChatsLazyRouteChildren, + ) + +interface AccountLayoutRouteChildren { + AccountLayoutContactsRoute: typeof AccountLayoutContactsRoute + AccountLayoutChatsLazyRoute: typeof AccountLayoutChatsLazyRouteWithChildren } -const AccountChatsLazyRouteWithChildren = - AccountChatsLazyRoute._addFileChildren(AccountChatsLazyRouteChildren) +const AccountLayoutRouteChildren: AccountLayoutRouteChildren = { + AccountLayoutContactsRoute: AccountLayoutContactsRoute, + AccountLayoutChatsLazyRoute: AccountLayoutChatsLazyRouteWithChildren, +} + +const AccountLayoutRouteWithChildren = AccountLayoutRoute._addFileChildren( + AccountLayoutRouteChildren, +) + +interface AccountRouteChildren { + AccountLayoutRoute: typeof AccountLayoutRouteWithChildren +} + +const AccountRouteChildren: AccountRouteChildren = { + AccountLayoutRoute: AccountLayoutRouteWithChildren, +} + +const AccountRouteWithChildren = + AccountRoute._addFileChildren(AccountRouteChildren) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/bootstrap-relays': typeof BootstrapRelaysRoute + '/inbox-relays': typeof InboxRelaysRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute - '/$account/contacts': typeof AccountContactsRoute - '/$account/relays': typeof AccountRelaysRoute + '/$account': typeof AccountLayoutRouteWithChildren '/auth/connect': typeof AuthConnectRoute '/auth/import': typeof AuthImportRoute '/auth/new': typeof AuthNewRoute - '/$account/chats': typeof AccountChatsLazyRouteWithChildren - '/$account/chats/$id': typeof AccountChatsIdRoute - '/$account/chats/new': typeof AccountChatsNewLazyRoute + '/$account/contacts': typeof AccountLayoutContactsRoute + '/$account/chats': typeof AccountLayoutChatsLazyRouteWithChildren + '/$account/chats/$id': typeof AccountLayoutChatsIdRoute + '/$account/chats/new': typeof AccountLayoutChatsNewLazyRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/bootstrap-relays': typeof BootstrapRelaysRoute + '/inbox-relays': typeof InboxRelaysRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute - '/$account/contacts': typeof AccountContactsRoute - '/$account/relays': typeof AccountRelaysRoute + '/$account': typeof AccountLayoutRouteWithChildren '/auth/connect': typeof AuthConnectRoute '/auth/import': typeof AuthImportRoute '/auth/new': typeof AuthNewRoute - '/$account/chats': typeof AccountChatsLazyRouteWithChildren - '/$account/chats/$id': typeof AccountChatsIdRoute - '/$account/chats/new': typeof AccountChatsNewLazyRoute + '/$account/contacts': typeof AccountLayoutContactsRoute + '/$account/chats': typeof AccountLayoutChatsLazyRouteWithChildren + '/$account/chats/$id': typeof AccountLayoutChatsIdRoute + '/$account/chats/new': typeof AccountLayoutChatsNewLazyRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/bootstrap-relays': typeof BootstrapRelaysRoute + '/inbox-relays': typeof InboxRelaysRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute - '/$account/contacts': typeof AccountContactsRoute - '/$account/relays': typeof AccountRelaysRoute + '/$account': typeof AccountRouteWithChildren + '/$account/_layout': typeof AccountLayoutRouteWithChildren '/auth/connect': typeof AuthConnectRoute '/auth/import': typeof AuthImportRoute '/auth/new': typeof AuthNewRoute - '/$account/chats': typeof AccountChatsLazyRouteWithChildren - '/$account/chats/$id': typeof AccountChatsIdRoute - '/$account/chats/new': typeof AccountChatsNewLazyRoute + '/$account/_layout/contacts': typeof AccountLayoutContactsRoute + '/$account/_layout/chats': typeof AccountLayoutChatsLazyRouteWithChildren + '/$account/_layout/chats/$id': typeof AccountLayoutChatsIdRoute + '/$account/_layout/chats/new': typeof AccountLayoutChatsNewLazyRoute } export interface FileRouteTypes { @@ -260,13 +320,14 @@ export interface FileRouteTypes { fullPaths: | '/' | '/bootstrap-relays' + | '/inbox-relays' | '/new' | '/reset' - | '/$account/contacts' - | '/$account/relays' + | '/$account' | '/auth/connect' | '/auth/import' | '/auth/new' + | '/$account/contacts' | '/$account/chats' | '/$account/chats/$id' | '/$account/chats/new' @@ -274,13 +335,14 @@ export interface FileRouteTypes { to: | '/' | '/bootstrap-relays' + | '/inbox-relays' | '/new' | '/reset' - | '/$account/contacts' - | '/$account/relays' + | '/$account' | '/auth/connect' | '/auth/import' | '/auth/new' + | '/$account/contacts' | '/$account/chats' | '/$account/chats/$id' | '/$account/chats/new' @@ -288,43 +350,43 @@ export interface FileRouteTypes { | '__root__' | '/' | '/bootstrap-relays' + | '/inbox-relays' | '/new' | '/reset' - | '/$account/contacts' - | '/$account/relays' + | '/$account' + | '/$account/_layout' | '/auth/connect' | '/auth/import' | '/auth/new' - | '/$account/chats' - | '/$account/chats/$id' - | '/$account/chats/new' + | '/$account/_layout/contacts' + | '/$account/_layout/chats' + | '/$account/_layout/chats/$id' + | '/$account/_layout/chats/new' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute BootstrapRelaysRoute: typeof BootstrapRelaysRoute + InboxRelaysRoute: typeof InboxRelaysRoute NewLazyRoute: typeof NewLazyRoute ResetLazyRoute: typeof ResetLazyRoute - AccountContactsRoute: typeof AccountContactsRoute - AccountRelaysRoute: typeof AccountRelaysRoute + AccountRoute: typeof AccountRouteWithChildren AuthConnectRoute: typeof AuthConnectRoute AuthImportRoute: typeof AuthImportRoute AuthNewRoute: typeof AuthNewRoute - AccountChatsLazyRoute: typeof AccountChatsLazyRouteWithChildren } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, BootstrapRelaysRoute: BootstrapRelaysRoute, + InboxRelaysRoute: InboxRelaysRoute, NewLazyRoute: NewLazyRoute, ResetLazyRoute: ResetLazyRoute, - AccountContactsRoute: AccountContactsRoute, - AccountRelaysRoute: AccountRelaysRoute, + AccountRoute: AccountRouteWithChildren, AuthConnectRoute: AuthConnectRoute, AuthImportRoute: AuthImportRoute, AuthNewRoute: AuthNewRoute, - AccountChatsLazyRoute: AccountChatsLazyRouteWithChildren, } export const routeTree = rootRoute @@ -341,14 +403,13 @@ export const routeTree = rootRoute "children": [ "/", "/bootstrap-relays", + "/inbox-relays", "/new", "/reset", - "/$account/contacts", - "/$account/relays", + "/$account", "/auth/connect", "/auth/import", - "/auth/new", - "/$account/chats" + "/auth/new" ] }, "/": { @@ -357,17 +418,28 @@ export const routeTree = rootRoute "/bootstrap-relays": { "filePath": "bootstrap-relays.tsx" }, + "/inbox-relays": { + "filePath": "inbox-relays.tsx" + }, "/new": { "filePath": "new.lazy.tsx" }, "/reset": { "filePath": "reset.lazy.tsx" }, - "/$account/contacts": { - "filePath": "$account.contacts.tsx" + "/$account": { + "filePath": "$account", + "children": [ + "/$account/_layout" + ] }, - "/$account/relays": { - "filePath": "$account.relays.tsx" + "/$account/_layout": { + "filePath": "$account/_layout.tsx", + "parent": "/$account", + "children": [ + "/$account/_layout/contacts", + "/$account/_layout/chats" + ] }, "/auth/connect": { "filePath": "auth/connect.tsx" @@ -378,20 +450,25 @@ export const routeTree = rootRoute "/auth/new": { "filePath": "auth/new.tsx" }, - "/$account/chats": { - "filePath": "$account.chats.lazy.tsx", + "/$account/_layout/contacts": { + "filePath": "$account/_layout/contacts.tsx", + "parent": "/$account/_layout" + }, + "/$account/_layout/chats": { + "filePath": "$account/_layout/chats.lazy.tsx", + "parent": "/$account/_layout", "children": [ - "/$account/chats/$id", - "/$account/chats/new" + "/$account/_layout/chats/$id", + "/$account/_layout/chats/new" ] }, - "/$account/chats/$id": { - "filePath": "$account.chats.$id.tsx", - "parent": "/$account/chats" + "/$account/_layout/chats/$id": { + "filePath": "$account/_layout/chats.$id.tsx", + "parent": "/$account/_layout/chats" }, - "/$account/chats/new": { - "filePath": "$account.chats.new.lazy.tsx", - "parent": "/$account/chats" + "/$account/_layout/chats/new": { + "filePath": "$account/_layout/chats.new.lazy.tsx", + "parent": "/$account/_layout/chats" } } } diff --git a/src/routes/$account.chats.$id.lazy.tsx b/src/routes/$account.chats.$id.lazy.tsx deleted file mode 100644 index 595f832..0000000 --- a/src/routes/$account.chats.$id.lazy.tsx +++ /dev/null @@ -1,377 +0,0 @@ -import { commands } from "@/commands"; -import { cn, getReceivers, groupEventByDate, time, upload } from "@/commons"; -import { Spinner } from "@/components/spinner"; -import { User } from "@/components/user"; -import { CoopIcon } from "@/icons/coop"; -import { ArrowUp, Paperclip, X } from "@phosphor-icons/react"; -import * as ScrollArea from "@radix-ui/react-scroll-area"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { listen } from "@tauri-apps/api/event"; -import { message } from "@tauri-apps/plugin-dialog"; -import type { NostrEvent } from "nostr-tools"; -import { - type Dispatch, - type SetStateAction, - useCallback, - useRef, - useState, - useTransition, -} from "react"; -import { useEffect } from "react"; -import { Virtualizer, type VirtualizerHandle } from "virtua"; - -type EventPayload = { - event: string; - sender: string; -}; - -export const Route = createLazyFileRoute("/$account/chats/$id")({ - component: Screen, -}); - -function Screen() { - return ( -
-
- -
-
- ); -} - -function Header() { - const { account, id } = Route.useParams(); - const { platform } = Route.useRouteContext(); - - return ( -
-
-
- - - - - - - - - - -
-
-
-
- - - - -
Connected
-
-
-
- ); -} - -function List() { - const { account, id } = Route.useParams(); - const { isLoading, isError, data } = useQuery({ - queryKey: ["chats", id], - queryFn: async () => { - const res = await commands.getChatMessages(id); - - if (res.status === "ok") { - const raw = res.data; - const events: NostrEvent[] = raw.map((item) => JSON.parse(item)); - - return events; - } else { - throw new Error(res.error); - } - }, - select: (data) => { - const groups = groupEventByDate(data); - return Object.entries(groups).reverse(); - }, - refetchOnWindowFocus: false, - }); - - const queryClient = useQueryClient(); - const scrollRef = useRef(null); - const ref = useRef(null); - const shouldStickToBottom = useRef(true); - - const renderItem = useCallback( - (item: NostrEvent, idx: number) => { - const self = account === item.pubkey; - - return ( -
-
-
- {item.content} -
-
-
- - {time(item.created_at)} - -
-
- ); - }, - [data], - ); - - useEffect(() => { - const unlisten = listen("event", async (data) => { - const event: NostrEvent = JSON.parse(data.payload.event); - const sender = data.payload.sender; - const receivers = getReceivers(event.tags); - const group = [account, id]; - - if (!group.includes(sender)) return; - if (!group.some((item) => receivers.includes(item))) return; - - await queryClient.setQueryData( - ["chats", id], - (prevEvents: NostrEvent[]) => { - if (!prevEvents) return [event]; - return [event, ...prevEvents]; - }, - ); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, [account, id]); - - useEffect(() => { - if (!data?.length) return; - if (!ref.current) return; - if (!shouldStickToBottom.current) return; - - ref.current.scrollToIndex(data.length - 1, { - align: "end", - }); - }, [data]); - - return ( - - - { - if (!ref.current) return; - shouldStickToBottom.current = - offset - ref.current.scrollSize + ref.current.viewportSize >= - -1.5; - }} - > - {isLoading ? ( - <> -
-
-
-
-
-
-
-
-
-
- - ) : isError ? ( -
-
- Cannot load message. Please try again later. -
-
- ) : !data.length ? ( -
- -
- ) : ( - data.map((item) => ( -
-
- {item[0]} -
-
- {item[1] - .sort((a, b) => a.created_at - b.created_at) - .map((item, idx) => renderItem(item, idx))} -
-
- )) - )} - - - - - - - - ); -} - -function Form() { - const { id } = Route.useParams(); - const inboxRelays = Route.useLoaderData(); - - const [newMessage, setNewMessage] = useState(""); - const [attaches, setAttaches] = useState([]); - const [isPending, startTransition] = useTransition(); - - const remove = (item: string) => { - setAttaches((prev) => prev.filter((att) => att !== item)); - }; - - const submit = () => { - startTransition(async () => { - if (!newMessage.length) return; - - const content = `${newMessage}\r\n${attaches.join("\r\n")}`; - const res = await commands.sendMessage(id, content); - - if (res.status === "error") { - await message(res.error, { - title: "Send mesaage failed", - kind: "error", - }); - return; - } - - setNewMessage(""); - }); - }; - - return ( -
- {!inboxRelays.length ? ( -
- This user doesn't have inbox relays. You cannot send messages to them. -
- ) : ( -
- {attaches?.length ? ( -
- {attaches.map((item, index) => ( - - ))} -
- ) : null} -
-
- -
- setNewMessage(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") submit(); - }} - className="flex-1 h-9 rounded-full px-3.5 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:outline-none focus:border-blue-500 placeholder:text-neutral-400 dark:placeholder:text-neutral-600" - /> - -
-
- )} -
- ); -} - -function AttachMedia({ - onUpload, -}: { onUpload: Dispatch> }) { - const [isPending, startTransition] = useTransition(); - - const attach = () => { - startTransition(async () => { - const file = await upload(); - - if (file) { - onUpload((prev) => [...prev, file]); - } else { - return; - } - }); - }; - - return ( - - ); -} diff --git a/src/routes/$account.chats.$id.tsx b/src/routes/$account.chats.$id.tsx deleted file mode 100644 index db71fbe..0000000 --- a/src/routes/$account.chats.$id.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { commands } from "@/commands"; -import { Spinner } from "@/components/spinner"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/chats/$id")({ - loader: async ({ params, context }) => { - const res = await commands.connectInboxRelays(params.id, false); - - if (res.status === "ok") { - // Add id to chat manager to unsubscribe later. - context.chatManager.set(params.id, params.id); - - return res.data; - } else { - return []; - } - }, - pendingComponent: Pending, - pendingMs: 200, - pendingMinMs: 100, -}); - -function Pending() { - return ( -
-
- - - Connection in progress. Please wait ... - -
-
- ); -} diff --git a/src/routes/$account.chats.lazy.tsx b/src/routes/$account.chats.lazy.tsx deleted file mode 100644 index 0938909..0000000 --- a/src/routes/$account.chats.lazy.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import { commands } from "@/commands"; -import { ago, cn } from "@/commons"; -import { Spinner } from "@/components/spinner"; -import { User } from "@/components/user"; -import { - ArrowRight, - CaretDown, - CirclesFour, - Plus, - X, -} from "@phosphor-icons/react"; -import * as Dialog from "@radix-ui/react-dialog"; -import * as Progress from "@radix-ui/react-progress"; -import * as ScrollArea from "@radix-ui/react-scroll-area"; -import { useQuery } from "@tanstack/react-query"; -import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; -import { listen } from "@tauri-apps/api/event"; -import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; -import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { message } from "@tauri-apps/plugin-dialog"; -import { open } from "@tauri-apps/plugin-shell"; -import { type NostrEvent, nip19 } from "nostr-tools"; -import { useCallback, useEffect, useRef, useState, useTransition } from "react"; -import { Virtualizer } from "virtua"; - -type EventPayload = { - event: string; - sender: string; -}; - -export const Route = createLazyFileRoute("/$account/chats")({ - component: Screen, -}); - -function Screen() { - return ( -
-
-
- -
-
- -
-
- ); -} - -function Header() { - const { platform } = Route.useRouteContext(); - const { account } = Route.useParams(); - - return ( -
- -
- - - - -
-
- ); -} - -function ChatList() { - const { account } = Route.useParams(); - const { queryClient } = Route.useRouteContext(); - const { isLoading, data } = useQuery({ - queryKey: ["chats"], - queryFn: async () => { - const res = await commands.getChats(); - - if (res.status === "ok") { - const raw = res.data; - const events = raw.map((item) => JSON.parse(item) as NostrEvent); - - return events; - } else { - throw new Error(res.error); - } - }, - select: (data) => data.sort((a, b) => b.created_at - a.created_at), - refetchOnMount: false, - refetchOnWindowFocus: false, - }); - - const [isSync, setIsSync] = useState(false); - const [progress, setProgress] = useState(0); - - useEffect(() => { - const timer = setInterval( - () => setProgress((prev) => (prev <= 100 ? prev + 4 : 100)), - 1200, - ); - return () => clearInterval(timer); - }, []); - - useEffect(() => { - const unlisten = listen("synchronized", async () => { - await queryClient.refetchQueries({ queryKey: ["chats"] }); - setIsSync(true); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, []); - - useEffect(() => { - const unlisten = listen("event", async (data) => { - const event: NostrEvent = JSON.parse(data.payload.event); - const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]); - - if (chats) { - const index = chats.findIndex((item) => item.pubkey === event.pubkey); - - if (index === -1) { - await queryClient.setQueryData( - ["chats"], - (prevEvents: NostrEvent[]) => { - if (!prevEvents) return prevEvents; - if (event.pubkey === account) return; - - return [event, ...prevEvents]; - }, - ); - } else { - const newEvents = [...chats]; - newEvents[index] = { - ...event, - }; - - await queryClient.setQueryData(["chats"], newEvents); - } - } - }); - - return () => { - unlisten.then((f) => f()); - }; - }, []); - - return ( - - - {isLoading ? ( - <> - {[...Array(5).keys()].map((i) => ( -
-
-
-
- ))} - - ) : isSync && !data.length ? ( -
-
- No chats. -
-
- ) : ( - data.map((item) => ( - - {({ isActive, isTransitioning }) => ( - - - -
-
- - - {account === item.pubkey ? "(you)" : ""} - -
- {isTransitioning ? ( - - ) : ( - - {ago(item.created_at)} - - )} -
-
-
- )} - - )) - )} - - {!isSync ? : null} - - - - - - ); -} - -function SyncPopup({ progress }: { progress: number }) { - return ( -
-
- - - - Syncing message... -
-
- ); -} - -function Compose() { - const [isOpen, setIsOpen] = useState(false); - const [target, setTarget] = useState(""); - const [newMessage, setNewMessage] = useState(""); - const [isPending, startTransition] = useTransition(); - - const { account } = Route.useParams(); - const { isLoading, data: contacts } = useQuery({ - queryKey: ["contacts", account], - queryFn: async () => { - const res = await commands.getContactList(); - - if (res.status === "ok") { - return res.data; - } else { - return []; - } - }, - refetchOnWindowFocus: false, - enabled: isOpen, - }); - - const navigate = Route.useNavigate(); - const scrollRef = useRef(null); - - const pasteFromClipboard = async () => { - const val = await readText(); - setTarget(val); - }; - - const sendMessage = () => { - startTransition(async () => { - if (!newMessage.length) return; - if (!target.length) return; - if (!target.startsWith("npub1")) { - await message("You must enter the public key as npub", { - title: "Send Message", - kind: "error", - }); - return; - } - - const decoded = nip19.decode(target); - let id: string; - - if (decoded.type !== "npub") { - await message("You must enter the public key as npub", { - title: "Send Message", - kind: "error", - }); - return; - } else { - id = decoded.data; - } - - // Connect to user's inbox relays - const connect = await commands.connectInboxRelays(target, false); - - // Send message - if (connect.status === "ok") { - const res = await commands.sendMessage(id, newMessage); - - if (res.status === "ok") { - setTarget(""); - setNewMessage(""); - setIsOpen(false); - - navigate({ - to: "/$account/chats/$id", - params: { account, id }, - }); - } else { - await message(res.error, { title: "Send Message", kind: "error" }); - return; - } - } else { - await message(connect.error, { - title: "Connect Inbox Relays", - kind: "error", - }); - return; - } - }); - }; - - return ( - - - - - - - -
-
- Send to - - - -
-
- To: -
- setTarget(e.target.value)} - disabled={isPending} - className="w-full pr-14 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" - /> - -
-
-
- Message: - setNewMessage(e.target.value)} - disabled={isPending} - className="flex-1 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" - /> - -
-
- - - - {isLoading ? ( -
- -
- ) : !contacts?.length ? ( -
-

Contact is empty.

-
- ) : ( - contacts?.map((contact) => ( - - )) - )} -
-
- - - - -
-
-
-
- ); -} - -function CurrentUser() { - const params = Route.useParams(); - const navigate = Route.useNavigate(); - - const showContextMenu = useCallback(async (e: React.MouseEvent) => { - e.preventDefault(); - - const menuItems = await Promise.all([ - MenuItem.new({ - text: "Copy Public Key", - action: async () => { - const npub = nip19.npubEncode(params.account); - await writeText(npub); - }, - }), - MenuItem.new({ - text: "Settings", - action: () => navigate({ to: "/" }), - }), - MenuItem.new({ - text: "Feedback", - action: async () => await open("https://github.com/lumehq/coop/issues"), - }), - PredefinedMenuItem.new({ item: "Separator" }), - MenuItem.new({ - text: "Switch account", - action: () => navigate({ to: "/" }), - }), - ]); - - const menu = await Menu.new({ - items: menuItems, - }); - - await menu.popup().catch((e) => console.error(e)); - }, []); - - return ( - - ); -} diff --git a/src/routes/$account.chats.new.lazy.tsx b/src/routes/$account.chats.new.lazy.tsx deleted file mode 100644 index 7e7f8d3..0000000 --- a/src/routes/$account.chats.new.lazy.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { CoopIcon } from "@/icons/coop"; -import { createLazyFileRoute } from "@tanstack/react-router"; - -export const Route = createLazyFileRoute("/$account/chats/new")({ - component: Screen, -}); - -function Screen() { - return ( -
- -

- coop on nostr. -

-
- ); -} diff --git a/src/routes/$account.contacts.lazy.tsx b/src/routes/$account.contacts.lazy.tsx deleted file mode 100644 index 2f8084c..0000000 --- a/src/routes/$account.contacts.lazy.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { User } from "@/components/user"; -import { X } from "@phosphor-icons/react"; -import * as ScrollArea from "@radix-ui/react-scroll-area"; -import { Link, createLazyFileRoute } from "@tanstack/react-router"; - -export const Route = createLazyFileRoute("/$account/contacts")({ - component: Screen, -}); - -function Screen() { - const params = Route.useParams(); - const contacts = Route.useLoaderData(); - - return ( - -
-
-
Contact List
-
- - - -
-
- -
- {contacts.map((contact) => ( - - - - - - - - - ))} -
-
- - - - - - ); -} diff --git a/src/routes/$account.contacts.tsx b/src/routes/$account.contacts.tsx deleted file mode 100644 index f9ac9fd..0000000 --- a/src/routes/$account.contacts.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { commands } from "@/commands"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/contacts")({ - loader: async () => { - const res = await commands.getContactList(); - - if (res.status === "ok") { - return res.data; - } else { - return []; - } - }, -}); diff --git a/src/routes/$account.relays.tsx b/src/routes/$account.relays.tsx deleted file mode 100644 index 16a17b5..0000000 --- a/src/routes/$account.relays.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { commands } from "@/commands"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/relays")({ - loader: async ({ params }) => { - const res = await commands.collectInboxRelays(params.account); - - if (res.status === "ok") { - return res.data; - } else { - throw new Error(res.error); - } - }, -}); diff --git a/src/routes/$account/_layout.tsx b/src/routes/$account/_layout.tsx new file mode 100644 index 0000000..1629340 --- /dev/null +++ b/src/routes/$account/_layout.tsx @@ -0,0 +1,16 @@ +import { commands } from "@/commands"; +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/$account/_layout")({ + beforeLoad: async ({ params }) => { + const res = await commands.ensureInboxRelays(params.account); + + if (res.status === "error") { + throw redirect({ + to: "/inbox-relays", + search: { account: params.account, redirect: window.location.href }, + replace: true, + }); + } + }, +}); diff --git a/src/routes/$account/_layout/chats.$id.lazy.tsx b/src/routes/$account/_layout/chats.$id.lazy.tsx new file mode 100644 index 0000000..46ba7a4 --- /dev/null +++ b/src/routes/$account/_layout/chats.$id.lazy.tsx @@ -0,0 +1,378 @@ +import { commands } from '@/commands' +import { cn, getReceivers, groupEventByDate, time, upload } from '@/commons' +import { Spinner } from '@/components/spinner' +import { User } from '@/components/user' +import { CoopIcon } from '@/icons/coop' +import { ArrowUp, Paperclip, X } from '@phosphor-icons/react' +import * as ScrollArea from '@radix-ui/react-scroll-area' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { createLazyFileRoute } from '@tanstack/react-router' +import { listen } from '@tauri-apps/api/event' +import { message } from '@tauri-apps/plugin-dialog' +import type { NostrEvent } from 'nostr-tools' +import { + type Dispatch, + type SetStateAction, + useCallback, + useRef, + useState, + useTransition, +} from 'react' +import { useEffect } from 'react' +import { Virtualizer, type VirtualizerHandle } from 'virtua' + +type EventPayload = { + event: string + sender: string +} + +export const Route = createLazyFileRoute('/$account/_layout/chats/$id')({ + component: Screen, +}) + +function Screen() { + return ( +
+
+ + +
+ ) +} + +function Header() { + const { account, id } = Route.useParams() + const { platform } = Route.useRouteContext() + + return ( +
+
+
+ + + + + + + + + + +
+
+
+
+ + + + +
Connected
+
+
+
+ ) +} + +function List() { + const { account, id } = Route.useParams() + const { isLoading, isError, data } = useQuery({ + queryKey: ['chats', id], + queryFn: async () => { + const res = await commands.getChatMessages(id) + + if (res.status === 'ok') { + const raw = res.data + const events: NostrEvent[] = raw.map((item) => JSON.parse(item)) + + return events + } else { + throw new Error(res.error) + } + }, + select: (data) => { + const groups = groupEventByDate(data) + return Object.entries(groups).reverse() + }, + refetchOnWindowFocus: false, + }) + + const queryClient = useQueryClient() + const scrollRef = useRef(null) + const ref = useRef(null) + const shouldStickToBottom = useRef(true) + + const renderItem = useCallback( + (item: NostrEvent, idx: number) => { + const self = account === item.pubkey + + return ( +
+
+
+ {item.content} +
+
+
+ + {time(item.created_at)} + +
+
+ ) + }, + [data], + ) + + useEffect(() => { + const unlisten = listen('event', async (data) => { + const event: NostrEvent = JSON.parse(data.payload.event) + const sender = data.payload.sender + const receivers = getReceivers(event.tags) + const group = [account, id] + + if (!group.includes(sender)) return + if (!group.some((item) => receivers.includes(item))) return + + await queryClient.setQueryData( + ['chats', id], + (prevEvents: NostrEvent[]) => { + if (!prevEvents) return [event] + return [event, ...prevEvents] + }, + ) + }) + + return () => { + unlisten.then((f) => f()) + } + }, [account, id]) + + useEffect(() => { + if (!data?.length) return + if (!ref.current) return + if (!shouldStickToBottom.current) return + + ref.current.scrollToIndex(data.length - 1, { + align: 'end', + }) + }, [data]) + + return ( + + + { + if (!ref.current) return + shouldStickToBottom.current = + offset - ref.current.scrollSize + ref.current.viewportSize >= -1.5 + }} + > + {isLoading ? ( + <> +
+
+
+
+
+
+
+
+
+
+ + ) : isError ? ( +
+
+ Cannot load message. Please try again later. +
+
+ ) : !data.length ? ( +
+ +
+ ) : ( + data.map((item) => ( +
+
+ {item[0]} +
+
+ {item[1] + .sort((a, b) => a.created_at - b.created_at) + .map((item, idx) => renderItem(item, idx))} +
+
+ )) + )} + + + + + + + + ) +} + +function Form() { + const { id } = Route.useParams() + const inboxRelays = Route.useLoaderData() + + const [newMessage, setNewMessage] = useState('') + const [attaches, setAttaches] = useState([]) + const [isPending, startTransition] = useTransition() + + const remove = (item: string) => { + setAttaches((prev) => prev.filter((att) => att !== item)) + } + + const submit = () => { + startTransition(async () => { + if (!newMessage.length) return + + const content = `${newMessage}\r\n${attaches.join('\r\n')}` + const res = await commands.sendMessage(id, content) + + if (res.status === 'error') { + await message(res.error, { + title: 'Send mesaage failed', + kind: 'error', + }) + return + } + + setNewMessage('') + }) + } + + return ( +
+ {!inboxRelays.length ? ( +
+ This user doesn't have inbox relays. You cannot send messages to them. +
+ ) : ( +
+ {attaches?.length ? ( +
+ {attaches.map((item, index) => ( + + ))} +
+ ) : null} +
+
+ +
+ setNewMessage(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') submit() + }} + className="flex-1 h-9 rounded-full px-3.5 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:outline-none focus:border-blue-500 placeholder:text-neutral-400 dark:placeholder:text-neutral-600" + /> + +
+
+ )} +
+ ) +} + +function AttachMedia({ + onUpload, +}: { + onUpload: Dispatch> +}) { + const [isPending, startTransition] = useTransition() + + const attach = () => { + startTransition(async () => { + const file = await upload() + + if (file) { + onUpload((prev) => [...prev, file]) + } else { + return + } + }) + } + + return ( + + ) +} diff --git a/src/routes/$account/_layout/chats.$id.tsx b/src/routes/$account/_layout/chats.$id.tsx new file mode 100644 index 0000000..d382b51 --- /dev/null +++ b/src/routes/$account/_layout/chats.$id.tsx @@ -0,0 +1,34 @@ +import { commands } from '@/commands' +import { Spinner } from '@/components/spinner' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/$account/_layout/chats/$id')({ + loader: async ({ params, context }) => { + const res = await commands.connectInboxRelays(params.id, false) + + if (res.status === 'ok') { + // Add id to chat manager to unsubscribe later. + context.chatManager.set(params.id, params.id) + + return res.data + } else { + return [] + } + }, + pendingComponent: Pending, + pendingMs: 200, + pendingMinMs: 100, +}) + +function Pending() { + return ( +
+
+ + + Connection in progress. Please wait ... + +
+
+ ) +} diff --git a/src/routes/$account/_layout/chats.lazy.tsx b/src/routes/$account/_layout/chats.lazy.tsx new file mode 100644 index 0000000..681a43d --- /dev/null +++ b/src/routes/$account/_layout/chats.lazy.tsx @@ -0,0 +1,505 @@ +import { commands } from '@/commands' +import { ago, cn } from '@/commons' +import { Spinner } from '@/components/spinner' +import { User } from '@/components/user' +import { + ArrowRight, + CaretDown, + CirclesFour, + Plus, + X, +} from '@phosphor-icons/react' +import * as Dialog from '@radix-ui/react-dialog' +import * as Progress from '@radix-ui/react-progress' +import * as ScrollArea from '@radix-ui/react-scroll-area' +import { useQuery } from '@tanstack/react-query' +import { Link, Outlet, createLazyFileRoute } from '@tanstack/react-router' +import { listen } from '@tauri-apps/api/event' +import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu' +import { readText, writeText } from '@tauri-apps/plugin-clipboard-manager' +import { message } from '@tauri-apps/plugin-dialog' +import { open } from '@tauri-apps/plugin-shell' +import { type NostrEvent, nip19 } from 'nostr-tools' +import { useCallback, useEffect, useRef, useState, useTransition } from 'react' +import { Virtualizer } from 'virtua' + +type EventPayload = { + event: string + sender: string +} + +export const Route = createLazyFileRoute('/$account/_layout/chats')({ + component: Screen, +}) + +function Screen() { + return ( +
+
+
+ +
+
+ +
+
+ ) +} + +function Header() { + const { platform } = Route.useRouteContext() + const { account } = Route.useParams() + + return ( +
+ +
+ + + + +
+
+ ) +} + +function ChatList() { + const { account } = Route.useParams() + const { queryClient } = Route.useRouteContext() + const { isLoading, data } = useQuery({ + queryKey: ['chats'], + queryFn: async () => { + const res = await commands.getChats() + + if (res.status === 'ok') { + const raw = res.data + const events = raw.map((item) => JSON.parse(item) as NostrEvent) + + return events + } else { + throw new Error(res.error) + } + }, + select: (data) => data.sort((a, b) => b.created_at - a.created_at), + refetchOnMount: false, + refetchOnWindowFocus: false, + }) + + const [isSync, setIsSync] = useState(false) + const [progress, setProgress] = useState(0) + + useEffect(() => { + const timer = setInterval( + () => setProgress((prev) => (prev <= 100 ? prev + 4 : 100)), + 1200, + ) + return () => clearInterval(timer) + }, []) + + useEffect(() => { + const unlisten = listen('synchronized', async () => { + await queryClient.refetchQueries({ queryKey: ['chats'] }) + setIsSync(true) + }) + + return () => { + unlisten.then((f) => f()) + } + }, []) + + useEffect(() => { + const unlisten = listen('event', async (data) => { + const event: NostrEvent = JSON.parse(data.payload.event) + const chats: NostrEvent[] = await queryClient.getQueryData(['chats']) + + if (chats) { + const index = chats.findIndex((item) => item.pubkey === event.pubkey) + + if (index === -1) { + await queryClient.setQueryData( + ['chats'], + (prevEvents: NostrEvent[]) => { + if (!prevEvents) return prevEvents + if (event.pubkey === account) return + + return [event, ...prevEvents] + }, + ) + } else { + const newEvents = [...chats] + newEvents[index] = { + ...event, + } + + await queryClient.setQueryData(['chats'], newEvents) + } + } + }) + + return () => { + unlisten.then((f) => f()) + } + }, []) + + return ( + + + {isLoading ? ( + <> + {[...Array(5).keys()].map((i) => ( +
+
+
+
+ ))} + + ) : isSync && !data.length ? ( +
+
+ No chats. +
+
+ ) : ( + data.map((item) => ( + + {({ isActive, isTransitioning }) => ( + + + +
+
+ + + {account === item.pubkey ? '(you)' : ''} + +
+ {isTransitioning ? ( + + ) : ( + + {ago(item.created_at)} + + )} +
+
+
+ )} + + )) + )} + + {!isSync ? : null} + + + + + + ) +} + +function SyncPopup({ progress }: { progress: number }) { + return ( +
+
+ + + + Syncing message... +
+
+ ) +} + +function Compose() { + const [isOpen, setIsOpen] = useState(false) + const [target, setTarget] = useState('') + const [newMessage, setNewMessage] = useState('') + const [isPending, startTransition] = useTransition() + + const { account } = Route.useParams() + const { isLoading, data: contacts } = useQuery({ + queryKey: ['contacts', account], + queryFn: async () => { + const res = await commands.getContactList() + + if (res.status === 'ok') { + return res.data + } else { + return [] + } + }, + refetchOnWindowFocus: false, + enabled: isOpen, + }) + + const navigate = Route.useNavigate() + const scrollRef = useRef(null) + + const pasteFromClipboard = async () => { + const val = await readText() + setTarget(val) + } + + const sendMessage = () => { + startTransition(async () => { + if (!newMessage.length) return + if (!target.length) return + if (!target.startsWith('npub1')) { + await message('You must enter the public key as npub', { + title: 'Send Message', + kind: 'error', + }) + return + } + + const decoded = nip19.decode(target) + let id: string + + if (decoded.type !== 'npub') { + await message('You must enter the public key as npub', { + title: 'Send Message', + kind: 'error', + }) + return + } else { + id = decoded.data + } + + // Connect to user's inbox relays + const connect = await commands.connectInboxRelays(target, false) + + // Send message + if (connect.status === 'ok') { + const res = await commands.sendMessage(id, newMessage) + + if (res.status === 'ok') { + setTarget('') + setNewMessage('') + setIsOpen(false) + + navigate({ + to: '/$account/chats/$id', + params: { account, id }, + }) + } else { + await message(res.error, { title: 'Send Message', kind: 'error' }) + return + } + } else { + await message(connect.error, { + title: 'Connect Inbox Relays', + kind: 'error', + }) + return + } + }) + } + + return ( + + + + + + + +
+
+ Send to + + + +
+
+ To: +
+ setTarget(e.target.value)} + disabled={isPending} + className="w-full pr-14 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" + /> + +
+
+
+ Message: + setNewMessage(e.target.value)} + disabled={isPending} + className="flex-1 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" + /> + +
+
+ + + + {isLoading ? ( +
+ +
+ ) : !contacts?.length ? ( +
+

Contact is empty.

+
+ ) : ( + contacts?.map((contact) => ( + + )) + )} +
+
+ + + + +
+
+
+
+ ) +} + +function CurrentUser() { + const params = Route.useParams() + const navigate = Route.useNavigate() + + const showContextMenu = useCallback(async (e: React.MouseEvent) => { + e.preventDefault() + + const menuItems = await Promise.all([ + MenuItem.new({ + text: 'Copy Public Key', + action: async () => { + const npub = nip19.npubEncode(params.account) + await writeText(npub) + }, + }), + MenuItem.new({ + text: 'Settings', + action: () => navigate({ to: '/' }), + }), + MenuItem.new({ + text: 'Feedback', + action: async () => await open('https://github.com/lumehq/coop/issues'), + }), + PredefinedMenuItem.new({ item: 'Separator' }), + MenuItem.new({ + text: 'Switch account', + action: () => navigate({ to: '/' }), + }), + ]) + + const menu = await Menu.new({ + items: menuItems, + }) + + await menu.popup().catch((e) => console.error(e)) + }, []) + + return ( + + ) +} diff --git a/src/routes/$account/_layout/chats.new.lazy.tsx b/src/routes/$account/_layout/chats.new.lazy.tsx new file mode 100644 index 0000000..3922c0f --- /dev/null +++ b/src/routes/$account/_layout/chats.new.lazy.tsx @@ -0,0 +1,20 @@ +import { CoopIcon } from '@/icons/coop' +import { createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/$account/_layout/chats/new')({ + component: Screen, +}) + +function Screen() { + return ( +
+ +

+ coop on nostr. +

+
+ ) +} diff --git a/src/routes/$account/_layout/contacts.lazy.tsx b/src/routes/$account/_layout/contacts.lazy.tsx new file mode 100644 index 0000000..64407ff --- /dev/null +++ b/src/routes/$account/_layout/contacts.lazy.tsx @@ -0,0 +1,63 @@ +import { User } from '@/components/user' +import { X } from '@phosphor-icons/react' +import * as ScrollArea from '@radix-ui/react-scroll-area' +import { Link, createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/$account/_layout/contacts')({ + component: Screen, +}) + +function Screen() { + const params = Route.useParams() + const contacts = Route.useLoaderData() + + return ( + +
+
+
Contact List
+
+ + + +
+
+ +
+ {contacts.map((contact) => ( + + + + + + + + + ))} +
+
+ + + + + + ) +} diff --git a/src/routes/$account/_layout/contacts.tsx b/src/routes/$account/_layout/contacts.tsx new file mode 100644 index 0000000..f333f20 --- /dev/null +++ b/src/routes/$account/_layout/contacts.tsx @@ -0,0 +1,14 @@ +import { commands } from '@/commands' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/$account/_layout/contacts')({ + loader: async () => { + const res = await commands.getContactList() + + if (res.status === 'ok') { + return res.data + } else { + return [] + } + }, +}) diff --git a/src/routes/$account.relays.lazy.tsx b/src/routes/inbox-relays.lazy.tsx similarity index 78% rename from src/routes/$account.relays.lazy.tsx rename to src/routes/inbox-relays.lazy.tsx index e16906e..160ce25 100644 --- a/src/routes/$account.relays.lazy.tsx +++ b/src/routes/inbox-relays.lazy.tsx @@ -2,28 +2,47 @@ import { commands } from "@/commands"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; import { Plus, X } from "@phosphor-icons/react"; +import { useQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; import { message } from "@tauri-apps/plugin-dialog"; -import { useEffect, useState, useTransition } from "react"; +import { useState, useTransition } from "react"; -export const Route = createLazyFileRoute("/$account/relays")({ +export const Route = createLazyFileRoute("/inbox-relays")({ component: Screen, }); function Screen() { - const navigate = Route.useNavigate(); - const inboxRelays = Route.useLoaderData(); - const { account } = Route.useParams(); + const { account, redirect } = Route.useSearch(); + const { queryClient } = Route.useRouteContext(); + const { + data: relays, + error, + isError, + isLoading, + } = useQuery({ + queryKey: ["relays", account], + queryFn: async () => { + const res = await commands.getInboxRelays(account); + + if (res.status === "ok") { + return res.data; + } else { + throw new Error(res.error); + } + }, + refetchOnWindowFocus: false, + }); const [newRelay, setNewRelay] = useState(""); - const [relays, setRelays] = useState([]); const [isPending, startTransition] = useTransition(); + const navigate = Route.useNavigate(); + const add = () => { try { let url = newRelay; - if (relays.length >= 3) { + if (relays?.length >= 3) { return message("You should keep relay lists small (1 - 3 relays).", { kind: "info", }); @@ -37,7 +56,10 @@ function Screen() { const relay = new URL(url); // Update - setRelays((prev) => [...prev, relay.toString()]); + queryClient.setQueryData(["relays", account], (prev: string[]) => [ + ...prev, + relay.toString(), + ]); setNewRelay(""); } catch { message("URL is not valid.", { kind: "error" }); @@ -45,12 +67,14 @@ function Screen() { }; const remove = (relay: string) => { - setRelays((prev) => prev.filter((item) => item !== relay)); + queryClient.setQueryData(["relays", account], (prev: string[]) => + prev.filter((item) => item !== relay), + ); }; const submit = () => { startTransition(async () => { - if (!relays.length) { + if (!relays?.length) { await message("You need to add at least 1 relay", { kind: "info" }); return; } @@ -59,8 +83,7 @@ function Screen() { if (res.status === "ok") { navigate({ - to: "/", - params: { account }, + to: redirect, replace: true, }); } else { @@ -73,9 +96,21 @@ function Screen() { }); }; - useEffect(() => { - setRelays(inboxRelays); - }, [inboxRelays]); + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+

{error.message}

+
+ ); + } return (
@@ -151,7 +186,7 @@ function Screen() {