diff --git a/app/components/Chat/index.tsx b/app/components/Chat/index.tsx index 90fe11701..5179f3209 100644 --- a/app/components/Chat/index.tsx +++ b/app/components/Chat/index.tsx @@ -23,10 +23,11 @@ export function Chat({ inputRef, actionFetcher, isOpen, - setIsOpen, + toggleOpen, + unreadCount, } = useChat(id); - if (!user) return null; + if (!user || !messages) return null; return ( <> @@ -76,13 +77,16 @@ export function Chat({ )} - + {unreadCount > 0 && ( +
{unreadCount}
+ )} ); } diff --git a/app/components/Chat/useChat.ts b/app/components/Chat/useChat.ts index 7d35076bb..f3eab9437 100644 --- a/app/components/Chat/useChat.ts +++ b/app/components/Chat/useChat.ts @@ -12,6 +12,7 @@ export default function useChat(id: string) { const [messagesAfterLoad, setMessagesAfterLoad] = React.useState< ChatLoaderData["messages"] >([]); + const [unreadCount, setUnreadCount] = React.useState(0); const loaderFetcher = useFetcher(); const actionFetcher = useFetcher(); const containerRef = React.useRef(null); @@ -21,16 +22,33 @@ export default function useChat(id: string) { const user = useUser(); invariant(user, "!user"); - useSocketEvent(`chat-${id}`, (data: Unpacked) => { - if (data.sender.id === user.id) return; - setMessagesAfterLoad((messages) => [...messages, data]); - setIsOpen(true); - }); + const eventHandler = React.useCallback( + (data: Unpacked) => { + if (data.sender.id === user.id) return; + setMessagesAfterLoad((messages) => [...messages, data]); + if (!isOpen) { + setUnreadCount((count) => count + 1); + } + }, + [isOpen] + ); + + useSocketEvent(`chat-${id}`, eventHandler); React.useEffect(() => { loaderFetcher.load(chatRoute([id])); }, []); + // open chat on data load if there are messages + React.useEffect(() => { + if (!loaderFetcher.data) return; + + if (loaderFetcher.data.messages.length > 0) { + setIsOpen(true); + } + }, [loaderFetcher.data]); + + // after sending message reset and refocus input so user can keep typing React.useEffect(() => { if (actionFetcher.submission) { formRef.current?.reset(); @@ -69,6 +87,17 @@ export default function useChat(id: string) { return newMessageContent; }, [actionFetcher]); + const toggleOpen = React.useCallback(() => { + setIsOpen((open) => { + if (!open) { + setUnreadCount(0); + return true; + } + + return false; + }); + }, []); + React.useEffect(() => { if (!containerRef.current) return; containerRef.current.scrollTop = containerRef.current.scrollHeight; @@ -82,6 +111,7 @@ export default function useChat(id: string) { inputRef, actionFetcher, isOpen, - setIsOpen, + toggleOpen, + unreadCount, }; } diff --git a/app/hooks/useSocketEvent.ts b/app/hooks/useSocketEvent.ts index a05ab32f9..2af9d4d30 100644 --- a/app/hooks/useSocketEvent.ts +++ b/app/hooks/useSocketEvent.ts @@ -11,5 +11,5 @@ export function useSocketEvent(event: string, handler: (data: any) => void) { return () => { socket.close(); }; - }, []); + }, [handler]); } diff --git a/app/styles/global.css b/app/styles/global.css index 8845c80f7..e9c5f0823 100644 --- a/app/styles/global.css +++ b/app/styles/global.css @@ -588,6 +588,23 @@ hr { width: 28px; } +.chat__fab__unread { + position: fixed; + z-index: 70; + bottom: 60px; + left: 50.9%; + display: flex; + width: 1rem; + height: 1rem; + align-items: center; + justify-content: center; + background-color: var(--theme-warning); + border-radius: 50%; + color: var(--button-text); + font-size: var(--fonts-xs); + font-weight: var(--bold); +} + .chat__window { position: fixed; z-index: 50; @@ -620,6 +637,10 @@ hr { .chat__window { left: 85%; } + + .chat__fab__unread { + left: 85.9%; + } } .chat__messages {