diff --git a/app/features/chat/chat-hooks.ts b/app/features/chat/chat-hooks.ts new file mode 100644 index 000000000..d134c5307 --- /dev/null +++ b/app/features/chat/chat-hooks.ts @@ -0,0 +1,71 @@ +import React from "react"; +import type { ChatMessage } from "./chat-types"; +import { useUser } from "../auth/core/user"; + +// increasing this = scrolling happens even when scrolled more upwards +const THRESHOLD = 100; + +export function useChatAutoScroll( + messages: ChatMessage[], + ref: React.RefObject, +) { + const user = useUser(); + const [firstLoadHandled, setFirstLoadHandled] = React.useState(false); + const [unseenMessages, setUnseenMessages] = React.useState(false); + + React.useEffect(() => { + const messagesContainer = ref.current!; + const isScrolledToBottom = + Math.abs( + messagesContainer.scrollHeight - + messagesContainer.clientHeight - + messagesContainer.scrollTop, + ) <= THRESHOLD; + const latestMessageIsOwn = + messages[messages.length - 1]?.userId === user?.id; + + // lets wait for messages to load first + if (!firstLoadHandled && messages.length === 0) return; + + if (isScrolledToBottom || latestMessageIsOwn || !firstLoadHandled) { + setFirstLoadHandled(true); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } else if (!isScrolledToBottom) { + setUnseenMessages(true); + } + }, [messages, ref, user, firstLoadHandled]); + + React.useEffect(() => { + const messagesContainer = ref.current!; + + function handleScroll() { + if ( + messagesContainer.scrollTop + messagesContainer.clientHeight >= + messagesContainer.scrollHeight - THRESHOLD + ) { + setUnseenMessages(false); + } + } + + messagesContainer.addEventListener("scroll", handleScroll); + + return () => { + messagesContainer.removeEventListener("scroll", handleScroll); + }; + }, [ref]); + + const scrollToBottom = () => { + ref.current!.scrollTop = ref.current!.scrollHeight; + }; + + const reset = () => { + setFirstLoadHandled(false); + setUnseenMessages(false); + }; + + return { + unseenMessagesInTheRoom: unseenMessages, + resetScroller: reset, + scrollToBottom, + }; +} diff --git a/app/features/chat/components/Chat.tsx b/app/features/chat/components/Chat.tsx index 603b2135f..31e8f3c97 100644 --- a/app/features/chat/components/Chat.tsx +++ b/app/features/chat/components/Chat.tsx @@ -16,6 +16,7 @@ import { messageTypeToSound, soundEnabled, soundVolume } from "../chat-utils"; import { soundPath } from "~/utils/urls"; import { useTranslation } from "react-i18next"; import { logger } from "~/utils/logger"; +import { useChatAutoScroll } from "../chat-hooks"; type ChatUser = Pick & { chatNameColor: string | null; @@ -83,10 +84,8 @@ export function Chat({ [send], ); - React.useEffect(() => { - const messagesContainer = messagesContainerRef.current!; - messagesContainer.scrollTop = messagesContainer.scrollHeight; - }, [messages]); + const { unseenMessagesInTheRoom, scrollToBottom, resetScroller } = + useChatAutoScroll(messages, messagesContainerRef); React.useEffect(() => { onMount?.(); @@ -139,7 +138,10 @@ export function Chat({ className={clsx("chat__room-button", { current: currentRoom === room.code, })} - onClick={() => setCurrentRoom(room.code)} + onClick={() => { + setCurrentRoom(room.code); + resetScroller(); + }} > {room.label} @@ -183,6 +185,11 @@ export function Chat({ ); })} + {unseenMessagesInTheRoom ? ( + + ) : null}