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 }]) => (