From d307342f7e9ef7a693cd17c945682bfc5148bf84 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 29 Jul 2021 16:30:06 +1000 Subject: [PATCH] Simplify message handling to avoid re-rendering Only retrieve historical messages when starting the app. This allows avoid re-rendering issues. This is an example dApp. No need to waste time on React optimisation. --- examples/web-chat/src/App.tsx | 82 +++++++++--------- examples/web-chat/src/ChatList.tsx | 129 +++++------------------------ examples/web-chat/src/Room.tsx | 8 +- 3 files changed, 65 insertions(+), 154 deletions(-) diff --git a/examples/web-chat/src/App.tsx b/examples/web-chat/src/App.tsx index b55bea7f1e..12829d90b8 100644 --- a/examples/web-chat/src/App.tsx +++ b/examples/web-chat/src/App.tsx @@ -1,7 +1,7 @@ -import PeerId from 'peer-id'; -import { useEffect, useState } from 'react'; +import { useEffect, useReducer, useState } from 'react'; import './App.css'; import { + Direction, Environment, getStatusFleetNodes, StoreCodec, @@ -48,7 +48,6 @@ export const ChatContentTopic = '/toy-chat/2/huilong/proto'; async function retrieveStoreMessages( waku: Waku, - peerId: PeerId, setArchivedMessages: (value: Message[]) => void ): Promise { const callback = (wakuMessages: WakuMessage[]): void => { @@ -64,9 +63,9 @@ async function retrieveStoreMessages( }; const res = await waku.store.queryHistory({ - peerId, contentTopics: [ChatContentTopic], pageSize: 5, + direction: Direction.FORWARD, callback, }); @@ -74,13 +73,16 @@ async function retrieveStoreMessages( } export default function App() { - const [newMessages, setNewMessages] = useState([]); - const [archivedMessages, setArchivedMessages] = useState([]); + const [messages, dispatchMessages] = useReducer(reduceMessages, []); const [waku, setWaku] = useState(undefined); const [nick, setNick] = useState(() => { const persistedNick = window.localStorage.getItem('nick'); return persistedNick !== null ? persistedNick : generate(); }); + const [ + historicalMessagesRetrieved, + setHistoricalMessagesRetrieved, + ] = useState(false); useEffect(() => { localStorage.setItem('nick', nick); @@ -94,12 +96,14 @@ export default function App() { useEffect(() => { if (!waku) return; + // Let's retrieve previous messages before listening to new messages + if (!historicalMessagesRetrieved) return; const handleRelayMessage = (wakuMsg: WakuMessage) => { console.log('Message received: ', wakuMsg); const msg = Message.fromWakuMessage(wakuMsg); if (msg) { - setNewMessages([msg]); + dispatchMessages([msg]); } }; @@ -108,45 +112,36 @@ export default function App() { return function cleanUp() { waku?.relay.deleteObserver(handleRelayMessage, [ChatContentTopic]); }; - }, [waku]); + }, [waku, historicalMessagesRetrieved]); useEffect(() => { if (!waku) return; + if (historicalMessagesRetrieved) return; - const handleProtocolChange = async ( - _waku: Waku, - { peerId, protocols }: { peerId: PeerId; protocols: string[] } - ) => { - if (protocols.includes(StoreCodec)) { - console.log(`${peerId.toB58String()}: retrieving archived messages}`); - try { - const length = await retrieveStoreMessages( - _waku, - peerId, - setArchivedMessages - ); - console.log(`${peerId.toB58String()}: messages retrieved:`, length); - } catch (e) { - console.log( - `${peerId.toB58String()}: error encountered when retrieving archived messages`, - e - ); + const connectedToStorePeer = new Promise((resolve) => + waku.libp2p.peerStore.once( + 'change:protocols', + ({ peerId, protocols }) => { + if (protocols.includes(StoreCodec)) { + resolve(peerId); + } } - } - }; - - waku.libp2p.peerStore.on( - 'change:protocols', - handleProtocolChange.bind({}, waku) + ) ); - return function cleanUp() { - waku?.libp2p.peerStore.removeListener( - 'change:protocols', - handleProtocolChange.bind({}, waku) - ); - }; - }, [waku]); + connectedToStorePeer.then(() => { + console.log(`Retrieving archived messages}`); + setHistoricalMessagesRetrieved(true); + + try { + retrieveStoreMessages(waku, dispatchMessages).then((length) => + console.log(`Messages retrieved:`, length) + ); + } catch (e) { + console.log(`Error encountered when retrieving archived messages`, e); + } + }); + }, [waku, historicalMessagesRetrieved]); return (
{ const { command, response } = handleCommand(input, waku, setNick); const commandMessages = response.map((msg) => { return Message.fromUtf8String(command, msg); }); - setNewMessages(commandMessages); + dispatchMessages(commandMessages); }} /> @@ -207,3 +201,7 @@ function selectFleetEnv() { return Environment.Prod; } } + +function reduceMessages(state: Message[], newMessages: Message[]) { + return state.concat(newMessages); +} diff --git a/examples/web-chat/src/ChatList.tsx b/examples/web-chat/src/ChatList.tsx index ee29e6f9af..b074717aae 100644 --- a/examples/web-chat/src/ChatList.tsx +++ b/examples/web-chat/src/ChatList.tsx @@ -1,91 +1,43 @@ -import { useEffect, useRef, useState } from 'react'; +import { memo, useEffect, useRef } from 'react'; import { Message as LiveMessage, MessageText, - MessageGroup, MessageList, } from '@livechat/ui-kit'; import { Message } from './Message'; interface Props { - archivedMessages: Message[]; - newMessages: Message[]; + messages: Message[]; } +memo(ChatList); + export default function ChatList(props: Props) { - const [messages, setMessages] = useState([]); - const [groupedMessages, setGroupedMessages] = useState([]); - let updatedMessages; - - if (IsThereNewMessages(props.newMessages, messages)) { - updatedMessages = messages.concat(props.newMessages); - if (IsThereNewMessages(props.archivedMessages, updatedMessages)) { - updatedMessages = copyMergeUniqueReplace( - props.archivedMessages, - updatedMessages - ); - } - } else { - if (IsThereNewMessages(props.archivedMessages, messages)) { - updatedMessages = copyMergeUniqueReplace( - props.archivedMessages, - messages - ); - } - } - - if (updatedMessages) { - setGroupedMessages(groupMessagesBySender(updatedMessages)); - setMessages(updatedMessages); - } - - const renderedGroupedMessages = groupedMessages.map((currentMessageGroup) => ( - - {currentMessageGroup.map((currentMessage) => ( - - {currentMessage.payloadAsUtf8} - - ))} - + const renderedMessages = props.messages.map((message) => ( + + {message.payloadAsUtf8} + )); return ( - {renderedGroupedMessages} - + {renderedMessages} + ); } -function groupMessagesBySender(messageArray: Message[]): Message[][] { - let currentSender = -1; - let lastNick = ''; - let messagesBySender: Message[][] = []; - let currentSenderMessage = 0; - - for (let currentMessage of messageArray) { - if (lastNick !== currentMessage.nick) { - currentSender++; - messagesBySender[currentSender] = []; - currentSenderMessage = 0; - lastNick = currentMessage.nick; - } - messagesBySender[currentSender][currentSenderMessage++] = currentMessage; - } - return messagesBySender; -} - function formatDisplayDate(message: Message): string { return message.timestamp.toLocaleString([], { month: 'short', @@ -96,49 +48,14 @@ function formatDisplayDate(message: Message): string { }); } -const AlwaysScrollToBottom = (props: { newMessages: Message[] }) => { +const AlwaysScrollToBottom = (props: { messages: Message[] }) => { const elementRef = useRef(); useEffect(() => { // @ts-ignore elementRef.current.scrollIntoView(); - }, [props.newMessages]); + }, [props.messages]); // @ts-ignore return
; }; - -function IsThereNewMessages( - newValues: Message[], - currentValues: Message[] -): boolean { - if (newValues.length === 0) return false; - if (currentValues.length === 0) return true; - - return !newValues.find((newMsg) => - currentValues.find(isEqual.bind({}, newMsg)) - ); -} - -function copyMergeUniqueReplace( - newValues: Message[], - currentValues: Message[] -) { - const copy = currentValues.slice(); - newValues.forEach((msg) => { - if (!copy.find(isEqual.bind({}, msg))) { - copy.push(msg); - } - }); - copy.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf()); - return copy; -} - -function isEqual(lhs: Message, rhs: Message): boolean { - return ( - lhs.nick === rhs.nick && - lhs.payloadAsUtf8 === rhs.payloadAsUtf8 && - lhs.timestamp.valueOf() === rhs.timestamp.valueOf() && - lhs.sentTimestamp?.valueOf() === rhs.sentTimestamp?.valueOf() - ); -} diff --git a/examples/web-chat/src/Room.tsx b/examples/web-chat/src/Room.tsx index 9b217e8254..ccc46f4768 100644 --- a/examples/web-chat/src/Room.tsx +++ b/examples/web-chat/src/Room.tsx @@ -7,8 +7,7 @@ import { TitleBar } from '@livechat/ui-kit'; import { Message } from './Message'; interface Props { - newMessages: Message[]; - archivedMessages: Message[]; + messages: Message[]; commandHandler: (cmd: string) => void; nick: string; } @@ -32,10 +31,7 @@ export default function Room(props: Props) { leftIcons={`Peers: ${relayPeers} relay, ${storePeers} store.`} title="Waku v2 chat app" /> - +