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.
This commit is contained in:
Franck Royer 2021-07-29 16:30:06 +10:00
parent b4a440cb03
commit d307342f7e
No known key found for this signature in database
GPG Key ID: A82ED75A8DFC50A4
3 changed files with 65 additions and 154 deletions

View File

@ -1,7 +1,7 @@
import PeerId from 'peer-id'; import { useEffect, useReducer, useState } from 'react';
import { useEffect, useState } from 'react';
import './App.css'; import './App.css';
import { import {
Direction,
Environment, Environment,
getStatusFleetNodes, getStatusFleetNodes,
StoreCodec, StoreCodec,
@ -48,7 +48,6 @@ export const ChatContentTopic = '/toy-chat/2/huilong/proto';
async function retrieveStoreMessages( async function retrieveStoreMessages(
waku: Waku, waku: Waku,
peerId: PeerId,
setArchivedMessages: (value: Message[]) => void setArchivedMessages: (value: Message[]) => void
): Promise<number> { ): Promise<number> {
const callback = (wakuMessages: WakuMessage[]): void => { const callback = (wakuMessages: WakuMessage[]): void => {
@ -64,9 +63,9 @@ async function retrieveStoreMessages(
}; };
const res = await waku.store.queryHistory({ const res = await waku.store.queryHistory({
peerId,
contentTopics: [ChatContentTopic], contentTopics: [ChatContentTopic],
pageSize: 5, pageSize: 5,
direction: Direction.FORWARD,
callback, callback,
}); });
@ -74,13 +73,16 @@ async function retrieveStoreMessages(
} }
export default function App() { export default function App() {
const [newMessages, setNewMessages] = useState<Message[]>([]); const [messages, dispatchMessages] = useReducer(reduceMessages, []);
const [archivedMessages, setArchivedMessages] = useState<Message[]>([]);
const [waku, setWaku] = useState<Waku | undefined>(undefined); const [waku, setWaku] = useState<Waku | undefined>(undefined);
const [nick, setNick] = useState<string>(() => { const [nick, setNick] = useState<string>(() => {
const persistedNick = window.localStorage.getItem('nick'); const persistedNick = window.localStorage.getItem('nick');
return persistedNick !== null ? persistedNick : generate(); return persistedNick !== null ? persistedNick : generate();
}); });
const [
historicalMessagesRetrieved,
setHistoricalMessagesRetrieved,
] = useState(false);
useEffect(() => { useEffect(() => {
localStorage.setItem('nick', nick); localStorage.setItem('nick', nick);
@ -94,12 +96,14 @@ export default function App() {
useEffect(() => { useEffect(() => {
if (!waku) return; if (!waku) return;
// Let's retrieve previous messages before listening to new messages
if (!historicalMessagesRetrieved) return;
const handleRelayMessage = (wakuMsg: WakuMessage) => { const handleRelayMessage = (wakuMsg: WakuMessage) => {
console.log('Message received: ', wakuMsg); console.log('Message received: ', wakuMsg);
const msg = Message.fromWakuMessage(wakuMsg); const msg = Message.fromWakuMessage(wakuMsg);
if (msg) { if (msg) {
setNewMessages([msg]); dispatchMessages([msg]);
} }
}; };
@ -108,45 +112,36 @@ export default function App() {
return function cleanUp() { return function cleanUp() {
waku?.relay.deleteObserver(handleRelayMessage, [ChatContentTopic]); waku?.relay.deleteObserver(handleRelayMessage, [ChatContentTopic]);
}; };
}, [waku]); }, [waku, historicalMessagesRetrieved]);
useEffect(() => { useEffect(() => {
if (!waku) return; if (!waku) return;
if (historicalMessagesRetrieved) return;
const handleProtocolChange = async ( const connectedToStorePeer = new Promise((resolve) =>
_waku: Waku, waku.libp2p.peerStore.once(
{ peerId, protocols }: { peerId: PeerId; protocols: string[] } 'change:protocols',
) => { ({ peerId, protocols }) => {
if (protocols.includes(StoreCodec)) { if (protocols.includes(StoreCodec)) {
console.log(`${peerId.toB58String()}: retrieving archived messages}`); resolve(peerId);
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
);
} }
} )
};
waku.libp2p.peerStore.on(
'change:protocols',
handleProtocolChange.bind({}, waku)
); );
return function cleanUp() { connectedToStorePeer.then(() => {
waku?.libp2p.peerStore.removeListener( console.log(`Retrieving archived messages}`);
'change:protocols', setHistoricalMessagesRetrieved(true);
handleProtocolChange.bind({}, waku)
); try {
}; retrieveStoreMessages(waku, dispatchMessages).then((length) =>
}, [waku]); console.log(`Messages retrieved:`, length)
);
} catch (e) {
console.log(`Error encountered when retrieving archived messages`, e);
}
});
}, [waku, historicalMessagesRetrieved]);
return ( return (
<div <div
@ -157,14 +152,13 @@ export default function App() {
<ThemeProvider theme={themes}> <ThemeProvider theme={themes}>
<Room <Room
nick={nick} nick={nick}
newMessages={newMessages} messages={messages}
archivedMessages={archivedMessages}
commandHandler={(input: string) => { commandHandler={(input: string) => {
const { command, response } = handleCommand(input, waku, setNick); const { command, response } = handleCommand(input, waku, setNick);
const commandMessages = response.map((msg) => { const commandMessages = response.map((msg) => {
return Message.fromUtf8String(command, msg); return Message.fromUtf8String(command, msg);
}); });
setNewMessages(commandMessages); dispatchMessages(commandMessages);
}} }}
/> />
</ThemeProvider> </ThemeProvider>
@ -207,3 +201,7 @@ function selectFleetEnv() {
return Environment.Prod; return Environment.Prod;
} }
} }
function reduceMessages(state: Message[], newMessages: Message[]) {
return state.concat(newMessages);
}

View File

@ -1,91 +1,43 @@
import { useEffect, useRef, useState } from 'react'; import { memo, useEffect, useRef } from 'react';
import { import {
Message as LiveMessage, Message as LiveMessage,
MessageText, MessageText,
MessageGroup,
MessageList, MessageList,
} from '@livechat/ui-kit'; } from '@livechat/ui-kit';
import { Message } from './Message'; import { Message } from './Message';
interface Props { interface Props {
archivedMessages: Message[]; messages: Message[];
newMessages: Message[];
} }
memo(ChatList);
export default function ChatList(props: Props) { export default function ChatList(props: Props) {
const [messages, setMessages] = useState<Message[]>([]); const renderedMessages = props.messages.map((message) => (
const [groupedMessages, setGroupedMessages] = useState<Message[][]>([]); <LiveMessage
let updatedMessages; key={
message.sentTimestamp
if (IsThereNewMessages(props.newMessages, messages)) { ? message.sentTimestamp.valueOf()
updatedMessages = messages.concat(props.newMessages); : '' +
if (IsThereNewMessages(props.archivedMessages, updatedMessages)) { message.timestamp.valueOf() +
updatedMessages = copyMergeUniqueReplace( message.nick +
props.archivedMessages, message.payloadAsUtf8
updatedMessages }
); authorName={message.nick}
} date={formatDisplayDate(message)}
} else { >
if (IsThereNewMessages(props.archivedMessages, messages)) { <MessageText>{message.payloadAsUtf8}</MessageText>
updatedMessages = copyMergeUniqueReplace( </LiveMessage>
props.archivedMessages,
messages
);
}
}
if (updatedMessages) {
setGroupedMessages(groupMessagesBySender(updatedMessages));
setMessages(updatedMessages);
}
const renderedGroupedMessages = groupedMessages.map((currentMessageGroup) => (
<MessageGroup onlyFirstWithMeta>
{currentMessageGroup.map((currentMessage) => (
<LiveMessage
key={
currentMessage.sentTimestamp
? currentMessage.sentTimestamp.valueOf()
: '' +
currentMessage.timestamp.valueOf() +
currentMessage.nick +
currentMessage.payloadAsUtf8
}
authorName={currentMessage.nick}
date={formatDisplayDate(currentMessage)}
>
<MessageText>{currentMessage.payloadAsUtf8}</MessageText>
</LiveMessage>
))}
</MessageGroup>
)); ));
return ( return (
<MessageList active containScrollInSubtree> <MessageList active containScrollInSubtree>
{renderedGroupedMessages} {renderedMessages}
<AlwaysScrollToBottom newMessages={props.newMessages} /> <AlwaysScrollToBottom messages={props.messages} />
</MessageList> </MessageList>
); );
} }
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 { function formatDisplayDate(message: Message): string {
return message.timestamp.toLocaleString([], { return message.timestamp.toLocaleString([], {
month: 'short', month: 'short',
@ -96,49 +48,14 @@ function formatDisplayDate(message: Message): string {
}); });
} }
const AlwaysScrollToBottom = (props: { newMessages: Message[] }) => { const AlwaysScrollToBottom = (props: { messages: Message[] }) => {
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
useEffect(() => { useEffect(() => {
// @ts-ignore // @ts-ignore
elementRef.current.scrollIntoView(); elementRef.current.scrollIntoView();
}, [props.newMessages]); }, [props.messages]);
// @ts-ignore // @ts-ignore
return <div ref={elementRef} />; return <div ref={elementRef} />;
}; };
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()
);
}

View File

@ -7,8 +7,7 @@ import { TitleBar } from '@livechat/ui-kit';
import { Message } from './Message'; import { Message } from './Message';
interface Props { interface Props {
newMessages: Message[]; messages: Message[];
archivedMessages: Message[];
commandHandler: (cmd: string) => void; commandHandler: (cmd: string) => void;
nick: string; nick: string;
} }
@ -32,10 +31,7 @@ export default function Room(props: Props) {
leftIcons={`Peers: ${relayPeers} relay, ${storePeers} store.`} leftIcons={`Peers: ${relayPeers} relay, ${storePeers} store.`}
title="Waku v2 chat app" title="Waku v2 chat app"
/> />
<ChatList <ChatList messages={props.messages} />
newMessages={props.newMessages}
archivedMessages={props.archivedMessages}
/>
<MessageInput <MessageInput
sendMessage={ sendMessage={
waku waku