diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index d54b87214..66f379b1f 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -22,6 +22,7 @@ import { HomeView } from "@/features/home/ui/HomeView"; import { useChannelMessagesQuery, mergeMessages, + useDeleteMessageMutation, useEditMessageMutation, useSendMessageMutation, useChannelSubscription, @@ -140,6 +141,7 @@ export function AppShell() { identityQuery.data, ); const toggleReactionMutation = useToggleReactionMutation(); + const deleteMessageMutation = useDeleteMessageMutation(activeChannel); const editMessageMutation = useEditMessageMutation(activeChannel); const availableChannelIds = React.useMemo( () => new Set(channels.map((channel) => channel.id)), @@ -222,12 +224,14 @@ export function AppShell() { const { handleCancelEdit, handleCancelReply, + handleDelete, handleEdit, handleEditSave, handleReply, handleSend, handleToggleReaction, } = useChannelPaneHandlers({ + deleteMessageMutation, editMessageMutation, editTargetId, replyTargetId, @@ -424,7 +428,6 @@ export function AppShell() { if (previousActiveChannelIdRef.current === activeChannelId) { return; } - previousActiveChannelIdRef.current = activeChannelId; setReplyTargetId(null); requestedAncestorIdsRef.current.clear(); @@ -440,7 +443,10 @@ export function AppShell() { if (replyTargetId && !replyTargetMessage) { setReplyTargetId(null); } - }, [replyTargetId, replyTargetMessage]); + if (editTargetId && !editTargetMessage) { + setEditTargetId(null); + } + }, [editTargetId, editTargetMessage, replyTargetId, replyTargetMessage]); React.useEffect(() => { if (!activeChannel || activeChannel.channelType === "forum") { return; @@ -753,6 +759,7 @@ export function AppShell() { messages={timelineMessages} onCancelEdit={handleCancelEdit} onCancelReply={handleCancelReply} + onDelete={handleDelete} onEdit={handleEdit} onEditSave={handleEditSave} onReply={handleReply} diff --git a/desktop/src/app/ChannelPane.tsx b/desktop/src/app/ChannelPane.tsx index 25c2cfd0d..4c90a8c73 100644 --- a/desktop/src/app/ChannelPane.tsx +++ b/desktop/src/app/ChannelPane.tsx @@ -23,6 +23,7 @@ type ChannelPaneProps = { messages: TimelineMessage[]; onCancelEdit?: () => void; onCancelReply: () => void; + onDelete?: (message: TimelineMessage) => void; onEdit?: (message: TimelineMessage) => void; onEditSave?: (content: string) => Promise; onReply: (message: TimelineMessage) => void; @@ -56,6 +57,7 @@ export const ChannelPane = React.memo(function ChannelPane({ messages, onCancelEdit, onCancelReply, + onDelete, onEdit, onEditSave, onReply, @@ -92,6 +94,7 @@ export const ChannelPane = React.memo(function ChannelPane({ } isLoading={isTimelineLoading} messages={messages} + onDelete={onDelete} onEdit={onEdit} onReply={onReply} onTargetReached={onTargetReached} diff --git a/desktop/src/app/useChannelPaneHandlers.ts b/desktop/src/app/useChannelPaneHandlers.ts index 832272e06..0dee12c46 100644 --- a/desktop/src/app/useChannelPaneHandlers.ts +++ b/desktop/src/app/useChannelPaneHandlers.ts @@ -1,6 +1,7 @@ import * as React from "react"; import type { + useDeleteMessageMutation, useEditMessageMutation, useSendMessageMutation, useToggleReactionMutation, @@ -15,6 +16,7 @@ import type { * rather than listing the whole mutation as a dependency. */ export function useChannelPaneHandlers({ + deleteMessageMutation, editMessageMutation, editTargetId, replyTargetId, @@ -23,6 +25,7 @@ export function useChannelPaneHandlers({ setReplyTargetId, toggleReactionMutation, }: { + deleteMessageMutation: ReturnType; editMessageMutation: ReturnType; editTargetId: string | null; replyTargetId: string | null; @@ -41,6 +44,9 @@ export function useChannelPaneHandlers({ const sendMutateRef = React.useRef(sendMessageMutation.mutateAsync); sendMutateRef.current = sendMessageMutation.mutateAsync; + const deleteMutateRef = React.useRef(deleteMessageMutation.mutateAsync); + deleteMutateRef.current = deleteMessageMutation.mutateAsync; + const editMutateRef = React.useRef(editMessageMutation.mutateAsync); editMutateRef.current = editMessageMutation.mutateAsync; @@ -55,6 +61,10 @@ export function useChannelPaneHandlers({ setEditTargetId(null); }, [setEditTargetId]); + const handleDelete = React.useCallback(async (message: { id: string }) => { + await deleteMutateRef.current({ eventId: message.id }); + }, []); + const handleEdit = React.useCallback( (message: { id: string }) => { setEditTargetId((current) => @@ -121,6 +131,7 @@ export function useChannelPaneHandlers({ return { handleCancelEdit, handleCancelReply, + handleDelete, handleEdit, handleEditSave, handleReply, diff --git a/desktop/src/features/forum/hooks.ts b/desktop/src/features/forum/hooks.ts index 5e9501bd4..d70c906f8 100644 --- a/desktop/src/features/forum/hooks.ts +++ b/desktop/src/features/forum/hooks.ts @@ -1,11 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - deleteMessage, - getForumPosts, - getForumThread, -} from "@/shared/api/forum"; -import { sendChannelMessage } from "@/shared/api/tauri"; +import { getForumPosts, getForumThread } from "@/shared/api/forum"; +import { deleteMessage, sendChannelMessage } from "@/shared/api/tauri"; import type { Channel, ForumPostsResponse, diff --git a/desktop/src/features/messages/hooks.ts b/desktop/src/features/messages/hooks.ts index 1d70b44d7..314d0b12d 100644 --- a/desktop/src/features/messages/hooks.ts +++ b/desktop/src/features/messages/hooks.ts @@ -16,6 +16,7 @@ import { import { relayClient } from "@/shared/api/relayClient"; import { addReaction, + deleteMessage, editMessage, removeReaction, sendChannelMessage, @@ -421,6 +422,23 @@ export function useToggleReactionMutation() { }); } +export function useDeleteMessageMutation(channel: Channel | null) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ eventId }) => { + await deleteMessage(eventId); + }, + onSuccess: (_data, { eventId }) => { + if (!channel) return; + queryClient.setQueryData( + channelMessagesKey(channel.id), + (current = []) => current.filter((message) => message.id !== eventId), + ); + }, + }); +} + export function useEditMessageMutation(channel: Channel | null) { const queryClient = useQueryClient(); diff --git a/desktop/src/features/messages/ui/MessageActionBar.tsx b/desktop/src/features/messages/ui/MessageActionBar.tsx index d6aea4215..295417a11 100644 --- a/desktop/src/features/messages/ui/MessageActionBar.tsx +++ b/desktop/src/features/messages/ui/MessageActionBar.tsx @@ -1,6 +1,12 @@ import Picker from "@emoji-mart/react"; import data from "@emoji-mart/data"; -import { CornerUpLeft, LoaderCircle, Pencil, SmilePlus } from "lucide-react"; +import { + CornerUpLeft, + LoaderCircle, + Pencil, + SmilePlus, + Trash2, +} from "lucide-react"; import * as React from "react"; import type { @@ -8,12 +14,23 @@ import type { TimelineReaction, } from "@/features/messages/types"; import { cn } from "@/shared/lib/cn"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/shared/ui/alert-dialog"; import { Button } from "@/shared/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; export function MessageActionBar({ activeReplyTargetId = null, message, + onDelete, onEdit, onReactionSelect, onReply, @@ -23,6 +40,7 @@ export function MessageActionBar({ }: { activeReplyTargetId?: string | null; message: TimelineMessage; + onDelete?: (message: TimelineMessage) => void; onEdit?: (message: TimelineMessage) => void; onReactionSelect?: (emoji: string) => Promise; onReply?: (message: TimelineMessage) => void; @@ -31,11 +49,18 @@ export function MessageActionBar({ reactionPending?: boolean; }) { const [isReactionPickerOpen, setIsReactionPickerOpen] = React.useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); + const hasDeleteAction = Boolean(onDelete); const hasEditAction = Boolean(onEdit); const hasReplyAction = Boolean(onReply); const hasReactionAction = Boolean(onReactionSelect); - if (!hasReplyAction && !hasReactionAction && !hasEditAction) { + if ( + !hasReplyAction && + !hasReactionAction && + !hasEditAction && + !hasDeleteAction + ) { return null; } @@ -47,12 +72,12 @@ export function MessageActionBar({ return (
) : null} + {hasDeleteAction ? ( + <> + + + + + + Delete message? + + This will permanently delete this message and cannot be + undone. + + + + + + + + + + + + + + ) : null} + {hasReplyAction ? (