fix: prevent duplicate message rendering in collaborative sessions

🔧 MESSAGE DEDUPLICATION: Eliminate double rendering when second user sends messages

## Root Cause Analysis:
Messages from other users were being processed by multiple event handlers:
1. Regular 'message' event handler
2. Goose message detection (heuristic)
3. Session message handler

This caused the same message to appear multiple times in the chat.

## Solution Implementation:

### Enhanced Message Classification:
✅ Added message type flags (isGooseMessage, isSessionMessage)
✅ Use else-if chain to ensure only one handler processes each message
✅ Prevent multiple event emissions for the same message

### Smart Event Routing:
✅ Regular messages: Only emit 'message' if not Goose/session message
✅ Goose messages: Emit 'gooseMessage' + special 'gooseSessionSync' for collaboration
✅ Session messages: Emit 'sessionMessage' for legacy session handling
✅ Structured logging to track which handler processes each message

### Enhanced Matrix Context:
✅ Updated onMessage to support both default and named event listeners
✅ Overloaded function signature for backward compatibility
✅ Support for 'gooseSessionSync' event in useSessionSharing

### Session Sharing Integration:
✅ Added listener for 'gooseSessionSync' events
✅ Proper cleanup for all event listeners
✅ Maintains existing functionality while preventing duplicates

## Technical Flow:
1. Message received → Classify as regular/Goose/session
2. Emit appropriate event(s) → Only one primary handler per message
3. UI renders once → No duplicate message components
4. Collaboration sync → Works via dedicated 'gooseSessionSync' event

## Benefits:
🎯 **No More Duplicates**: Each message renders exactly once
🔄 **Preserved Functionality**: All existing features still work
🦆 **Goose Communication**: Still detects and handles Goose messages
📝 **Session Sync**: Collaborative sessions work without duplicates
🧹 **Clean Logging**: Clear indication of message processing path

Now when a second user sends a message in a collaborative session,
it will appear exactly once with the correct sender avatar! 🦆✨
This commit is contained in:
spencrmartin
2025-11-14 11:19:36 -05:00
parent d9d7edcb2e
commit c3c031e4e4
3 changed files with 46 additions and 18 deletions
+13 -4
View File
@@ -38,7 +38,7 @@ interface MatrixContextType {
getOrCreateDirectMessageRoom: (userId: string) => Promise<string>;
// Events
onMessage: (callback: (data: any) => void) => () => void;
onMessage: ((callback: (data: any) => void) => () => void) & ((eventName: string, callback: (data: any) => void) => () => void);
onAIMessage: (callback: (message: GooseAIMessage) => void) => () => void;
onGooseMessage: (callback: (message: GooseChatMessage) => void) => () => void;
onSessionMessage: (callback: (data: any) => void) => () => void;
@@ -191,9 +191,18 @@ export const MatrixProvider: React.FC<MatrixProviderProps> = ({ children, matrix
await matrixService.setDisplayName(displayName);
};
const onMessage = (callback: (data: any) => void) => {
matrixService.on('message', callback);
return () => matrixService.off('message', callback);
const onMessage = (eventNameOrCallback: string | ((data: any) => void), callback?: (data: any) => void) => {
if (typeof eventNameOrCallback === 'string' && callback) {
// Support for specific event names like 'gooseSessionSync'
matrixService.on(eventNameOrCallback, callback);
return () => matrixService.off(eventNameOrCallback, callback);
} else if (typeof eventNameOrCallback === 'function') {
// Default behavior for 'message' event
matrixService.on('message', eventNameOrCallback);
return () => matrixService.off('message', eventNameOrCallback);
} else {
throw new Error('Invalid arguments for onMessage');
}
};
const onAIMessage = (callback: (message: GooseAIMessage) => void) => {
@@ -208,10 +208,12 @@ export const useSessionSharing = ({
const sessionCleanup = onSessionMessage(handleSessionMessage);
const messageCleanup = onMessage(handleRegularMessage);
const gooseSessionCleanup = onMessage('gooseSessionSync', handleRegularMessage);
return () => {
sessionCleanup();
messageCleanup();
gooseSessionCleanup();
};
}, [isConnected, sessionId, state.roomId, currentUser?.userId, onSessionMessage, onMessage, onMessageSync, onParticipantJoin]);
+31 -14
View File
@@ -495,8 +495,12 @@ export class MatrixService extends EventEmitter {
senderInfo,
};
let isGooseMessage = false;
let isSessionMessage = false;
// Check if this is a structured Goose message (new format)
if (content['goose.message.type']) {
isGooseMessage = true;
const gooseChatMessage: GooseChatMessage = {
type: content['goose.message.type'] as any,
messageId: content['goose.message.id'] || this.generateMessageId(),
@@ -517,14 +521,13 @@ export class MatrixService extends EventEmitter {
},
};
console.log('🦆 Received Goose message:', gooseChatMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)');
console.log('🦆 Received structured Goose message:', gooseChatMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)');
this.emit('gooseMessage', gooseChatMessage);
// Don't return early - let it also be processed as regular message
}
// Check if this is a legacy Goose AI message (using custom properties)
if (content['goose.type']) {
else if (content['goose.type']) {
isGooseMessage = true;
const aiMessage: GooseAIMessage = {
type: `ai.${content['goose.type']}` as any,
sessionId: content['goose.session_id'] || room.roomId,
@@ -535,14 +538,13 @@ export class MatrixService extends EventEmitter {
metadata: { ...content, isFromSelf },
};
console.log('🦆 Received legacy Goose AI message:', aiMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)');
this.emit('aiMessage', aiMessage);
// Don't return early for legacy messages either
}
// Check if sender appears to be a Goose instance (heuristic detection) - but not ourselves
const senderUser = this.client?.getUser(sender);
if (!isFromSelf && this.isGooseInstance(sender, senderUser?.displayName)) {
else if (!isFromSelf && this.isGooseInstance(sender, senderUser?.displayName)) {
isGooseMessage = true;
// Treat as potential Goose message even without explicit markers
const gooseChatMessage: GooseChatMessage = {
type: 'goose.chat',
@@ -558,20 +560,35 @@ export class MatrixService extends EventEmitter {
this.emit('gooseMessage', gooseChatMessage);
}
// Always emit regular message event (but skip our own messages to avoid echo in regular chat)
if (!isFromSelf) {
this.emit('message', messageData);
}
// Also check if this is a session-related message and emit as session message
// Check if this is a session-related message
if (content.body && !isFromSelf) {
// Check for Goose session messages (from useSessionSharing)
if (content.body.includes('goose-session-message:') ||
content.body.includes('goose-session-invite:') ||
content.body.includes('goose-session-joined:')) {
isSessionMessage = true;
console.log('📝 Received session message from:', sender);
this.emit('sessionMessage', messageData);
}
}
// Only emit regular message event if it's not already handled as a Goose message or session message
// This prevents duplicate rendering
if (!isFromSelf && !isGooseMessage && !isSessionMessage) {
console.log('💬 Received regular message from:', sender);
this.emit('message', messageData);
}
// However, always emit for session messages in collaborative sessions
// (even if they're from Goose instances) to ensure proper synchronization
if (isSessionMessage) {
// Already emitted above
} else if (!isFromSelf && isGooseMessage) {
// For Goose messages that might also need to be treated as regular messages in collaborative sessions
// We'll emit a special event that useSessionSharing can listen to
console.log('🔄 Emitting Goose message as potential session sync from:', sender);
this.emit('gooseSessionSync', messageData);
}
}
/**