feat: Enhance messaging page scrolling and typing indicator UI, and add socket acknowledgement timeouts for messaging actions.
This commit is contained in:
@@ -118,6 +118,7 @@ export function MessagingPage() {
|
|||||||
const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({});
|
const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({});
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const justSwitchedConversationRef = useRef(false);
|
||||||
|
|
||||||
const currentUserId = session?.user?.id;
|
const currentUserId = session?.user?.id;
|
||||||
const userRole = session?.user?.role;
|
const userRole = session?.user?.role;
|
||||||
@@ -296,14 +297,21 @@ export function MessagingPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scroll to bottom when new message arrives or conversation changes
|
// Mark that we switched conversations so we scroll after messages load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
justSwitchedConversationRef.current = true;
|
||||||
}, [currentConversation?.id]);
|
}, [currentConversation?.id]);
|
||||||
|
|
||||||
// Scroll to bottom when new messages are added (not when loading more)
|
// Scroll to bottom when messages load after opening a chat, or when a new message arrives
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
|
// After opening/switching a conversation — scroll once messages are rendered
|
||||||
|
if (justSwitchedConversationRef.current) {
|
||||||
|
justSwitchedConversationRef.current = false;
|
||||||
|
setTimeout(scrollToBottom, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// New incoming message — scroll if it's very recent
|
||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
const isVeryRecent = new Date().getTime() - new Date(lastMessage.createdAt).getTime() < 2000;
|
const isVeryRecent = new Date().getTime() - new Date(lastMessage.createdAt).getTime() < 2000;
|
||||||
if (isVeryRecent) {
|
if (isVeryRecent) {
|
||||||
@@ -312,6 +320,13 @@ export function MessagingPage() {
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// Scroll to bottom when typing indicator appears
|
||||||
|
useEffect(() => {
|
||||||
|
if (typingUsers.size > 0) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}, [typingUsers]);
|
||||||
|
|
||||||
// Filter conversations by search
|
// Filter conversations by search
|
||||||
const filteredConversations = conversations.filter((conv) =>
|
const filteredConversations = conversations.filter((conv) =>
|
||||||
conv.otherParty?.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
conv.otherParty?.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
@@ -616,11 +631,16 @@ export function MessagingPage() {
|
|||||||
{typingUserNames.length > 0 && (
|
{typingUserNames.length > 0 && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="bg-[#00293d]/10 text-[#00293D] rounded-[15px] px-4 py-3">
|
<div className="bg-[#00293d]/10 text-[#00293D] rounded-[15px] px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||||
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||||
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||||
</div>
|
</div>
|
||||||
|
<span className="font-serif font-normal text-[12px] leading-[16px] text-[#00293D]/50">
|
||||||
|
{formatMessageTime(new Date().toISOString())}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -161,7 +161,13 @@ class SocketService {
|
|||||||
resolve({ success: false, error: 'Not connected' });
|
resolve({ success: false, error: 'Not connected' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
resolve({ success: false, error: 'Socket timeout' });
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
this.socket.emit('join_conversation', { conversationId }, (response: { success: boolean; error?: string }) => {
|
this.socket.emit('join_conversation', { conversationId }, (response: { success: boolean; error?: string }) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -183,10 +189,17 @@ class SocketService {
|
|||||||
resolve({ success: false, error: 'Not connected' });
|
resolve({ success: false, error: 'Not connected' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timeout fallback in case acknowledgement never arrives
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
resolve({ success: false, error: 'Socket timeout' });
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
this.socket.emit(
|
this.socket.emit(
|
||||||
'send_message',
|
'send_message',
|
||||||
{ conversationId, message },
|
{ conversationId, message },
|
||||||
(response: { success: boolean; message?: Message; error?: string }) => {
|
(response: { success: boolean; message?: Message; error?: string }) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -213,7 +226,13 @@ class SocketService {
|
|||||||
resolve({ success: false, error: 'Not connected' });
|
resolve({ success: false, error: 'Not connected' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
resolve({ success: false, error: 'Socket timeout' });
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
this.socket.emit('mark_read', { conversationId }, (response: { success: boolean; error?: string }) => {
|
this.socket.emit('mark_read', { conversationId }, (response: { success: boolean; error?: string }) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user