fix(room): prevent double-send, log resubscribe errors, dim pending messages

- sendMessage: guard with sendingRef to prevent concurrent in-flight
  sends (was missing — rapid clicks could create duplicate messages)
- resubscribeAll: log at warn level instead of silently swallowing,
  so operators can observe auth expiry or persistent failure patterns
- RoomMessageBubble: apply opacity-60 when isPending or isFailed,
  and hide action toolbar for pending messages (can't react/act on
  unconfirmed messages)
This commit is contained in:
ZhenYi 2026-04-16 19:29:34 +08:00
parent 677e88980b
commit 7416f37cec
3 changed files with 17 additions and 5 deletions

View File

@ -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 && (
<div className="flex items-start gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
{isNarrow ? (
/* Narrow: all actions in dropdown */

View File

@ -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],

View File

@ -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);
}
}
}