feat: chat
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Pick a thread to forward a message into.
|
||||
*
|
||||
* The destination list is the caller's own inbox, so there is no way to name a
|
||||
* thread they aren't in — and the BFF re-checks both memberships anyway, since
|
||||
* a client-supplied conversation id proves nothing.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { UserAvatar } from "@/components/ui/user-avatar";
|
||||
import {
|
||||
chat,
|
||||
conversationTitle,
|
||||
groupAvatarUrl,
|
||||
type ChatMessage,
|
||||
type InboxItem,
|
||||
} from "@/lib/chat";
|
||||
|
||||
export function ForwardDialog({
|
||||
message,
|
||||
onClose,
|
||||
}: {
|
||||
message: ChatMessage;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [threads, setThreads] = useState<InboxItem[]>([]);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
chat
|
||||
.inbox("chats")
|
||||
.then((rows) => {
|
||||
if (cancelled) return;
|
||||
setThreads(rows ?? []);
|
||||
setState("ready");
|
||||
})
|
||||
.catch(() => !cancelled && setState("error"));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [onClose]);
|
||||
|
||||
const forward = async (to: InboxItem) => {
|
||||
setBusy(to.id);
|
||||
try {
|
||||
await chat.forward(to.id, message.id);
|
||||
toast.success(`Forwarded to ${conversationTitle(to)}`);
|
||||
onClose();
|
||||
} catch {
|
||||
toast.error("Could not forward the message");
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/40 sm:items-center">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
onClick={onClose}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="relative z-10 max-h-[70vh] w-full max-w-md overflow-hidden rounded-t-2xl border border-border bg-background sm:rounded-2xl">
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
<h2 className="text-sm font-semibold text-foreground">Forward to…</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="rounded-md p-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[55vh] overflow-y-auto">
|
||||
{state === "error" ? (
|
||||
<p className="p-4 text-sm text-destructive">
|
||||
Could not load your chats.
|
||||
</p>
|
||||
) : state === "loading" ? (
|
||||
<p className="p-4 text-sm text-muted-foreground">Loading…</p>
|
||||
) : threads.length === 0 ? (
|
||||
<p className="p-4 text-sm text-muted-foreground">
|
||||
You have no other chats yet.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-border">
|
||||
{threads.map((t) => (
|
||||
<li key={t.id}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy !== null}
|
||||
onClick={() => forward(t)}
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-muted/60 disabled:opacity-60"
|
||||
>
|
||||
<UserAvatar
|
||||
src={
|
||||
t.kind === "group"
|
||||
? groupAvatarUrl(t.id, t.avatar_key)
|
||||
: t.peer?.avatar_url
|
||||
}
|
||||
name={conversationTitle(t)}
|
||||
className="size-9 shrink-0"
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-sm text-foreground">
|
||||
{conversationTitle(t)}
|
||||
</span>
|
||||
{busy === t.id && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Sending…
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user