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 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 { message } from "@tauri-apps/plugin-dialog"; import type { NostrEvent } from "nostr-tools"; import { useCallback, useEffect, useState, useTransition } from "react"; type ChatPayload = { events: string[]; }; 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, }); useEffect(() => { const unlisten = listen("sync_chat", async (data) => { const raw = data.payload.events; const events: NostrEvent[] = raw.map((item) => JSON.parse(item)); const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]); if (chats?.length) { const newEvents = [...events, ...chats]; const uniqs = [ ...new Map(newEvents.map((item) => [item.pubkey, item])).values(), ]; await queryClient.setQueryData(["chats"], uniqs); } else { await queryClient.setQueryData(["chats"], events); } }); 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 || !data.length ? (
{[...Array(5).keys()].map((i) => (
))}
) : !data?.length ? (
No chats.
) : ( data.map((item) => ( {({ isActive }) => (
{account === item.pubkey ? "(you)" : ""}
{ago(item.created_at)}
)} )) )} ); } 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 sendMessage = async () => { startTransition(async () => { if (!newMessage.length) return; if (!target.length) return; const res = await commands.sendMessage(target, newMessage); if (res.status === "ok") { navigate({ to: "/$account/chats/$id", params: { account, id: target }, }); } else { await message(res.error, { title: "Coop", kind: "error" }); return; } }); }; return (
Send to
To: setTarget(e.target.value)} disabled={isPending || isLoading} className="flex-1 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" />
Message: setNewMessage(e.target.value)} disabled={isPending || isLoading} 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: "Contacts", action: () => navigate({ to: "/$account/contacts", params: { account: params.account }, }), }), MenuItem.new({ text: "Settings", action: () => navigate({ to: "/" }), }), MenuItem.new({ text: "Feedback", action: () => navigate({ to: "/" }), }), 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 ( ); }