"use client"; /** * MentionsDrawer — the users tagged on a post. Right-side drawer on desktop, * bottom sheet on mobile. Read-only for everyone except the post's author, * who gets an autocomplete to add people and an X to remove them. * * `onCountChange(delta)` lets the card update its mentions-icon badge as the * author adds/removes, without a refetch. */ import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import Image from "next/image"; import { X } from "lucide-react"; import { toast } from "sonner"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { useMediaQuery } from "@/components/selection-toolbar/shared"; import { posts, type UserSummary } from "@/lib/social"; import { useAuth } from "@/components/auth/auth-provider"; import { MentionAutocomplete } from "@/components/social/mention-autocomplete"; import { cn } from "@/lib/utils"; export function MentionsDrawer({ open, postId, authorId, onClose, onCountChange, }: { open: boolean; postId: string; authorId: string; onClose: () => void; onCountChange?: (delta: number) => void; }) { const isMobile = useMediaQuery("(max-width: 767px)"); const direction = isMobile ? "bottom" : "right"; const { user } = useAuth(); const isAuthor = !!user && user.id === authorId; const [list, setList] = useState([]); const [status, setStatus] = useState<"loading" | "idle" | "error">("loading"); const reqRef = useRef(0); useEffect(() => { if (!open) return; // Deferred to a timer so the setState calls aren't synchronous in the // effect body. const t = window.setTimeout(() => { const token = ++reqRef.current; setStatus("loading"); posts .mentions(postId) .then((res) => { if (reqRef.current === token) { setList(res); setStatus("idle"); } }) .catch(() => { if (reqRef.current === token) setStatus("error"); }); }, 0); return () => window.clearTimeout(t); }, [open, postId]); async function add(u: UserSummary) { if (!u.id || list.some((m) => m.id === u.id)) return; setList((prev) => [...prev, u]); // optimistic onCountChange?.(1); try { await posts.addMention(postId, u.id); } catch { setList((prev) => prev.filter((m) => m.id !== u.id)); onCountChange?.(-1); toast.error("Couldn't add that person"); } } async function remove(u: UserSummary) { if (!u.id) return; setList((prev) => prev.filter((m) => m.id !== u.id)); // optimistic onCountChange?.(-1); try { await posts.removeMention(postId, u.id); } catch { setList((prev) => [...prev, u]); onCountChange?.(1); toast.error("Couldn't remove that person"); } } return ( { if (!o) onClose(); }} > Mentions
{/* Author add-field lives inside the scroll area so its inline results scroll with the sheet instead of overflowing a fixed header. */} {isAuthor ? (
m.id).filter((id): id is string => !!id)} onSelect={add} placeholder="Add someone…" />
) : null} {status === "loading" ? ( Loading… ) : status === "error" ? ( Couldn't load mentions. ) : list.length === 0 ? ( {isAuthor ? "No one tagged yet — add someone above." : "No one is tagged."} ) : ( list.map((u) => (
{u.name} @{u.handle} {isAuthor ? ( ) : null}
)) )}
); } function RowAvatar({ src, name }: { src: string | null; name: string }) { if (src) { return ( {name} ); } const initials = name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase() || "?"; return ( {initials} ); } function Message({ children }: { children: React.ReactNode }) { return (

{children}

); }