diff --git a/web-chat/src/App.tsx b/web-chat/src/App.tsx index 1a68481fef..bd197bf3ea 100644 --- a/web-chat/src/App.tsx +++ b/web-chat/src/App.tsx @@ -2,7 +2,8 @@ import { multiaddr } from 'multiaddr'; import PeerId from 'peer-id'; import { useEffect, useState } from 'react'; import './App.css'; -import { ChatMessage } from 'waku/chat_message'; +import { ChatMessage } from './ChatMessage'; +import { ChatMessage as WakuChatMessage } from 'waku/chat_message'; import { WakuMessage } from 'waku/waku_message'; import { RelayDefaultTopic } from 'waku/waku_relay'; import { StoreCodec } from 'waku/waku_store'; @@ -45,15 +46,16 @@ const themes = { export const ChatContentTopic = 'dingpu'; export default function App() { - let [stateMessages, setMessages] = useState([]); + let [newMessages, setNewMessages] = useState([]); + let [archivedMessages, setArchivedMessages] = useState([]); let [stateWaku, setWaku] = useState(undefined); let [nick, setNick] = useState(generate()); useEffect(() => { - const handleNewMessages = (event: { data: Uint8Array }) => { + const handleRelayMessage = (event: { data: Uint8Array }) => { const chatMsg = decodeWakuMessage(event.data); if (chatMsg) { - copyAppendReplace([chatMsg], stateMessages, setMessages); + setNewMessages([chatMsg]); } }; @@ -73,8 +75,11 @@ export default function App() { const messages = response .map((wakuMsg) => wakuMsg.payload) .filter((payload) => !!payload) - .map((payload) => ChatMessage.decode(payload as Uint8Array)); - copyMergeUniqueReplace(messages, stateMessages, setMessages); + .map((payload) => WakuChatMessage.decode(payload as Uint8Array)) + .map((wakuChatMessage) => + ChatMessage.fromWakuChatMessage(wakuChatMessage) + ); + setArchivedMessages(messages); } } }; @@ -84,7 +89,7 @@ export default function App() { .then(() => console.log('Waku init done')) .catch((e) => console.log('Waku init failed ', e)); } else { - stateWaku.libp2p.pubsub.on(RelayDefaultTopic, handleNewMessages); + stateWaku.libp2p.pubsub.on(RelayDefaultTopic, handleRelayMessage); stateWaku.libp2p.peerStore.on( 'change:protocols', @@ -95,7 +100,7 @@ export default function App() { return () => { stateWaku?.libp2p.pubsub.removeListener( RelayDefaultTopic, - handleNewMessages + handleRelayMessage ); stateWaku?.libp2p.peerStore.removeListener( 'change:protocols', @@ -103,7 +108,7 @@ export default function App() { ); }; } - }, [stateWaku, stateMessages]); + }, [stateWaku]); return (
{ const { command, response } = handleCommand( input, @@ -122,9 +128,9 @@ export default function App() { setNick ); const commandMessages = response.map((msg) => { - return new ChatMessage(new Date(), command, msg); + return new ChatMessage(new Date(), new Date(), command, msg); }); - copyAppendReplace(commandMessages, stateMessages, setMessages); + setNewMessages(commandMessages); }} /> @@ -162,37 +168,7 @@ function decodeWakuMessage(data: Uint8Array): null | ChatMessage { if (!wakuMsg.payload) { return null; } - return ChatMessage.decode(wakuMsg.payload); -} - -function copyAppendReplace( - newValues: Array, - currentValues: Array, - setter: (val: Array) => void -) { - const copy = currentValues.slice(); - setter(copy.concat(newValues)); -} - -function copyMergeUniqueReplace( - newValues: ChatMessage[], - currentValues: ChatMessage[], - setter: (val: ChatMessage[]) => void -) { - 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()); - setter(copy); -} - -function isEqual(lhs: ChatMessage, rhs: ChatMessage): boolean { - return ( - lhs.nick === rhs.nick && - lhs.message === rhs.message && - lhs.timestamp.toString() === rhs.timestamp.toString() + return ChatMessage.fromWakuChatMessage( + WakuChatMessage.decode(wakuMsg.payload) ); } diff --git a/web-chat/src/ChatList.tsx b/web-chat/src/ChatList.tsx index 71af3597d8..8a6c076062 100644 --- a/web-chat/src/ChatList.tsx +++ b/web-chat/src/ChatList.tsx @@ -1,5 +1,5 @@ -import { useEffect, useRef } from 'react'; -import { ChatMessage } from 'waku/chat_message'; +import { useEffect, useRef, useState } from 'react'; +import { ChatMessage } from './ChatMessage'; import { Message, MessageText, @@ -8,19 +8,45 @@ import { } from '@livechat/ui-kit'; interface Props { - messages: ChatMessage[]; + archivedMessages: ChatMessage[]; + newMessages: ChatMessage[]; } export default function ChatList(props: Props) { - const messages = props.messages; + const [messages, setMessages] = useState([]); + let updatedMessages; - const messagesGroupedBySender = groupMessagesBySender(props.messages).map( + if (IsThereNewMessages(props.newMessages, messages)) { + updatedMessages = messages.slice().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) { + setMessages(updatedMessages); + } + + const messagesGroupedBySender = groupMessagesBySender(messages).map( (currentMessageGroup) => ( {currentMessageGroup.map((currentMessage) => ( @@ -34,7 +60,7 @@ export default function ChatList(props: Props) { return ( {messagesGroupedBySender} - + ); } @@ -58,7 +84,7 @@ function groupMessagesBySender(messageArray: ChatMessage[]): ChatMessage[][] { } function formatDisplayDate(message: ChatMessage): string { - return message.timestamp.toLocaleString([], { + return message.sentTimestamp.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', @@ -67,14 +93,48 @@ function formatDisplayDate(message: ChatMessage): string { }); } -const AlwaysScrollToBottom = (props: Props) => { +const AlwaysScrollToBottom = (props: { newMessages: ChatMessage[] }) => { const elementRef = useRef(); useEffect(() => { // @ts-ignore elementRef.current.scrollIntoView(); - }, [props.messages]); + }, [props.newMessages]); // @ts-ignore return
; }; + +function IsThereNewMessages( + newValues: ChatMessage[], + currentValues: ChatMessage[] +): 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: ChatMessage[], + currentValues: ChatMessage[] +) { + const copy = currentValues.slice(); + newValues.forEach((msg) => { + if (!copy.find(isEqual.bind({}, msg))) { + copy.push(msg); + } + }); + copy.sort((a, b) => a.sentTimestamp.valueOf() - b.sentTimestamp.valueOf()); + return copy; +} + +function isEqual(lhs: ChatMessage, rhs: ChatMessage): boolean { + return ( + lhs.nick === rhs.nick && + lhs.message === rhs.message && + lhs.sentTimestamp.toString() === rhs.sentTimestamp.toString() + ); +} diff --git a/web-chat/src/ChatMessage.ts b/web-chat/src/ChatMessage.ts new file mode 100644 index 0000000000..8b8f696572 --- /dev/null +++ b/web-chat/src/ChatMessage.ts @@ -0,0 +1,19 @@ +import { ChatMessage as WakuChatMessage } from 'waku/chat_message'; + +export class ChatMessage { + constructor( + public receivedTimestampMs: Date, + public sentTimestamp: Date, + public nick: string, + public message: string + ) {} + + static fromWakuChatMessage(wakuChatMessage: WakuChatMessage): ChatMessage { + return new ChatMessage( + new Date(), + wakuChatMessage.timestamp, + wakuChatMessage.nick, + wakuChatMessage.message + ); + } +} diff --git a/web-chat/src/MessageInput.tsx b/web-chat/src/MessageInput.tsx index bdd2f4424a..d8a8bfe208 100644 --- a/web-chat/src/MessageInput.tsx +++ b/web-chat/src/MessageInput.tsx @@ -10,8 +10,7 @@ import { } from '@livechat/ui-kit'; interface Props { - messageHandler: (msg: string) => void; - sendMessage: (() => Promise) | undefined; + sendMessage: ((msg: string) => Promise) | undefined; } export default function MessageInput(props: Props) { @@ -20,14 +19,13 @@ export default function MessageInput(props: Props) { const sendMessage = async () => { if (props.sendMessage) { - await props.sendMessage(); + await props.sendMessage(inputText); setInputText(''); } }; const messageHandler = (event: ChangeEvent) => { setInputText(event.target.value); - props.messageHandler(event.target.value); }; const keyPressHandler = async (event: KeyboardEvent) => { diff --git a/web-chat/src/Room.tsx b/web-chat/src/Room.tsx index 1353819039..76ccc5cc4b 100644 --- a/web-chat/src/Room.tsx +++ b/web-chat/src/Room.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { ChatMessage } from 'waku/chat_message'; +import { ChatMessage } from './ChatMessage'; +import { ChatMessage as WakuChatMessage } from 'waku/chat_message'; import { WakuMessage } from 'waku/waku_message'; import { ChatContentTopic } from './App'; import ChatList from './ChatList'; @@ -8,13 +8,13 @@ import { useWaku } from './WakuContext'; import { TitleBar } from '@livechat/ui-kit'; interface Props { - lines: ChatMessage[]; + newMessages: ChatMessage[]; + archivedMessages: ChatMessage[]; commandHandler: (cmd: string) => void; nick: string; } export default function Room(props: Props) { - let [messageToSend, setMessageToSend] = useState(''); const { waku } = useWaku(); return ( @@ -23,12 +23,14 @@ export default function Room(props: Props) { style={{ height: '98vh', display: 'flex', flexDirection: 'column' }} > - + { + ? async (messageToSend) => { return handleMessage( messageToSend, props.nick, @@ -52,7 +54,7 @@ async function handleMessage( if (message.startsWith('/')) { commandHandler(message); } else { - const chatMessage = new ChatMessage(new Date(), nick, message); + const chatMessage = new WakuChatMessage(new Date(), nick, message); const wakuMsg = WakuMessage.fromBytes( chatMessage.encode(), ChatContentTopic diff --git a/web-chat/src/command.ts b/web-chat/src/command.ts index 1da11566d4..e6ee0e4835 100644 --- a/web-chat/src/command.ts +++ b/web-chat/src/command.ts @@ -78,6 +78,32 @@ function peers(waku: Waku | undefined): string[] { return response; } +function connections(waku: Waku | undefined): string[] { + if (!waku) { + return ['Waku node is starting']; + } + let response: string[] = []; + waku.libp2p.connections.forEach( + ( + connections: import('libp2p-interfaces/src/connection/connection')[], + peerId + ) => { + response.push(peerId + ':'); + let strConnections = ' connections: ['; + connections.forEach((connection) => { + strConnections += JSON.stringify(connection.stat); + strConnections += '; ' + JSON.stringify(connection.streams); + }); + strConnections += ']'; + response.push(strConnections); + } + ); + if (response.length === 0) { + response.push('Not connected to any peer.'); + } + return response; +} + export default function handleCommand( input: string, waku: Waku | undefined, @@ -102,6 +128,9 @@ export default function handleCommand( case '/peers': peers(waku).map((str) => response.push(str)); break; + case '/connections': + connections(waku).map((str) => response.push(str)); + break; default: response.push(`Unknown Command '${command}'`); }