mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Messaging: whole-board topics, delete conversations/threads, dialog overflow + board names
- Whole-board messaging: staff starting a Topic can add an entire association's
board in one click ("Whole Board — <Association>") in NewTopicThreadDialog;
create_topic_thread already permits staff to add board members.
- Delete for everyone: trash control on Direct conversations (deletes the 1:1
both directions) and on Topic threads (deletes the thread for the creator or
admin/manager — cascade). Backed by new RLS delete policies on direct_messages
and message_threads (migration allow_delete_conversations_and_threads).
- Fix New Message dialog overflow for real: the grid child needed min-w-0 (+
overflow-hidden on the card); applied the same guard to the Topic dialog.
- Fix board members showing as "Unknown User": fall back to board_members.member_name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,17 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Search, PenSquare, Loader2 } from "lucide-react";
|
import { Search, PenSquare, Loader2, Trash2 } from "lucide-react";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
||||||
|
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedPartnerId: string | null;
|
selectedPartnerId: string | null;
|
||||||
@@ -24,9 +29,11 @@ function getInitials(name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ConversationList({ selectedPartnerId, onSelectPartner }: Props) {
|
export default function ConversationList({ selectedPartnerId, onSelectPartner }: Props) {
|
||||||
const { isHomeowner, isRvBoatLot, isBoardMember, isStaff } = useAuth();
|
const { user, isHomeowner, isRvBoatLot, isBoardMember, isStaff } = useAuth();
|
||||||
const { conversations, loading } = useConversations();
|
const { conversations, loading } = useConversations();
|
||||||
|
const { toast } = useToast();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null);
|
||||||
const useStaffConversation = isHomeowner || isRvBoatLot;
|
const useStaffConversation = isHomeowner || isRvBoatLot;
|
||||||
const isBoardOnly = isBoardMember && !isStaff;
|
const isBoardOnly = isBoardMember && !isStaff;
|
||||||
|
|
||||||
@@ -34,6 +41,23 @@ export default function ConversationList({ selectedPartnerId, onSelectPartner }:
|
|||||||
c.partner_name.toLowerCase().includes(search.toLowerCase())
|
c.partner_name.toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Delete the whole 1:1 conversation for everyone (both directions). The
|
||||||
|
// realtime subscription in useConversations refetches the list on delete.
|
||||||
|
const deleteConversation = async (partnerId: string) => {
|
||||||
|
if (!user) return;
|
||||||
|
const me = user.id;
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("direct_messages")
|
||||||
|
.delete()
|
||||||
|
.or(`and(sender_id.eq.${me},recipient_id.eq.${partnerId}),and(sender_id.eq.${partnerId},recipient_id.eq.${me})`);
|
||||||
|
if (error) {
|
||||||
|
toast({ title: "Couldn't delete conversation", description: error.message, variant: "destructive" });
|
||||||
|
} else {
|
||||||
|
toast({ title: "Conversation deleted" });
|
||||||
|
}
|
||||||
|
setPendingDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full border-r border-border">
|
<div className="flex flex-col h-full border-r border-border">
|
||||||
<div className="p-3 border-b border-border space-y-2">
|
<div className="p-3 border-b border-border space-y-2">
|
||||||
@@ -107,8 +131,8 @@ export default function ConversationList({ selectedPartnerId, onSelectPartner }:
|
|||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{filtered.map((conv) => (
|
{filtered.map((conv) => (
|
||||||
|
<div key={conv.partner_id} className="group relative">
|
||||||
<button
|
<button
|
||||||
key={conv.partner_id}
|
|
||||||
onClick={() => onSelectPartner(conv.partner_id, conv.partner_name)}
|
onClick={() => onSelectPartner(conv.partner_id, conv.partner_name)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-start gap-3 px-3 py-3 text-left hover:bg-accent transition-colors",
|
"w-full flex items-start gap-3 px-3 py-3 text-left hover:bg-accent transition-colors",
|
||||||
@@ -145,10 +169,39 @@ export default function ConversationList({ selectedPartnerId, onSelectPartner }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setPendingDelete({ id: conv.partner_id, name: conv.partner_name }); }}
|
||||||
|
title="Delete conversation"
|
||||||
|
className="absolute right-2 top-2 hidden group-hover:flex h-6 w-6 items-center justify-center rounded-md bg-background/80 text-muted-foreground hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
<AlertDialog open={!!pendingDelete} onOpenChange={(o) => { if (!o) setPendingDelete(null); }}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete this conversation?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This permanently deletes the conversation with {pendingDelete?.name} for everyone. This can't be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
onClick={() => pendingDelete && deleteConversation(pendingDelete.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -205,7 +258,7 @@ function NewConversationDialog({ onSelectPartner }: { onSelectPartner: (id: stri
|
|||||||
.neq("status", "archived"),
|
.neq("status", "archived"),
|
||||||
(supabase as any)
|
(supabase as any)
|
||||||
.from("board_members")
|
.from("board_members")
|
||||||
.select("user_id, role, associations(name)")
|
.select("user_id, role, member_name, associations(name)")
|
||||||
.not("user_id", "is", null),
|
.not("user_id", "is", null),
|
||||||
supabase.from("user_roles").select("user_id, role").in("role", ["admin", "manager"]),
|
supabase.from("user_roles").select("user_id, role").in("role", ["admin", "manager"]),
|
||||||
]);
|
]);
|
||||||
@@ -243,7 +296,7 @@ function NewConversationDialog({ onSelectPartner }: { onSelectPartner: (id: stri
|
|||||||
userIds.add(b.user_id);
|
userIds.add(b.user_id);
|
||||||
recipients.push({
|
recipients.push({
|
||||||
user_id: b.user_id,
|
user_id: b.user_id,
|
||||||
full_name: null,
|
full_name: b.member_name || null,
|
||||||
avatar_url: null,
|
avatar_url: null,
|
||||||
category: "board",
|
category: "board",
|
||||||
context: `Board • ${b.role || "Member"}${b.associations?.name ? ` (${b.associations.name})` : ""}`,
|
context: `Board • ${b.role || "Member"}${b.associations?.name ? ` (${b.associations.name})` : ""}`,
|
||||||
@@ -313,11 +366,11 @@ function NewConversationDialog({ onSelectPartner }: { onSelectPartner: (id: stri
|
|||||||
<PenSquare className="h-4 w-4" />
|
<PenSquare className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>New Message</DialogTitle>
|
<DialogTitle>New Message</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 min-w-0">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search recipients..."
|
placeholder="Search recipients..."
|
||||||
value={search}
|
value={search}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [recipients, setRecipients] = useState<Recipient[]>([]);
|
const [recipients, setRecipients] = useState<Recipient[]>([]);
|
||||||
|
const [boardGroups, setBoardGroups] = useState<{ id: string; name: string; memberIds: string[] }[]>([]);
|
||||||
const [loadingRecipients, setLoadingRecipients] = useState(false);
|
const [loadingRecipients, setLoadingRecipients] = useState(false);
|
||||||
const [topic, setTopic] = useState<string>("");
|
const [topic, setTopic] = useState<string>("");
|
||||||
const [subject, setSubject] = useState("");
|
const [subject, setSubject] = useState("");
|
||||||
@@ -53,13 +54,30 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
|
|||||||
|
|
||||||
const list: Recipient[] = [];
|
const list: Recipient[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const b of (board ?? []) as any[]) {
|
|
||||||
if (!b.user_id || b.user_id === user.id || seen.has(b.user_id)) continue;
|
if (isStaff) {
|
||||||
seen.add(b.user_id);
|
// Staff message whole boards: group board members by association so an
|
||||||
list.push({
|
// entire board can be added in one click.
|
||||||
user_id: b.user_id, name: "", category: "Board",
|
const groups = new Map<string, { id: string; name: string; memberIds: string[] }>();
|
||||||
context: `${b.role || "Board Member"}${b.associations?.name ? ` · ${b.associations.name}` : ""}`,
|
for (const b of (board ?? []) as any[]) {
|
||||||
});
|
if (!b.user_id || b.user_id === user.id || !b.association_id) continue;
|
||||||
|
const g = groups.get(b.association_id)
|
||||||
|
?? { id: b.association_id, name: b.associations?.name || "Association", memberIds: [] };
|
||||||
|
if (!g.memberIds.includes(b.user_id)) g.memberIds.push(b.user_id);
|
||||||
|
groups.set(b.association_id, g);
|
||||||
|
}
|
||||||
|
setBoardGroups(Array.from(groups.values()).filter((g) => g.memberIds.length).sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
|
} else {
|
||||||
|
// Board members: their own board peers, listed individually.
|
||||||
|
setBoardGroups([]);
|
||||||
|
for (const b of (board ?? []) as any[]) {
|
||||||
|
if (!b.user_id || b.user_id === user.id || seen.has(b.user_id)) continue;
|
||||||
|
seen.add(b.user_id);
|
||||||
|
list.push({
|
||||||
|
user_id: b.user_id, name: "", category: "Board",
|
||||||
|
context: `${b.role || "Board Member"}${b.associations?.name ? ` · ${b.associations.name}` : ""}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const id of mgmtIds) {
|
for (const id of mgmtIds) {
|
||||||
if (seen.has(id)) continue;
|
if (seen.has(id)) continue;
|
||||||
@@ -90,6 +108,15 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (memberIds: string[]) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
const allOn = memberIds.every((id) => next.has(id));
|
||||||
|
memberIds.forEach((id) => (allOn ? next.delete(id) : next.add(id)));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!topic) return toast({ title: "Pick a topic", variant: "destructive" });
|
if (!topic) return toast({ title: "Pick a topic", variant: "destructive" });
|
||||||
if (!subject.trim()) return toast({ title: "Add a subject", variant: "destructive" });
|
if (!subject.trim()) return toast({ title: "Add a subject", variant: "destructive" });
|
||||||
@@ -132,9 +159,9 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button size="sm" className="gap-1"><Plus className="h-4 w-4" /> New Topic</Button>
|
<Button size="sm" className="gap-1"><Plus className="h-4 w-4" /> New Topic</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className="max-w-lg overflow-hidden">
|
||||||
<DialogHeader><DialogTitle>New Topic</DialogTitle></DialogHeader>
|
<DialogHeader><DialogTitle>New Topic</DialogTitle></DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 min-w-0">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>Topic</Label>
|
<Label>Topic</Label>
|
||||||
<Select value={topic} onValueChange={setTopic}>
|
<Select value={topic} onValueChange={setTopic}>
|
||||||
@@ -153,17 +180,33 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
|
|||||||
<div className="max-h-44 overflow-y-auto rounded-md border border-border divide-y">
|
<div className="max-h-44 overflow-y-auto rounded-md border border-border divide-y">
|
||||||
{loadingRecipients ? (
|
{loadingRecipients ? (
|
||||||
<div className="flex justify-center py-6"><Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /></div>
|
<div className="flex justify-center py-6"><Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /></div>
|
||||||
) : recipients.length === 0 ? (
|
) : (boardGroups.length === 0 && recipients.length === 0) ? (
|
||||||
<p className="text-sm text-muted-foreground p-3">No contacts available.</p>
|
<p className="text-sm text-muted-foreground p-3">No contacts available.</p>
|
||||||
) : recipients.map((r) => (
|
) : (
|
||||||
<label key={r.user_id} className="flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-accent">
|
<>
|
||||||
<Checkbox checked={selected.has(r.user_id)} onCheckedChange={() => toggle(r.user_id)} />
|
{boardGroups.map((g) => {
|
||||||
<div className="min-w-0">
|
const allOn = g.memberIds.every((id) => selected.has(id));
|
||||||
<div className="text-sm font-medium truncate">{r.name}</div>
|
return (
|
||||||
<div className="text-[11px] text-muted-foreground truncate">{r.context}</div>
|
<label key={`board-${g.id}`} className="flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-accent">
|
||||||
</div>
|
<Checkbox checked={allOn} onCheckedChange={() => toggleGroup(g.memberIds)} />
|
||||||
</label>
|
<div className="min-w-0">
|
||||||
))}
|
<div className="text-sm font-medium truncate">Whole Board — {g.name}</div>
|
||||||
|
<div className="text-[11px] text-muted-foreground truncate">{g.memberIds.length} board member{g.memberIds.length === 1 ? "" : "s"}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{recipients.map((r) => (
|
||||||
|
<label key={r.user_id} className="flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-accent">
|
||||||
|
<Checkbox checked={selected.has(r.user_id)} onCheckedChange={() => toggle(r.user_id)} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-medium truncate">{r.name}</div>
|
||||||
|
<div className="text-[11px] text-muted-foreground truncate">{r.context}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { useMessageThreads } from "@/hooks/useMessageThreads";
|
import { useMessageThreads } from "@/hooks/useMessageThreads";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { topicBadgeClass } from "@/lib/messageTopics";
|
import { topicBadgeClass } from "@/lib/messageTopics";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Loader2, MessagesSquare } from "lucide-react";
|
import { Loader2, MessagesSquare, Trash2 } from "lucide-react";
|
||||||
import { format, isToday } from "date-fns";
|
import { format, isToday } from "date-fns";
|
||||||
import NewTopicThreadDialog from "./NewTopicThreadDialog";
|
import NewTopicThreadDialog from "./NewTopicThreadDialog";
|
||||||
|
import {
|
||||||
|
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
||||||
|
AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
function when(dateStr: string | null) {
|
function when(dateStr: string | null) {
|
||||||
if (!dateStr) return "";
|
if (!dateStr) return "";
|
||||||
@@ -16,6 +24,18 @@ export default function TopicThreadList({
|
|||||||
selectedThreadId, onSelect,
|
selectedThreadId, onSelect,
|
||||||
}: { selectedThreadId: string | null; onSelect: (id: string) => void }) {
|
}: { selectedThreadId: string | null; onSelect: (id: string) => void }) {
|
||||||
const { threads, loading } = useMessageThreads();
|
const { threads, loading } = useMessageThreads();
|
||||||
|
const { user, isStaff } = useAuth();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [pendingDelete, setPendingDelete] = useState<{ id: string; subject: string } | null>(null);
|
||||||
|
|
||||||
|
// Delete-for-everyone: removes the thread (RLS allows creator or admin/manager);
|
||||||
|
// cascade removes participants + messages, and realtime refetches the list.
|
||||||
|
const deleteThread = async (id: string) => {
|
||||||
|
const { error } = await (supabase as any).from("message_threads").delete().eq("id", id);
|
||||||
|
if (error) toast({ title: "Couldn't delete topic", description: error.message, variant: "destructive" });
|
||||||
|
else toast({ title: "Topic deleted" });
|
||||||
|
setPendingDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col border-r border-border">
|
<div className="flex h-full flex-col border-r border-border">
|
||||||
@@ -33,11 +53,11 @@ export default function TopicThreadList({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
threads.map((t) => (
|
threads.map((t) => (
|
||||||
|
<div key={t.id} className="group relative border-b border-border">
|
||||||
<button
|
<button
|
||||||
key={t.id}
|
|
||||||
onClick={() => onSelect(t.id)}
|
onClick={() => onSelect(t.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full text-left px-4 py-3 border-b border-border hover:bg-accent transition-colors",
|
"w-full text-left px-4 py-3 hover:bg-accent transition-colors",
|
||||||
selectedThreadId === t.id && "bg-accent",
|
selectedThreadId === t.id && "bg-accent",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -54,9 +74,40 @@ export default function TopicThreadList({
|
|||||||
<p className="text-[11px] text-muted-foreground/80 truncate mt-0.5">{t.participant_names.join(", ")}</p>
|
<p className="text-[11px] text-muted-foreground/80 truncate mt-0.5">{t.participant_names.join(", ")}</p>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{(t.created_by === user?.id || isStaff) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setPendingDelete({ id: t.id, subject: t.subject }); }}
|
||||||
|
title="Delete topic"
|
||||||
|
className="absolute right-2 top-2 hidden group-hover:flex h-6 w-6 items-center justify-center rounded-md bg-background/80 text-muted-foreground hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog open={!!pendingDelete} onOpenChange={(o) => { if (!o) setPendingDelete(null); }}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete this topic?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This permanently deletes “{pendingDelete?.subject}” and all its messages for everyone. This can't be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
onClick={() => pendingDelete && deleteThread(pendingDelete.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user