import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { ArrowLeft, ShieldCheck, SquarePen, ExternalLink } from "lucide-react"; import { api, type Conversation, type MessageUser } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; import { relativeTime } from "../lib/format"; import { partnerPub, SYSTEM_ID, useUnlocked } from "../lib/messaging"; import { openChat } from "../lib/chatDock"; import * as e2ee from "../lib/e2ee"; import Avatar from "./Avatar"; import KeyGate from "./KeyGate"; import ChatThread from "./ChatThread"; const POLL_MS = 20000; type View = "list" | "new" | { partnerId: number; name?: string; avatar?: string | null; system?: boolean }; export default function Messages({ meId }: { meId: number }) { const [view, setView] = useState("list"); const unlocked = useUnlocked(); if (typeof view === "object") { return ( setView("list")} /> ); } if (view === "new") { return setView({ partnerId: u.id, name: u.name, avatar: u.avatar_url })} onBack={() => setView("list")} />; } return ( setView({ partnerId: c.partner.id, name: c.partner.name, avatar: c.partner.avatar_url, system: c.partner.id === SYSTEM_ID })} onNew={() => setView("new")} /> ); } function ConversationList({ meId, unlocked, onOpen, onNew, }: { meId: number; unlocked: boolean; onOpen: (c: Conversation) => void; onNew: () => void; }) { const { t } = useTranslation(); const q = useLiveQuery<{ items: Conversation[] }>(["conversations"], api.conversations, { intervalMs: POLL_MS }); const items = q.data?.items ?? []; const [previews, setPreviews] = useState>({}); useEffect(() => { if (!unlocked) return; let cancelled = false; (async () => { const out: Record = {}; for (const c of items) { const m = c.last_message; if (m.kind === "user" && m.ciphertext && m.iv) { try { const pub = await partnerPub(c.partner.id); out[c.partner.id] = await e2ee.decryptFrom(c.partner.id, pub, m.ciphertext, m.iv); } catch { out[c.partner.id] = "⚠"; } } } if (!cancelled) setPreviews(out); })(); return () => { cancelled = true; }; }, [items, unlocked]); function previewText(c: Conversation): string { const m = c.last_message; if (m.kind === "system") return m.body ?? ""; if (!unlocked) return "🔒 " + t("messages.encrypted"); return previews[c.partner.id] ?? "…"; } return (
{t("messages.conversations")}
{unlocked && ( )}
{!unlocked && } {q.isLoading && !q.data ? (
{t("common.loading")}
) : items.length === 0 ? (
{t("messages.empty")}
) : (
{items.map((c) => { const isSystem = c.partner.id === SYSTEM_ID; return (
{!isSystem && unlocked && ( )}
); })}
)}
); } function Directory({ onPick, onBack }: { onPick: (u: MessageUser) => void; onBack: () => void }) { const { t } = useTranslation(); const [filter, setFilter] = useState(""); const q = useQuery({ queryKey: ["message-users"], queryFn: api.messageUsers }); const users = (q.data ?? []).filter((u) => u.name.toLowerCase().includes(filter.trim().toLowerCase())); return (
{t("messages.newTitle")}
setFilter(e.target.value)} placeholder={t("messages.searchPeople")} className="w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent" /> {q.isLoading ? (
{t("common.loading")}
) : users.length === 0 ? (
{t("messages.noPeople")}
) : (
{users.map((u) => ( ))}
)}
); } function PageThread({ meId, partnerId, isSystem, seedName, seedAvatar, unlocked, onBack, }: { meId: number; partnerId: number; isSystem: boolean; seedName?: string; seedAvatar?: string | null; unlocked: boolean; onBack: () => void; }) { const { t } = useTranslation(); const name = seedName ?? ""; return (
{name} {!isSystem && }
{!isSystem && unlocked && ( )}
); }