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