From e534644e5700917044db9c86c3075fbb383afecf Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 2 May 2026 16:31:53 +0200 Subject: [PATCH] feat(web): Message renders pending-bubble overlay + retry/discard chrome --- packages/web/src/components/chat/Message.tsx | 110 +++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/packages/web/src/components/chat/Message.tsx b/packages/web/src/components/chat/Message.tsx index ee85552c..2e802939 100644 --- a/packages/web/src/components/chat/Message.tsx +++ b/packages/web/src/components/chat/Message.tsx @@ -11,19 +11,55 @@ import { useChatStore } from '../../stores/chatStore'; import { useSpaceStore } from '../../stores/spaceStore'; import { useUIStore } from '../../stores/uiStore'; import { AttachmentRenderer } from './AttachmentRenderer'; +import { AttachmentProgress } from './AttachmentProgress'; import { EmbedRenderer } from './EmbedRenderer'; import { Username } from '../ui/Username'; import { EmojiPicker } from './EmojiPicker'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; import { isSelf, resolveDisplayIdentity } from '../../utils/identity'; +import { + isPendingMessage, + usePendingMessageStore, + type PendingMessageView, + type PendingAttachmentView, +} from '../../stores/pendingMessageStore'; +import { useTransferStore } from '../../stores/transferStore'; interface MessageProps { - message: MessageWithUser; + message: MessageWithUser | PendingMessageView; isCompact: boolean; isFirstInGroup: boolean; previousMessageId: string | null; } +interface PendingAttachmentTileProps { + transferId: string; +} + +function PendingAttachmentTile({ transferId }: PendingAttachmentTileProps) { + const transfer = useTransferStore((s) => s.transfers.get(transferId)); + const pauseUpload = useTransferStore((s) => s.pauseUpload); + const resumeUpload = useTransferStore((s) => s.resumeUpload); + const abortUpload = useTransferStore((s) => s.abortUpload); + if (!transfer) { + return
; + } + return ( +
+ pauseUpload(transfer.id) : undefined} + onResume={transfer.state === 'paused' ? () => resumeUpload(transfer.id) : undefined} + onAbort={() => abortUpload(transfer.id)} + size="tile" + /> +
+ ); +} + function formatTime(timestamp: number): string { const date = new Date(timestamp); const now = new Date(); @@ -93,11 +129,18 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId const members = useSpaceStore((s) => s.members); const openUserProfile = useUIStore((s) => s.openUserProfile); - const channelKey = message.channelId || (message as any).dmChannelId; + const pending = isPendingMessage(message) ? message.__pending : null; + const showInteractions = !pending; + + const channelKey: string = isPendingMessage(message) + ? message.channelId || message.dmChannelId || '' + : message.channelId || (message as MessageWithUser & { dmChannelId?: string }).dmChannelId || ''; const isAuthor = isSelf(message.user, currentUser); const channelPermissions = useSpaceStore((s) => s.channelPermissions); const myChPerms = channelPermissions.get(message.channelId); - const isDmMessage = !!(message as any).dmChannelId || !message.channelId; + const isDmMessage = isPendingMessage(message) + ? !!message.dmChannelId || !message.channelId + : !!(message as MessageWithUser & { dmChannelId?: string }).dmChannelId || !message.channelId; const canManageMessages = hasPermissionBit(myChPerms, PermissionBits.MANAGE_MESSAGES); const canSendMessages = isDmMessage || hasPermissionBit(myChPerms, PermissionBits.SEND_MESSAGES); const canDelete = isAuthor || canManageMessages; @@ -191,6 +234,11 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId }; const handleContextMenu = (e: React.MouseEvent) => { + if (pending) { + e.preventDefault(); + e.stopPropagation(); + return; + } e.preventDefault(); e.stopPropagation(); const selectedText = window.getSelection()?.toString() ?? ''; @@ -416,14 +464,58 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId {/* Attachments */} {message.attachments && message.attachments.length > 0 && (
- {message.attachments.map((att) => ( - - ))} + {message.attachments.map((att) => { + const pendingAtt = att as PendingAttachmentView; + if (pending && pendingAtt.__transferId) { + return ; + } + return ; + })} +
+ )} + + {/* Failed-state retry/discard row */} + {pending?.state === 'failed' && ( +
+ +
)} {/* Reactions */} - {Object.keys(reactionGroups).length > 0 && ( + {showInteractions && Object.keys(reactionGroups).length > 0 && (
{Object.entries(reactionGroups).map(([emoji, { count, me }]) => (