mirror of https://github.com/waku-org/js-waku.git
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:
parent
b4a440cb03
commit
d307342f7e
|
@ -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[] }
|
||||
) => {
|
||||
const connectedToStorePeer = new Promise((resolve) =>
|
||||
waku.libp2p.peerStore.once(
|
||||
'change:protocols',
|
||||
({ peerId, protocols }) => {
|
||||
if (protocols.includes(StoreCodec)) {
|
||||
console.log(`${peerId.toB58String()}: retrieving archived messages}`);
|
||||
resolve(peerId);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
connectedToStorePeer.then(() => {
|
||||
console.log(`Retrieving archived messages}`);
|
||||
setHistoricalMessagesRetrieved(true);
|
||||
|
||||
try {
|
||||
const length = await retrieveStoreMessages(
|
||||
_waku,
|
||||
peerId,
|
||||
setArchivedMessages
|
||||
retrieveStoreMessages(waku, dispatchMessages).then((length) =>
|
||||
console.log(`Messages retrieved:`, length)
|
||||
);
|
||||
console.log(`${peerId.toB58String()}: messages retrieved:`, length);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`${peerId.toB58String()}: error encountered when retrieving archived messages`,
|
||||
e
|
||||
);
|
||||
console.log(`Error encountered when retrieving archived messages`, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
waku.libp2p.peerStore.on(
|
||||
'change:protocols',
|
||||
handleProtocolChange.bind({}, waku)
|
||||
);
|
||||
|
||||
return function cleanUp() {
|
||||
waku?.libp2p.peerStore.removeListener(
|
||||
'change:protocols',
|
||||
handleProtocolChange.bind({}, waku)
|
||||
);
|
||||
};
|
||||
}, [waku]);
|
||||
});
|
||||
}, [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);
|
||||
}
|
||||
|
|
|
@ -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) => (
|
||||
const renderedMessages = props.messages.map((message) => (
|
||||
<LiveMessage
|
||||
key={
|
||||
currentMessage.sentTimestamp
|
||||
? currentMessage.sentTimestamp.valueOf()
|
||||
message.sentTimestamp
|
||||
? message.sentTimestamp.valueOf()
|
||||
: '' +
|
||||
currentMessage.timestamp.valueOf() +
|
||||
currentMessage.nick +
|
||||
currentMessage.payloadAsUtf8
|
||||
message.timestamp.valueOf() +
|
||||
message.nick +
|
||||
message.payloadAsUtf8
|
||||
}
|
||||
authorName={currentMessage.nick}
|
||||
date={formatDisplayDate(currentMessage)}
|
||||
authorName={message.nick}
|
||||
date={formatDisplayDate(message)}
|
||||
>
|
||||
<MessageText>{currentMessage.payloadAsUtf8}</MessageText>
|
||||
<MessageText>{message.payloadAsUtf8}</MessageText>
|
||||
</LiveMessage>
|
||||
))}
|
||||
</MessageGroup>
|
||||
));
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue