diff --git a/src/components/room/RoomMessageBubble.tsx b/src/components/room/RoomMessageBubble.tsx
index 6e06011..42b22ad 100644
--- a/src/components/room/RoomMessageBubble.tsx
+++ b/src/components/room/RoomMessageBubble.tsx
@@ -215,6 +215,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
grouped ? 'py-0.5' : 'py-2',
!isSystem && 'hover:bg-muted/30',
isSystem && 'border-l-2 border-amber-500/60 bg-amber-500/5',
+ (isPending || isFailed) && 'opacity-60',
)}
>
{/* Avatar */}
@@ -416,7 +417,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
)}
{/* Action toolbar - inline icons when wide, collapsed to dropdown when narrow */}
- {!isEditing && !isRevoked && (
+ {!isEditing && !isRevoked && !isPending && (
{isNarrow ? (
/* Narrow: all actions in dropdown */
diff --git a/src/contexts/room-context.tsx b/src/contexts/room-context.tsx
index 4b7f552..9322d36 100644
--- a/src/contexts/room-context.tsx
+++ b/src/contexts/room-context.tsx
@@ -784,10 +784,17 @@ export function RoomProvider({
[activeRoomId],
);
+ // Guard against double-sending while a previous send is in-flight.
+ // Without this, rapid clicking can queue multiple optimistic messages and
+ // create duplicate sends on the server.
+ const sendingRef = useRef(false);
+
const sendMessage = useCallback(
async (content: string, contentType = 'text', inReplyTo?: string) => {
const client = wsClientRef.current;
if (!activeRoomId || !client) return;
+ if (sendingRef.current) return;
+ sendingRef.current = true;
// Optimistic update: add message immediately so user sees it instantly
const optimisticId = `optimistic-${crypto.randomUUID()}`;
@@ -843,6 +850,8 @@ export function RoomProvider({
),
);
handleRoomError('Send message', err);
+ } finally {
+ sendingRef.current = false;
}
},
[activeRoomId, user],
diff --git a/src/lib/room-ws-client.ts b/src/lib/room-ws-client.ts
index 67b41e8..9ba0476 100644
--- a/src/lib/room-ws-client.ts
+++ b/src/lib/room-ws-client.ts
@@ -916,15 +916,17 @@ export class RoomWsClient {
for (const roomId of this.subscribedRooms) {
try {
await this.request('room.subscribe', { room_id: roomId });
- } catch {
- // ignore
+ } catch (err) {
+ // Resubscribe failure is non-fatal — messages still arrive via REST poll.
+ // Log at warn level so operators can observe patterns (e.g. auth expiry).
+ console.warn(`[RoomWs] resubscribe room failed (will retry on next reconnect): ${roomId}`, err);
}
}
for (const projectName of this.subscribedProjects) {
try {
await this.request('project.subscribe', { project_name: projectName });
- } catch {
- // ignore
+ } catch (err) {
+ console.warn(`[RoomWs] resubscribe project failed (will retry on next reconnect): ${projectName}`, err);
}
}
}