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:
parent
677e88980b
commit
7416f37cec
@ -215,6 +215,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
|||||||
grouped ? 'py-0.5' : 'py-2',
|
grouped ? 'py-0.5' : 'py-2',
|
||||||
!isSystem && 'hover:bg-muted/30',
|
!isSystem && 'hover:bg-muted/30',
|
||||||
isSystem && 'border-l-2 border-amber-500/60 bg-amber-500/5',
|
isSystem && 'border-l-2 border-amber-500/60 bg-amber-500/5',
|
||||||
|
(isPending || isFailed) && 'opacity-60',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
@ -416,7 +417,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action toolbar - inline icons when wide, collapsed to dropdown when narrow */}
|
{/* 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">
|
<div className="flex items-start gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
{isNarrow ? (
|
{isNarrow ? (
|
||||||
/* Narrow: all actions in dropdown */
|
/* Narrow: all actions in dropdown */
|
||||||
|
|||||||
@ -784,10 +784,17 @@ export function RoomProvider({
|
|||||||
[activeRoomId],
|
[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(
|
const sendMessage = useCallback(
|
||||||
async (content: string, contentType = 'text', inReplyTo?: string) => {
|
async (content: string, contentType = 'text', inReplyTo?: string) => {
|
||||||
const client = wsClientRef.current;
|
const client = wsClientRef.current;
|
||||||
if (!activeRoomId || !client) return;
|
if (!activeRoomId || !client) return;
|
||||||
|
if (sendingRef.current) return;
|
||||||
|
sendingRef.current = true;
|
||||||
|
|
||||||
// Optimistic update: add message immediately so user sees it instantly
|
// Optimistic update: add message immediately so user sees it instantly
|
||||||
const optimisticId = `optimistic-${crypto.randomUUID()}`;
|
const optimisticId = `optimistic-${crypto.randomUUID()}`;
|
||||||
@ -843,6 +850,8 @@ export function RoomProvider({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
handleRoomError('Send message', err);
|
handleRoomError('Send message', err);
|
||||||
|
} finally {
|
||||||
|
sendingRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeRoomId, user],
|
[activeRoomId, user],
|
||||||
|
|||||||
@ -916,15 +916,17 @@ export class RoomWsClient {
|
|||||||
for (const roomId of this.subscribedRooms) {
|
for (const roomId of this.subscribedRooms) {
|
||||||
try {
|
try {
|
||||||
await this.request('room.subscribe', { room_id: roomId });
|
await this.request('room.subscribe', { room_id: roomId });
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore
|
// 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) {
|
for (const projectName of this.subscribedProjects) {
|
||||||
try {
|
try {
|
||||||
await this.request('project.subscribe', { project_name: projectName });
|
await this.request('project.subscribe', { project_name: projectName });
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore
|
console.warn(`[RoomWs] resubscribe project failed (will retry on next reconnect): ${projectName}`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user