Fix chat layout for mobile keyboard with dvh, sticky header/input, and smart scroll
This commit is contained in:
+45
-11
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router';
|
||||
import PageLayout from '@/components/layout/PageLayout';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -36,9 +36,22 @@ export default function Chat() {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const lastTypingEmit = useRef(0);
|
||||
const isAtBottom = useRef(true);
|
||||
|
||||
const friend = friends.find((f) => f.friend.id === friendId)?.friend;
|
||||
|
||||
const checkIfAtBottom = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
const threshold = 50;
|
||||
isAtBottom.current =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||
}, []);
|
||||
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
if (!user) {
|
||||
@@ -57,9 +70,28 @@ export default function Chat() {
|
||||
};
|
||||
}, [user, authLoading, friendId, navigate, connect, disconnect, openConversation, clearChat]);
|
||||
|
||||
// Scroll to bottom on new messages if user hasn't scrolled up
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
if (isAtBottom.current) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [messages, scrollToBottom]);
|
||||
|
||||
// Handle visual viewport resize (mobile keyboard open/close)
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
|
||||
const handleResize = () => {
|
||||
if (isAtBottom.current) {
|
||||
// Use instant scroll when keyboard animates to avoid janky smooth scroll
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
};
|
||||
|
||||
vv.addEventListener('resize', handleResize);
|
||||
return () => vv.removeEventListener('resize', handleResize);
|
||||
}, [scrollToBottom]);
|
||||
|
||||
async function handleSend(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -69,6 +101,7 @@ export default function Chat() {
|
||||
try {
|
||||
sendMessage(friendId, input.trim());
|
||||
setInput('');
|
||||
isAtBottom.current = true;
|
||||
} catch {
|
||||
toast.error('Failed to send message');
|
||||
} finally {
|
||||
@@ -77,6 +110,7 @@ export default function Chat() {
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
checkIfAtBottom();
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container || !hasMore || loading) return;
|
||||
if (container.scrollTop === 0) {
|
||||
@@ -87,10 +121,10 @@ export default function Chat() {
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="flex h-[calc(100vh-8rem)] flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 border-b border-border pb-3">
|
||||
<PageLayout noPadding>
|
||||
<div className="flex flex-col" style={{ height: 'calc(100dvh - 3.5rem)' }}>
|
||||
{/* Chat header - sticky at top */}
|
||||
<div className="sticky top-0 z-10 flex items-center gap-3 border-b border-border bg-background px-4 py-3">
|
||||
<Link to="/friends">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
@@ -109,11 +143,11 @@ export default function Chat() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
{/* Messages - scrollable area fills remaining space */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 overflow-y-auto py-4 space-y-2"
|
||||
className="flex-1 overflow-y-auto px-4 py-4 space-y-2"
|
||||
>
|
||||
{loading && (
|
||||
<div className="flex justify-center py-2">
|
||||
@@ -152,8 +186,8 @@ export default function Chat() {
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<form onSubmit={handleSend} className="flex gap-2 border-t border-border pt-3">
|
||||
{/* Input - pinned to bottom */}
|
||||
<form onSubmit={handleSend} className="flex gap-2 border-t border-border bg-background px-4 py-3">
|
||||
<Input
|
||||
placeholder="Type a message..."
|
||||
value={input}
|
||||
|
||||
Reference in New Issue
Block a user