mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat: add delete message support for stream messages (#215)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -23,6 +23,7 @@ type ChannelPaneProps = {
|
||||
messages: TimelineMessage[];
|
||||
onCancelEdit?: () => void;
|
||||
onCancelReply: () => void;
|
||||
onDelete?: (message: TimelineMessage) => void;
|
||||
onEdit?: (message: TimelineMessage) => void;
|
||||
onEditSave?: (content: string) => Promise<void>;
|
||||
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}
|
||||
|
||||
@@ -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<typeof useDeleteMessageMutation>;
|
||||
editMessageMutation: ReturnType<typeof useEditMessageMutation>;
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void, Error, { eventId: string }>({
|
||||
mutationFn: async ({ eventId }) => {
|
||||
await deleteMessage(eventId);
|
||||
},
|
||||
onSuccess: (_data, { eventId }) => {
|
||||
if (!channel) return;
|
||||
queryClient.setQueryData<RelayEvent[]>(
|
||||
channelMessagesKey(channel.id),
|
||||
(current = []) => current.filter((message) => message.id !== eventId),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useEditMessageMutation(channel: Channel | null) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -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<void>;
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-28 overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-all duration-150 ease-out",
|
||||
"max-w-36 overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-all duration-150 ease-out",
|
||||
"translate-y-0 opacity-100 sm:max-w-0 sm:translate-y-1 sm:opacity-0",
|
||||
"sm:group-hover/message:max-w-28 sm:group-hover/message:translate-y-0 sm:group-hover/message:opacity-100",
|
||||
"sm:group-focus-within/message:max-w-28 sm:group-focus-within/message:translate-y-0 sm:group-focus-within/message:opacity-100",
|
||||
"sm:group-hover/message:max-w-36 sm:group-hover/message:translate-y-0 sm:group-hover/message:opacity-100",
|
||||
"sm:group-focus-within/message:max-w-36 sm:group-focus-within/message:translate-y-0 sm:group-focus-within/message:opacity-100",
|
||||
isReplyingToMessage || isReactionPickerOpen
|
||||
? "sm:max-w-28 sm:translate-y-0 sm:opacity-100"
|
||||
? "sm:max-w-36 sm:translate-y-0 sm:opacity-100"
|
||||
: "",
|
||||
)}
|
||||
data-testid={`message-action-bar-${message.id}`}
|
||||
@@ -141,6 +166,56 @@ export function MessageActionBar({
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{hasDeleteAction ? (
|
||||
<>
|
||||
<Button
|
||||
aria-label="Delete"
|
||||
className="h-6 w-6 rounded-full p-0"
|
||||
data-testid={`delete-message-${message.id}`}
|
||||
onClick={() => {
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
size="sm"
|
||||
title="Delete"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
<AlertDialog
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
open={isDeleteDialogOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete message?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete this message and cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button
|
||||
onClick={() => onDelete?.(message)}
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{hasReplyAction ? (
|
||||
<Button
|
||||
aria-label={isReplyingToMessage ? "Cancel reply" : "Reply"}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const MessageRow = React.memo(
|
||||
activeReplyTargetId = null,
|
||||
highlighted = false,
|
||||
message,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onToggleReaction,
|
||||
onReply,
|
||||
@@ -28,6 +29,7 @@ export const MessageRow = React.memo(
|
||||
activeReplyTargetId?: string | null;
|
||||
highlighted?: boolean;
|
||||
message: TimelineMessage;
|
||||
onDelete?: (message: TimelineMessage) => void;
|
||||
onEdit?: (message: TimelineMessage) => void;
|
||||
onToggleReaction?: (
|
||||
message: TimelineMessage,
|
||||
@@ -243,6 +245,7 @@ export const MessageRow = React.memo(
|
||||
<MessageActionBar
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
message={message}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onReactionSelect={
|
||||
canToggleReactions ? handleReactionSelect : undefined
|
||||
|
||||
@@ -24,6 +24,7 @@ type MessageTimelineProps = {
|
||||
hasOlderMessages?: boolean;
|
||||
isFetchingOlder?: boolean;
|
||||
profiles?: UserProfileLookup;
|
||||
onDelete?: (message: TimelineMessage) => void;
|
||||
onEdit?: (message: TimelineMessage) => void;
|
||||
onReply?: (message: TimelineMessage) => void;
|
||||
onToggleReaction?: (
|
||||
@@ -47,6 +48,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
hasOlderMessages = true,
|
||||
isFetchingOlder = false,
|
||||
profiles,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onReply,
|
||||
onToggleReaction,
|
||||
@@ -151,6 +153,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
currentPubkey={currentPubkey}
|
||||
highlightedMessageId={highlightedMessageId}
|
||||
messages={messages}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onReply={onReply}
|
||||
onToggleReaction={onToggleReaction}
|
||||
|
||||
@@ -16,6 +16,7 @@ type TimelineMessageListProps = {
|
||||
currentPubkey?: string;
|
||||
highlightedMessageId?: string | null;
|
||||
messages: TimelineMessage[];
|
||||
onDelete?: (message: TimelineMessage) => void;
|
||||
onEdit?: (message: TimelineMessage) => void;
|
||||
onReply?: (message: TimelineMessage) => void;
|
||||
onToggleReaction?: (
|
||||
@@ -31,6 +32,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
currentPubkey,
|
||||
highlightedMessageId = null,
|
||||
messages,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onReply,
|
||||
onToggleReaction,
|
||||
@@ -69,6 +71,11 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
highlighted={message.id === highlightedMessageId}
|
||||
message={message}
|
||||
onDelete={
|
||||
onDelete && currentPubkey && message.pubkey === currentPubkey
|
||||
? onDelete
|
||||
: undefined
|
||||
}
|
||||
onEdit={
|
||||
onEdit && currentPubkey && message.pubkey === currentPubkey
|
||||
? onEdit
|
||||
|
||||
@@ -96,10 +96,6 @@ function fromRawThreadReply(reply: RawThreadReply): ThreadReply {
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteMessage(eventId: string): Promise<void> {
|
||||
await invokeTauri("delete_message", { eventId });
|
||||
}
|
||||
|
||||
export async function getForumPosts(
|
||||
channelId: string,
|
||||
limit?: number,
|
||||
|
||||
@@ -745,6 +745,10 @@ export async function editMessage(
|
||||
await invokeTauri("edit_message", { channelId, eventId, content });
|
||||
}
|
||||
|
||||
export async function deleteMessage(eventId: string): Promise<void> {
|
||||
await invokeTauri("delete_message", { eventId });
|
||||
}
|
||||
|
||||
export async function addReaction(
|
||||
eventId: string,
|
||||
emoji: string,
|
||||
|
||||
Reference in New Issue
Block a user