diff --git a/src/components/messaging/ConversationList.tsx b/src/components/messaging/ConversationList.tsx
index 668464c..61182ca 100644
--- a/src/components/messaging/ConversationList.tsx
+++ b/src/components/messaging/ConversationList.tsx
@@ -7,12 +7,17 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
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 { cn } from "@/lib/utils";
+import { useToast } from "@/hooks/use-toast";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog";
+import {
+ AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
+ AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
interface Props {
selectedPartnerId: string | null;
@@ -24,9 +29,11 @@ function getInitials(name: string) {
}
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 { toast } = useToast();
const [search, setSearch] = useState("");
+ const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null);
const useStaffConversation = isHomeowner || isRvBoatLot;
const isBoardOnly = isBoardMember && !isStaff;
@@ -34,6 +41,23 @@ export default function ConversationList({ selectedPartnerId, onSelectPartner }:
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 (
@@ -107,8 +131,8 @@ export default function ConversationList({ selectedPartnerId, onSelectPartner }:
) : (
{filtered.map((conv) => (
+
+
+
))}
)}
+
+ { if (!o) setPendingDelete(null); }}>
+
+
+ Delete this conversation?
+
+ This permanently deletes the conversation with {pendingDelete?.name} for everyone. This can't be undone.
+
+
+
+ Cancel
+ pendingDelete && deleteConversation(pendingDelete.id)}
+ >
+ Delete
+
+
+
+
);
}
@@ -205,7 +258,7 @@ function NewConversationDialog({ onSelectPartner }: { onSelectPartner: (id: stri
.neq("status", "archived"),
(supabase as any)
.from("board_members")
- .select("user_id, role, associations(name)")
+ .select("user_id, role, member_name, associations(name)")
.not("user_id", "is", null),
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);
recipients.push({
user_id: b.user_id,
- full_name: null,
+ full_name: b.member_name || null,
avatar_url: null,
category: "board",
context: `Board • ${b.role || "Member"}${b.associations?.name ? ` (${b.associations.name})` : ""}`,
@@ -313,11 +366,11 @@ function NewConversationDialog({ onSelectPartner }: { onSelectPartner: (id: stri
-
+
New Message
-
+
([]);
+ const [boardGroups, setBoardGroups] = useState<{ id: string; name: string; memberIds: string[] }[]>([]);
const [loadingRecipients, setLoadingRecipients] = useState(false);
const [topic, setTopic] = useState
("");
const [subject, setSubject] = useState("");
@@ -53,13 +54,30 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
const list: Recipient[] = [];
const seen = new Set();
- 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}` : ""}`,
- });
+
+ if (isStaff) {
+ // Staff message whole boards: group board members by association so an
+ // entire board can be added in one click.
+ const groups = new Map();
+ 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) {
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 () => {
if (!topic) return toast({ title: "Pick a topic", variant: "destructive" });
if (!subject.trim()) return toast({ title: "Add a subject", variant: "destructive" });
@@ -132,9 +159,9 @@ export default function NewTopicThreadDialog({ onCreated }: { onCreated: (thread
-
+
New Topic
-
+
diff --git a/src/components/messaging/TopicThreadList.tsx b/src/components/messaging/TopicThreadList.tsx
index 901f549..1be34f8 100644
--- a/src/components/messaging/TopicThreadList.tsx
+++ b/src/components/messaging/TopicThreadList.tsx
@@ -1,10 +1,18 @@
+import { useState } from "react";
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 { Badge } from "@/components/ui/badge";
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 NewTopicThreadDialog from "./NewTopicThreadDialog";
+import {
+ AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
+ AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
function when(dateStr: string | null) {
if (!dateStr) return "";
@@ -16,6 +24,18 @@ export default function TopicThreadList({
selectedThreadId, onSelect,
}: { selectedThreadId: string | null; onSelect: (id: string) => void }) {
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 (
@@ -33,11 +53,11 @@ export default function TopicThreadList({
) : (
threads.map((t) => (
+
+ {(t.created_by === user?.id || isStaff) && (
+
+ )}
+
))
)}
+
+
{ if (!o) setPendingDelete(null); }}>
+
+
+ Delete this topic?
+
+ This permanently deletes “{pendingDelete?.subject}” and all its messages for everyone. This can't be undone.
+
+
+
+ Cancel
+ pendingDelete && deleteThread(pendingDelete.id)}
+ >
+ Delete
+
+
+
+
);
}