Skip to content

Commit ada990d

Browse files
committed
Fix Matrix chat message display and React hook violations
- Fixed sender messages not appearing in BaseChat by ensuring all messages emit display events - Resolved duplicate Matrix invitation notifications by introducing dedicated matrixRoomInvitation event - Fixed persistent TypeError: Cannot read properties of undefined (reading 'length') errors: * Added robust null checking for messages.length, commandHistory.length, friends.length * Fixed lastMessage.content array/string handling in AI response sync * Added safe sessionId.substring access with null checks - Fixed React Rules of Hooks violations by moving conditional returns after all hook calls - Fixed useEffect dependency array size changes with consistent null fallbacks - Updated notification theming to use CSS variables for dark mode compatibility - Added message deduplication in useSessionSharing to prevent duplicate processing - Enhanced AI response sync to only sync locally generated responses (not Matrix messages) All TypeErrors and React warnings resolved. Matrix chat now properly displays sender messages and handles notifications without duplication.
1 parent 5577a15 commit ada990d

File tree

4 files changed

+379
-121
lines changed

4 files changed

+379
-121
lines changed

ui/desktop/src/components/ChatInput.tsx

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ import { useSessionSharing } from '../hooks/useSessionSharing';
3939
import SessionSharing from './collaborative/SessionSharing';
4040
import { CollaborativeButton } from './collaborative';
4141
import EnhancedMentionPopover from './EnhancedMentionPopover';
42+
import { useMatrix } from '../contexts/MatrixContext';
4243

44+
// Force rebuild timestamp: 2025-01-15T01:00:00Z - All .length errors fixed
4345

4446
interface QueuedMessage {
4547
id: string;
@@ -352,54 +354,66 @@ export default function ChatInput({
352354
const [isAddCommandModalOpen, setIsAddCommandModalOpen] = useState(false);
353355
const [customCommands, setCustomCommands] = useState<CustomCommand[]>([]);
354356

355-
// Check if we're in Matrix mode by looking at URL parameters
356-
const isInMatrixMode = window.location.search.includes('matrixMode=true');
357+
// Simple Matrix detection: if sessionId looks like a Matrix room ID, it's a Matrix chat
358+
const isMatrixRoom = sessionId && sessionId.startsWith('!');
357359

358-
// Extract Matrix room ID from URL parameters if in Matrix mode
359-
const urlParams = new URLSearchParams(window.location.search);
360-
const matrixRoomId = urlParams.get('matrixRoomId');
360+
// Get Matrix context for current user information
361+
const { currentUser } = useMatrix();
361362

362-
// Session sharing hook - use Matrix room ID as sessionId in Matrix mode
363+
// Session sharing hook - simplified approach for Matrix rooms
363364
const sessionSharing = useSessionSharing({
364-
sessionId: isInMatrixMode && matrixRoomId ? matrixRoomId : (sessionId || 'default'),
365-
sessionTitle: isInMatrixMode && matrixRoomId ? `Matrix Room ${matrixRoomId.substring(0, 8)}` : `Chat Session ${sessionId?.substring(0, 8) || 'Default'}`,
365+
sessionId: sessionId, // Use the sessionId as-is (Matrix room ID or regular session ID)
366+
sessionTitle: isMatrixRoom && sessionId ? `Matrix Room ${sessionId.substring(0, 8)}` : `Chat Session ${sessionId?.substring(0, 8) || 'default'}`,
366367
messages: messages, // Always sync messages
367368
onMessageSync: (message) => {
368-
// Handle synced messages from session participants
369-
console.log('💬 Synced message from shared session:', message);
370-
// Add the synced message to local chat
369+
console.log('💬 ChatInput: Received message from useSessionSharing:', message);
370+
371+
// For Matrix rooms, messages from Matrix should appear normally
372+
// For regular sessions, messages should also appear normally
373+
// The key is that useSessionSharing handles both cases the same way
371374
if (append) {
372375
append(message);
376+
} else {
377+
console.warn('⚠️ ChatInput: append function is not available!');
373378
}
374379
},
375-
initialRoomId: isInMatrixMode && matrixRoomId ? matrixRoomId : undefined, // Pass Matrix room ID for Matrix mode
380+
initialRoomId: isMatrixRoom ? sessionId : null, // Pass Matrix room ID if it's a Matrix room
376381
onParticipantJoin: (participant) => {
377-
if (!isInMatrixMode) {
378-
console.log('👥 Participant joined session:', participant);
379-
}
382+
console.log('👥 Participant joined session:', participant);
380383
},
381384
onParticipantLeave: (userId) => {
382-
if (!isInMatrixMode) {
383-
console.log('👋 Participant left session:', userId);
384-
}
385+
console.log('👋 Participant left session:', userId);
385386
},
386387
});
387388

388389
// Listen for AI responses to sync to Matrix
390+
// FIXED: Robust null checking to prevent "Cannot read properties of undefined (reading 'length')" error
391+
// Updated: Fixed all commandHistory.length accesses with safeCommandHistory
392+
// Final fix: All .length accesses now properly null-checked
389393
useEffect(() => {
390-
if (!sessionSharing.isSessionActive || !messages.length) return;
394+
if (!sessionSharing.isSessionActive || !messages || !Array.isArray(messages) || messages.length === 0) return;
391395

392396
const lastMessage = messages[messages.length - 1];
393397

394398
// Check if the last message is an AI response (assistant role) and not already synced
395-
if (lastMessage && lastMessage.role === 'assistant' && !lastMessage.id?.startsWith('shared-') && !lastMessage.id?.startsWith('matrix-')) {
399+
// Also check if it's not from Matrix (to prevent sync loops)
400+
if (lastMessage &&
401+
lastMessage.role === 'assistant' &&
402+
!lastMessage.id?.startsWith('shared-') &&
403+
!lastMessage.id?.startsWith('matrix-') &&
404+
!lastMessage.sender) { // Messages from Matrix have sender info, local AI responses don't
396405
console.log('🤖 Syncing AI response to session:', lastMessage);
397406

398-
// Extract text content from the message
399-
const textContent = lastMessage.content
400-
.filter(c => c.type === 'text')
401-
.map(c => c.text)
402-
.join('');
407+
// Extract text content from the message - with robust null checking
408+
let textContent = '';
409+
if (lastMessage.content && Array.isArray(lastMessage.content)) {
410+
textContent = lastMessage.content
411+
.filter(c => c && c.type === 'text')
412+
.map(c => c.text || '')
413+
.join('');
414+
} else if (typeof lastMessage.content === 'string') {
415+
textContent = lastMessage.content;
416+
}
403417

404418
if (textContent.trim()) {
405419
sessionSharing.syncMessage({
@@ -476,14 +490,14 @@ export default function ChatInput({
476490
// Handle recipe prompt updates
477491
useEffect(() => {
478492
// If recipe is accepted and we have an initial prompt, and no messages yet, and we haven't set it before
479-
if (recipeAccepted && initialPrompt && messages.length === 0) {
493+
if (recipeAccepted && initialPrompt && messages && Array.isArray(messages) && messages.length === 0) {
480494
setDisplayValue(initialPrompt);
481495
setValue(initialPrompt);
482496
setTimeout(() => {
483497
textAreaRef.current?.focus();
484498
}, 0);
485499
}
486-
}, [recipeAccepted, initialPrompt, messages.length]);
500+
}, [recipeAccepted, initialPrompt, messages]);
487501

488502
// Draft functionality - load draft if no initial value or recipe
489503
useEffect(() => {
@@ -1060,11 +1074,14 @@ export default function ChatInput({
10601074
// Save current input if we're just starting to navigate history
10611075
if (historyIndex === -1) {
10621076
setSavedInput(displayValue || '');
1063-
setIsInGlobalHistory(commandHistory.length === 0);
1077+
// Determine which history we're using - ensure commandHistory is always an array
1078+
const safeCommandHistory = commandHistory || [];
1079+
setIsInGlobalHistory(safeCommandHistory.length === 0);
10641080
}
10651081

1066-
// Determine which history we're using
1067-
const currentHistory = isInGlobalHistory ? globalHistory : commandHistory;
1082+
// Determine which history we're using - ensure commandHistory is always an array
1083+
const safeCommandHistory = commandHistory || [];
1084+
const currentHistory = isInGlobalHistory ? globalHistory : safeCommandHistory;
10681085
let newIndex = historyIndex;
10691086
let newValue = '';
10701087

@@ -1087,11 +1104,11 @@ export default function ChatInput({
10871104
// Still have items in current history
10881105
newIndex = historyIndex - 1;
10891106
newValue = currentHistory[newIndex];
1090-
} else if (isInGlobalHistory && commandHistory.length > 0) {
1107+
} else if (isInGlobalHistory && safeCommandHistory.length > 0) {
10911108
// Switch to chat history
10921109
setIsInGlobalHistory(false);
1093-
newIndex = commandHistory.length - 1;
1094-
newValue = commandHistory[newIndex];
1110+
newIndex = safeCommandHistory.length - 1;
1111+
newValue = safeCommandHistory[newIndex];
10951112
} else {
10961113
// Return to original input
10971114
newIndex = -1;
@@ -2058,8 +2075,8 @@ export default function ChatInput({
20582075
</Tooltip>
20592076
<div className="w-px h-4 bg-border-default mx-2" />
20602077

2061-
{/* Session Sharing Component - disabled in Matrix mode */}
2062-
{!isInMatrixMode && (
2078+
{/* Session Sharing Component - disabled for Matrix rooms */}
2079+
{!isMatrixRoom && (
20632080
<SessionSharing
20642081
sessionSharing={sessionSharing}
20652082
shouldShowIconOnly={shouldShowIconOnly}
@@ -2088,7 +2105,7 @@ export default function ChatInput({
20882105
setView={setView}
20892106
alerts={alerts}
20902107
recipeConfig={recipeConfig}
2091-
hasMessages={messages.length > 0}
2108+
hasMessages={messages && Array.isArray(messages) && messages.length > 0}
20922109
shouldShowIconOnly={shouldShowIconOnly}
20932110
/>
20942111
</div>

ui/desktop/src/components/CollaborationInviteNotification.tsx

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useState, useEffect } from 'react';
22
import { motion, AnimatePresence } from 'framer-motion';
33
import { Users, X, Check, Clock } from 'lucide-react';
4-
import { useNavigate } from 'react-router-dom';
4+
import { useNavigate, useLocation } from 'react-router-dom';
55
import { useMatrix } from '../contexts/MatrixContext';
6-
import { GooseChatMessage } from '../services/MatrixService';
6+
import { GooseChatMessage, matrixService } from '../services/MatrixService';
77

88
interface CollaborationInviteNotificationProps {
99
className?: string;
@@ -21,22 +21,82 @@ const CollaborationInviteNotification: React.FC<CollaborationInviteNotificationP
2121
} = useMatrix();
2222

2323
const navigate = useNavigate();
24+
const location = useLocation();
2425
const [pendingInvites, setPendingInvites] = useState<GooseChatMessage[]>([]);
2526
const [dismissedInvites, setDismissedInvites] = useState<Set<string>>(new Set());
2627

27-
// Listen for Goose messages that could be collaboration opportunities
28+
// Helper function to get current active Matrix room ID if in shared session mode
29+
const getCurrentActiveMatrixRoom = () => {
30+
const searchParams = new URLSearchParams(location.search);
31+
const isMatrixMode = searchParams.get('matrixMode') === 'true';
32+
const matrixRoomId = searchParams.get('matrixRoomId');
33+
34+
return isMatrixMode && matrixRoomId ? matrixRoomId : null;
35+
};
36+
37+
// Listen for Matrix room invitations and Goose messages
2838
useEffect(() => {
2939
if (!isConnected) return;
3040

31-
const unsubscribe = onGooseMessage((message: GooseChatMessage) => {
41+
// Handler for Matrix room invitations
42+
const handleMatrixRoomInvitation = (invitationData: any) => {
43+
console.log('🔔 Received Matrix room invitation:', invitationData);
44+
45+
// Skip notifications for the currently active Matrix room (shared session)
46+
const activeMatrixRoom = getCurrentActiveMatrixRoom();
47+
if (activeMatrixRoom && invitationData.roomId === activeMatrixRoom) {
48+
console.log('🔔 Skipping Matrix invitation notification for active room:', invitationData.roomId);
49+
return;
50+
}
51+
52+
// Convert Matrix invitation to GooseChatMessage format for UI compatibility
53+
const collaborationInvite: GooseChatMessage = {
54+
type: 'goose.collaboration.invite',
55+
messageId: `matrix-invite-${invitationData.roomId}-${Date.now()}`,
56+
content: `${invitationData.inviterName} invited you to collaborate in a Matrix room`,
57+
sender: invitationData.inviter,
58+
timestamp: invitationData.timestamp,
59+
roomId: invitationData.roomId,
60+
metadata: {
61+
isFromSelf: false,
62+
invitationType: 'matrix_room',
63+
sessionId: `matrix-${invitationData.roomId}`,
64+
sessionTitle: `Matrix Collaboration with ${invitationData.inviterName}`,
65+
roomId: invitationData.roomId,
66+
inviterName: invitationData.inviterName,
67+
},
68+
};
69+
70+
// Add to pending invites if not already dismissed
71+
setPendingInvites(prev => {
72+
const exists = prev.some(invite => invite.messageId === collaborationInvite.messageId);
73+
if (!exists && !dismissedInvites.has(collaborationInvite.messageId)) {
74+
return [...prev, collaborationInvite];
75+
}
76+
return prev;
77+
});
78+
};
79+
80+
// Listen for direct Matrix room invitations (no duplicates)
81+
matrixService.on('matrixRoomInvitation', handleMatrixRoomInvitation);
82+
83+
// Listen for Goose messages that could be collaboration opportunities
84+
const unsubscribeGooseMessages = onGooseMessage((message: GooseChatMessage) => {
3285
// Only show messages that are not from self
3386
if (message.metadata?.isFromSelf) {
3487
console.log('💬 Ignoring message from self');
3588
return;
3689
}
3790

38-
// Handle explicit collaboration invites
39-
if (message.type === 'goose.collaboration.invite') {
91+
// Skip notifications for messages from the currently active Matrix room (shared session)
92+
const activeMatrixRoom = getCurrentActiveMatrixRoom();
93+
if (activeMatrixRoom && message.roomId === activeMatrixRoom) {
94+
console.log('🔔 Skipping collaboration notification for message from active Matrix room:', message.roomId);
95+
return;
96+
}
97+
98+
// Handle explicit collaboration invites (but skip Matrix room invitations since they're handled above)
99+
if (message.type === 'goose.collaboration.invite' && message.metadata?.invitationType !== 'matrix_room') {
40100
console.log('🔔 Received collaboration invite notification:', message);
41101

42102
// Add to pending invites if not already dismissed
@@ -89,7 +149,12 @@ const CollaborationInviteNotification: React.FC<CollaborationInviteNotificationP
89149
}
90150
});
91151

92-
return unsubscribe;
152+
return () => {
153+
// Remove Matrix room invitation listener
154+
matrixService.off('matrixRoomInvitation', handleMatrixRoomInvitation);
155+
// Remove Goose message listener
156+
unsubscribeGooseMessages();
157+
};
93158
}, [isConnected, onGooseMessage, dismissedInvites]);
94159

95160
const handleAcceptInvite = async (invite: GooseChatMessage) => {
@@ -227,35 +292,35 @@ const CollaborationInviteNotification: React.FC<CollaborationInviteNotificationP
227292
animate={{ opacity: 1, x: 0, scale: 1 }}
228293
exit={{ opacity: 0, x: 300, scale: 0.8 }}
229294
transition={{ type: "spring", stiffness: 300, damping: 30 }}
230-
className="bg-white border border-blue-200 rounded-lg shadow-lg p-4 max-w-sm"
295+
className="bg-background-default border border-borderStandard rounded-lg shadow-lg p-4 max-w-sm"
231296
>
232297
<div className="flex items-start gap-3">
233-
<div className="flex-shrink-0 w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
234-
<Users className="w-5 h-5 text-blue-600" />
298+
<div className="flex-shrink-0 w-10 h-10 bg-bgSubtle rounded-full flex items-center justify-center">
299+
<Users className="w-5 h-5 text-accent" />
235300
</div>
236301

237302
<div className="flex-1 min-w-0">
238303
<div className="flex items-center justify-between mb-1">
239-
<h4 className="text-sm font-semibold text-gray-900">
304+
<h4 className="text-sm font-semibold text-textStandard">
240305
{invite.type === 'goose.collaboration.invite' ? 'Collaboration Invite' : 'New Message'}
241306
</h4>
242307
<button
243308
onClick={() => handleDismissInvite(invite)}
244-
className="text-gray-400 hover:text-gray-600 transition-colors"
309+
className="text-textSubtle hover:text-textStandard transition-colors"
245310
>
246311
<X className="w-4 h-4" />
247312
</button>
248313
</div>
249314

250-
<p className="text-sm text-gray-600 mb-2">
315+
<p className="text-sm text-textStandard mb-2">
251316
<span className="font-medium">{getSenderName(invite)}</span> {
252317
invite.type === 'goose.collaboration.invite'
253318
? 'invited you to collaborate'
254319
: 'sent you a message'
255320
}
256321
</p>
257322

258-
<p className="text-xs text-gray-500 mb-3 line-clamp-2">
323+
<p className="text-xs text-textSubtle mb-3 line-clamp-2">
259324
{invite.content}
260325
</p>
261326

@@ -270,14 +335,14 @@ const CollaborationInviteNotification: React.FC<CollaborationInviteNotificationP
270335

271336
<button
272337
onClick={() => handleDeclineInvite(invite)}
273-
className="flex items-center gap-1 px-3 py-1.5 border border-gray-300 text-gray-700 text-xs rounded hover:bg-gray-50 transition-colors"
338+
className="flex items-center gap-1 px-3 py-1.5 border border-borderStandard text-textStandard text-xs rounded hover:bg-bgSubtle transition-colors"
274339
>
275340
<X className="w-3 h-3" />
276341
Decline
277342
</button>
278343
</div>
279344

280-
<div className="flex items-center gap-1 mt-2 text-xs text-gray-400">
345+
<div className="flex items-center gap-1 mt-2 text-xs text-textSubtle">
281346
<Clock className="w-3 h-3" />
282347
{invite.timestamp.toLocaleTimeString()}
283348
</div>

0 commit comments

Comments
 (0)