diff --git a/packages/web/src/components/chat/Message.tsx b/packages/web/src/components/chat/Message.tsx index bfd021d8..549782c2 100644 --- a/packages/web/src/components/chat/Message.tsx +++ b/packages/web/src/components/chat/Message.tsx @@ -173,10 +173,16 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId e.stopPropagation(); const selectedText = window.getSelection()?.toString() ?? ''; + // Detect if the right-click target is a content image (not an avatar) + const imgEl = (e.target as HTMLElement).closest('img') as HTMLImageElement | null; + const isContentImage = imgEl && !imgEl.closest('[data-avatar]'); + const imageUrl = isContentImage ? imgEl.src : null; + const items = buildMessageMenuItems({ message, selectedText, previousMessageId, + imageUrl, isAuthor, isDm: isDmMessage, canAddReactions, diff --git a/packages/web/src/components/chat/messageMenuItems.tsx b/packages/web/src/components/chat/messageMenuItems.tsx index 48842f22..d25ab4a9 100644 --- a/packages/web/src/components/chat/messageMenuItems.tsx +++ b/packages/web/src/components/chat/messageMenuItems.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type { ContextMenuItem } from '../../stores/contextMenuStore'; import type { MessageWithUser } from '@backspace/shared'; +import { saveImage, copyImageToClipboard } from '../../utils/imageActions'; const QUICK_EMOJIS = ['👍', '❤️', '😂', '😮']; @@ -19,6 +20,7 @@ interface MessageMenuParams { onReaction: (emoji: string) => void; onOpenEmojiPicker: () => void; onMarkUnread: (messageId: string) => void; + imageUrl?: string | null; } export function buildMessageMenuItems(params: MessageMenuParams): ContextMenuItem[] { @@ -37,10 +39,56 @@ export function buildMessageMenuItems(params: MessageMenuParams): ContextMenuIte onReaction, onOpenEmojiPicker, onMarkUnread, + imageUrl, } = params; const items: ContextMenuItem[] = []; + // ── Image Actions (when right-clicking an image) ────────────────────── + if (imageUrl) { + items.push({ + key: 'save-image', + type: 'action', + label: 'Save Image', + icon: ( + + + + ), + onClick: () => { + const filename = imageUrl.split('/').pop()?.split('?')[0] ?? 'image'; + saveImage(imageUrl, filename); + }, + }); + items.push({ + key: 'copy-image', + type: 'action', + label: 'Copy Image', + icon: ( + + + + ), + onClick: () => { + copyImageToClipboard(imageUrl); + }, + }); + items.push({ + key: 'open-original', + type: 'action', + label: 'Open Original', + icon: ( + + + + ), + onClick: () => { + window.open(imageUrl, '_blank', 'noopener'); + }, + }); + items.push({ key: 'image-sep', type: 'separator' }); + } + // ── Quick Reaction Row ────────────────────────────────────────────────── if (canAddReactions) { items.push({