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, 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<number> {
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<Message[]>([]);
const [archivedMessages, setArchivedMessages] = useState<Message[]>([]);
const [messages, dispatchMessages] = useReducer(reduceMessages, []);
const [waku, setWaku] = useState<Waku | undefined>(undefined);
const [nick, setNick] = useState<string>(() => {
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 (
<div
@ -157,14 +152,13 @@ export default function App() {
<ThemeProvider theme={themes}>
<Room
nick={nick}
newMessages={newMessages}
archivedMessages={archivedMessages}
messages={messages}
commandHandler={(input: string) => {
const { command, response } = handleCommand(input, waku, setNick);
const commandMessages = response.map((msg) => {
return Message.fromUtf8String(command, msg);
});
setNewMessages(commandMessages);
dispatchMessages(commandMessages);
}}
/>
</ThemeProvider>
@ -207,3 +201,7 @@ function selectFleetEnv() {
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 {
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<Message[]>([]);
const [groupedMessages, setGroupedMessages] = useState<Message[][]>([]);
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) => (
<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>
const renderedMessages = props.messages.map((message) => (
<LiveMessage
key={
message.sentTimestamp
? message.sentTimestamp.valueOf()
: '' +
message.timestamp.valueOf() +
message.nick +
message.payloadAsUtf8
}
authorName={message.nick}
date={formatDisplayDate(message)}
>
<MessageText>{message.payloadAsUtf8}</MessageText>
</LiveMessage>
));
return (
<MessageList active containScrollInSubtree>
{renderedGroupedMessages}
<AlwaysScrollToBottom newMessages={props.newMessages} />
{renderedMessages}
<AlwaysScrollToBottom messages={props.messages} />
</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 {
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<HTMLDivElement>();
useEffect(() => {
// @ts-ignore
elementRef.current.scrollIntoView();
}, [props.newMessages]);
}, [props.messages]);
// @ts-ignore
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';
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"
/>
<ChatList
newMessages={props.newMessages}
archivedMessages={props.archivedMessages}
/>
<ChatList messages={props.messages} />
<MessageInput
sendMessage={
waku