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',
|
||||
!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 */
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user