From 10b061e36acdb1a03a1c22bb07591f1f5ab3302b Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Tue, 25 Oct 2022 21:00:28 -0500 Subject: [PATCH] feat: [closes #22] implement message backfilling (#45) * feat: [#22] implement message backfilling * feat: scroll to latest backfilled message * feat: document transcript backfilling --- README.md | 1 + .../ChatTranscript/ChatTranscript.tsx | 14 ++++++++++++-- src/components/Room/useRoom.ts | 17 +++++++++++++++-- src/models/network.ts | 2 ++ src/pages/About/About.tsx | 6 +++++- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 22f0961..408f3ab 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Open https://chitchatter.im/ and join a room to start chatting with anyone else - The number displayed at the top-right of the screen shows how many peers you are connected to. Your peers are the only ones who can see your message. - Markdown support via [`react-markdown`](https://github.com/remarkjs/react-markdown). - Includes support for syntax highlighting of code. +- Conversation backfilling from peers when a new participant joins - Multiline message support (hold Shift and press Enter). - Dark and light themes diff --git a/src/components/ChatTranscript/ChatTranscript.tsx b/src/components/ChatTranscript/ChatTranscript.tsx index ef0a922..4b2f4c1 100644 --- a/src/components/ChatTranscript/ChatTranscript.tsx +++ b/src/components/ChatTranscript/ChatTranscript.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, useRef, useEffect } from 'react' +import { HTMLAttributes, useRef, useEffect, useState } from 'react' import cx from 'classnames' import Box from '@mui/material/Box' @@ -16,6 +16,7 @@ export const ChatTranscript = ({ userId, }: ChatTranscriptProps) => { const boxRef = useRef(null) + const [previousMessageLogLength, setPreviousMessageLogLength] = useState(0) useEffect(() => { const { current: boxEl } = boxRef @@ -29,14 +30,23 @@ export const ChatTranscript = ({ const lastChild = children[children.length - 1] const lastChildHeight = lastChild.clientHeight const previousScrollTopMax = scrollTopMax - lastChildHeight + const wasPreviouslyScrolledToBottom = + Math.ceil(scrollTop) >= Math.ceil(previousScrollTopMax) + const wasMessageLogPreviouslyEmpty = previousMessageLogLength === 0 + const shouldScrollToLatestMessage = + wasPreviouslyScrolledToBottom || wasMessageLogPreviouslyEmpty if ( - Math.ceil(scrollTop) >= Math.ceil(previousScrollTopMax) && + shouldScrollToLatestMessage && // scrollTo is not defined in the unit test environment 'scrollTo' in boxEl ) { boxEl.scrollTo({ top: scrollTopMax }) } + }, [messageLog.length, previousMessageLogLength]) + + useEffect(() => { + setPreviousMessageLogLength(messageLog.length) }, [messageLog.length]) return ( diff --git a/src/components/Room/useRoom.ts b/src/components/Room/useRoom.ts index d2ae979..d3501b4 100644 --- a/src/components/Room/useRoom.ts +++ b/src/components/Room/useRoom.ts @@ -6,7 +6,7 @@ import { v4 as uuid } from 'uuid' import { ShellContext } from 'contexts/ShellContext' import { SettingsContext } from 'contexts/SettingsContext' import { PeerActions } from 'models/network' -import { ReceivedMessage, UnsentMessage, Message } from 'models/chat' +import { Message, ReceivedMessage, UnsentMessage, isMessageReceived } from 'models/chat' import { funAnimalName } from 'fun-animal-names' import { getPeerName } from 'components/PeerNameDisplay' import { NotificationService } from 'services/Notification' @@ -62,6 +62,10 @@ export function useRoom( PeerActions.PEER_NAME ) + const [sendMessageTranscript, receiveMessageTranscript] = usePeerRoomAction< + ReceivedMessage[] + >(peerRoom, PeerActions.MESSAGE_TRANSCRIPT) + const [sendPeerMessage, receivePeerMessage] = usePeerRoomAction(peerRoom, PeerActions.MESSAGE) @@ -102,6 +106,12 @@ export function useRoom( } }) + receiveMessageTranscript(transcript => { + if (messageLog.length) return + + setMessageLog(transcript) + }) + receivePeerMessage(message => { const userSettings = settingsContext.getUserSettings() @@ -130,7 +140,10 @@ export function useRoom( shellContext.setNumberOfPeers(newNumberOfPeers) ;(async () => { try { - await sendPeerId(userId, peerId) + await Promise.all([ + sendPeerId(userId, peerId), + sendMessageTranscript(messageLog.filter(isMessageReceived), peerId), + ]) } catch (e) { console.error(e) } diff --git a/src/models/network.ts b/src/models/network.ts index e5afb6e..c33fdfe 100644 --- a/src/models/network.ts +++ b/src/models/network.ts @@ -1,4 +1,6 @@ +// NOTE: Action names are limited to 12 characters, otherwise Trystero breaks. export enum PeerActions { MESSAGE = 'MESSAGE', + MESSAGE_TRANSCRIPT = 'MSG_XSCRIPT', PEER_NAME = 'PEER_NAME', } diff --git a/src/pages/About/About.tsx b/src/pages/About/About.tsx index 3d19fd4..23d659b 100644 --- a/src/pages/About/About.tsx +++ b/src/pages/About/About.tsx @@ -28,7 +28,11 @@ Public rooms can be joined by **anyone** with the room URL. By default, rooms ar To connect to others, share the room URL with a secure tool such as [Burner Note](https://burnernote.com/) or [Yopass](https://yopass.se/). You will be notified when others join the room. -Chat message transcripts are erased as soon as you close the page or navigate away from the room. +##### Conversation backfilling + +Conversation transcripts are erased from local memory as soon as you close the page or navigate away from the room. Conversations are only ever held in volatile memory and never persisted to any disk by Chitchatter. + +When a peer joins a public room with participants already in it, the new peer will automatically request the transcript of the conversation that has already taken place from the other peers. Once all peers leave the room, the conversation is completely erased. #### Message Authoring