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 messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const justSwitchedConversationRef = useRef(false);
|
||||
|
||||
const currentUserId = session?.user?.id;
|
||||
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(() => {
|
||||
scrollToBottom();
|
||||
justSwitchedConversationRef.current = true;
|
||||
}, [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(() => {
|
||||
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 isVeryRecent = new Date().getTime() - new Date(lastMessage.createdAt).getTime() < 2000;
|
||||
if (isVeryRecent) {
|
||||
@@ -312,6 +320,13 @@ export function MessagingPage() {
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// Scroll to bottom when typing indicator appears
|
||||
useEffect(() => {
|
||||
if (typingUsers.size > 0) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [typingUsers]);
|
||||
|
||||
// Filter conversations by search
|
||||
const filteredConversations = conversations.filter((conv) =>
|
||||
conv.otherParty?.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
@@ -616,10 +631,15 @@ export function MessagingPage() {
|
||||
{typingUserNames.length > 0 && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-[#00293d]/10 text-[#00293D] rounded-[15px] px-4 py-3">
|
||||
<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: '150ms' }} />
|
||||
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
<div className="flex items-center gap-2">
|
||||
<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: '150ms' }} />
|
||||
<span className="w-2 h-2 bg-[#00293D]/50 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
<span className="font-serif font-normal text-[12px] leading-[16px] text-[#00293D]/50">
|
||||
{formatMessageTime(new Date().toISOString())}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,7 +161,13 @@ class SocketService {
|
||||
resolve({ success: false, error: 'Not connected' });
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
resolve({ success: false, error: 'Socket timeout' });
|
||||
}, 10000);
|
||||
|
||||
this.socket.emit('join_conversation', { conversationId }, (response: { success: boolean; error?: string }) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
@@ -183,10 +189,17 @@ class SocketService {
|
||||
resolve({ success: false, error: 'Not connected' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout fallback in case acknowledgement never arrives
|
||||
const timeout = setTimeout(() => {
|
||||
resolve({ success: false, error: 'Socket timeout' });
|
||||
}, 10000);
|
||||
|
||||
this.socket.emit(
|
||||
'send_message',
|
||||
{ conversationId, message },
|
||||
(response: { success: boolean; message?: Message; error?: string }) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
}
|
||||
);
|
||||
@@ -213,7 +226,13 @@ class SocketService {
|
||||
resolve({ success: false, error: 'Not connected' });
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
resolve({ success: false, error: 'Socket timeout' });
|
||||
}, 10000);
|
||||
|
||||
this.socket.emit('mark_read', { conversationId }, (response: { success: boolean; error?: string }) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user