feat: updated and refactored web-chat example (#214)
* make custom hooks * use @waku/react
This commit is contained in:
parent
52e3fb4afa
commit
9f9fa8646b
File diff suppressed because it is too large
Load Diff
|
@ -7,11 +7,12 @@
|
|||
"@libp2p/bootstrap": "^5.0.0",
|
||||
"@livechat/ui-kit": "^0.5.0-24",
|
||||
"@multiformats/multiaddr": "11.0.7",
|
||||
"@waku/react": "0.0.1-a",
|
||||
"@waku/byte-utils": "^0.0.2",
|
||||
"@waku/core": "^0.0.10",
|
||||
"@waku/create": "^0.0.4",
|
||||
"@waku/dns-discovery": "0.0.5",
|
||||
"@waku/interfaces": "^0.0.5",
|
||||
"@waku/interfaces": "^0.0.7",
|
||||
"@waku/peer-exchange": "^0.0.3",
|
||||
"process": "^0.11.10",
|
||||
"protons-runtime": "^3.1.0",
|
||||
|
|
|
@ -1,230 +1,51 @@
|
|||
import { useEffect, useReducer, useState } from "react";
|
||||
import "./App.css";
|
||||
import handleCommand from "./command";
|
||||
import Room from "./Room";
|
||||
import { WakuContext } from "./WakuContext";
|
||||
import { ThemeProvider } from "@livechat/ui-kit";
|
||||
import { generate } from "server-name-generator";
|
||||
import { Message } from "./Message";
|
||||
import { wakuDnsDiscovery } from "@waku/dns-discovery";
|
||||
import { wakuPeerExchangeDiscovery } from "@waku/peer-exchange";
|
||||
import { waitForRemotePeer } from "@waku/core";
|
||||
import { Protocols, WakuLight } from "@waku/interfaces";
|
||||
import { createLightNode } from "@waku/create";
|
||||
import { DecodedMessage, Decoder } from "@waku/core/lib/message/version_0";
|
||||
import { PageDirection } from "@waku/interfaces";
|
||||
import { PageDirection, LightNode } from "@waku/interfaces";
|
||||
|
||||
const themes = {
|
||||
AuthorName: {
|
||||
css: {
|
||||
fontSize: "1.1em",
|
||||
},
|
||||
},
|
||||
Message: {
|
||||
css: {
|
||||
margin: "0em",
|
||||
padding: "0em",
|
||||
fontSize: "0.83em",
|
||||
},
|
||||
},
|
||||
MessageText: {
|
||||
css: {
|
||||
margin: "0em",
|
||||
padding: "0.1em",
|
||||
paddingLeft: "1em",
|
||||
fontSize: "1.1em",
|
||||
},
|
||||
},
|
||||
MessageGroup: {
|
||||
css: {
|
||||
margin: "0em",
|
||||
padding: "0.2em",
|
||||
},
|
||||
},
|
||||
};
|
||||
import { useWaku, useContentPair } from "@waku/react";
|
||||
|
||||
export const ChatContentTopic = "/toy-chat/2/huilong/proto";
|
||||
const ChatDecoder = new Decoder(ChatContentTopic);
|
||||
import { useMessages, usePersistentNick } from "./hooks";
|
||||
|
||||
async function retrieveStoreMessages(
|
||||
waku: WakuLight,
|
||||
setArchivedMessages: (value: Message[]) => void
|
||||
): Promise<void> {
|
||||
const startTime = new Date();
|
||||
// Only retrieve a week of history
|
||||
startTime.setTime(Date.now() - 1000 * 60 * 60 * 24 * 7);
|
||||
|
||||
const endTime = new Date();
|
||||
|
||||
try {
|
||||
for await (const messagesPromises of waku.store.queryGenerator(
|
||||
[ChatDecoder],
|
||||
{
|
||||
pageSize: 5,
|
||||
pageDirection: PageDirection.FORWARD,
|
||||
timeFilter: {
|
||||
startTime,
|
||||
endTime,
|
||||
},
|
||||
}
|
||||
)) {
|
||||
const wakuMessages = await Promise.all(messagesPromises);
|
||||
|
||||
const messages: Message[] = [];
|
||||
wakuMessages
|
||||
.filter(isMessageDefined)
|
||||
.map((wakuMsg) => Message.fromWakuMessage(wakuMsg))
|
||||
.forEach((message) => {
|
||||
if (message) {
|
||||
messages.push(message);
|
||||
}
|
||||
});
|
||||
setArchivedMessages(messages);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to retrieve messages", e);
|
||||
}
|
||||
}
|
||||
const startTime = new Date();
|
||||
// Only retrieve a week of history
|
||||
startTime.setTime(Date.now() - 1000 * 60 * 60 * 24 * 7);
|
||||
const endTime = new Date();
|
||||
|
||||
export default function App() {
|
||||
const [messages, dispatchMessages] = useReducer(reduceMessages, []);
|
||||
const [waku, setWaku] = useState<WakuLight | 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);
|
||||
}, [nick]);
|
||||
|
||||
useEffect(() => {
|
||||
initWaku(setWaku)
|
||||
.then(() => console.log("Waku init done"))
|
||||
.catch((e) => console.log("Waku init failed ", e));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!waku) return;
|
||||
// Let's retrieve previous messages before listening to new messages
|
||||
if (!historicalMessagesRetrieved) return;
|
||||
|
||||
const handleIncomingMessage = (wakuMsg: DecodedMessage) => {
|
||||
console.log("Message received: ", wakuMsg);
|
||||
const msg = Message.fromWakuMessage(wakuMsg);
|
||||
if (msg) {
|
||||
dispatchMessages([msg]);
|
||||
}
|
||||
};
|
||||
|
||||
let unsubscribe: undefined | (() => Promise<void>);
|
||||
waku.filter.subscribe([ChatDecoder], handleIncomingMessage).then(
|
||||
(_unsubscribe) => {
|
||||
console.log("subscribed to ", ChatContentTopic);
|
||||
unsubscribe = _unsubscribe;
|
||||
const { node } = useWaku<LightNode>();
|
||||
const { decoder } = useContentPair();
|
||||
const [messages, pushLocalMessages] = useMessages({
|
||||
node,
|
||||
decoder,
|
||||
options: {
|
||||
pageSize: 5,
|
||||
pageDirection: PageDirection.FORWARD,
|
||||
timeFilter: {
|
||||
startTime,
|
||||
endTime,
|
||||
},
|
||||
(e) => {
|
||||
console.error("Failed to subscribe", e);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return function cleanUp() {
|
||||
if (!waku) return;
|
||||
if (typeof unsubscribe === "undefined") return;
|
||||
unsubscribe().then(
|
||||
() => {
|
||||
console.log("unsubscribed to ", ChatContentTopic);
|
||||
},
|
||||
(e) => console.error("Failed to unsubscribe", e)
|
||||
);
|
||||
};
|
||||
}, [waku, historicalMessagesRetrieved]);
|
||||
const [nick, setNick] = usePersistentNick();
|
||||
|
||||
useEffect(() => {
|
||||
if (!waku) return;
|
||||
if (historicalMessagesRetrieved) return;
|
||||
|
||||
const retrieveMessages = async () => {
|
||||
await waitForRemotePeer(waku, [
|
||||
Protocols.Store,
|
||||
Protocols.Filter,
|
||||
Protocols.LightPush,
|
||||
]);
|
||||
console.log(`Retrieving archived messages`);
|
||||
|
||||
try {
|
||||
retrieveStoreMessages(waku, dispatchMessages).then((length) => {
|
||||
console.log(`Messages retrieved:`, length);
|
||||
setHistoricalMessagesRetrieved(true);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(`Error encountered when retrieving archived messages`, e);
|
||||
}
|
||||
};
|
||||
|
||||
retrieveMessages();
|
||||
}, [waku, historicalMessagesRetrieved]);
|
||||
const onCommand = (text: string): void => {
|
||||
handleCommand(text, node, setNick).then(({ command, response }) => {
|
||||
const commandMessages = response.map((msg) => {
|
||||
return Message.fromUtf8String(command, msg);
|
||||
});
|
||||
pushLocalMessages(commandMessages);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat-app"
|
||||
style={{ height: "100vh", width: "100vw", overflow: "hidden" }}
|
||||
>
|
||||
<WakuContext.Provider value={{ waku: waku }}>
|
||||
<ThemeProvider theme={themes}>
|
||||
<Room
|
||||
nick={nick}
|
||||
messages={messages}
|
||||
commandHandler={(input: string) => {
|
||||
handleCommand(input, waku, setNick).then(
|
||||
({ command, response }) => {
|
||||
const commandMessages = response.map((msg) => {
|
||||
return Message.fromUtf8String(command, msg);
|
||||
});
|
||||
dispatchMessages(commandMessages);
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</WakuContext.Provider>
|
||||
<Room nick={nick} messages={messages} commandHandler={onCommand} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function initWaku(setter: (waku: WakuLight) => void) {
|
||||
try {
|
||||
const publicKey = "AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM";
|
||||
const fqdn = "test.waku.nodes.status.im";
|
||||
const enrTree = `enrtree://${publicKey}@${fqdn}`;
|
||||
const waku = await createLightNode({
|
||||
libp2p: {
|
||||
peerDiscovery: [
|
||||
wakuDnsDiscovery(enrTree, {
|
||||
store: 1,
|
||||
filter: 2,
|
||||
lightpush: 2,
|
||||
}),
|
||||
wakuPeerExchangeDiscovery(),
|
||||
],
|
||||
},
|
||||
});
|
||||
await waku.start();
|
||||
|
||||
setter(waku);
|
||||
} catch (e) {
|
||||
console.log("Issue starting waku ", e);
|
||||
}
|
||||
}
|
||||
|
||||
function reduceMessages(state: Message[], newMessages: Message[]) {
|
||||
return state.concat(newMessages);
|
||||
}
|
||||
|
||||
const isMessageDefined = (
|
||||
msg: DecodedMessage | undefined
|
||||
): msg is DecodedMessage => {
|
||||
return !!msg;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
Message as LiveMessage,
|
||||
MessageText,
|
||||
|
@ -10,8 +10,6 @@ interface Props {
|
|||
messages: Message[];
|
||||
}
|
||||
|
||||
memo(ChatList);
|
||||
|
||||
export default function ChatList(props: Props) {
|
||||
const renderedMessages = props.messages.map((message) => (
|
||||
<LiveMessage
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DecodedMessage } from "@waku/core/lib/message/version_0";
|
||||
import { IDecodedMessage } from "@waku/interfaces";
|
||||
import { ChatMessage } from "./chat_message";
|
||||
|
||||
export class Message {
|
||||
|
@ -11,7 +11,7 @@ export class Message {
|
|||
this.sentTimestamp = sentTimestamp;
|
||||
}
|
||||
|
||||
static fromWakuMessage(wakuMsg: DecodedMessage): Message | undefined {
|
||||
static fromWakuMessage(wakuMsg: IDecodedMessage): Message | undefined {
|
||||
if (wakuMsg.payload) {
|
||||
try {
|
||||
const chatMsg = ChatMessage.decode(wakuMsg.payload);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ChangeEvent, KeyboardEvent, useEffect, useState } from "react";
|
||||
import { useWaku } from "./WakuContext";
|
||||
import { useWaku } from "@waku/react";
|
||||
import { LightNode } from "@waku/interfaces";
|
||||
import {
|
||||
TextInput,
|
||||
TextComposer,
|
||||
|
@ -10,58 +11,54 @@ import {
|
|||
} from "@livechat/ui-kit";
|
||||
|
||||
interface Props {
|
||||
hasLightPushPeers: boolean;
|
||||
sendMessage: ((msg: string) => Promise<void>) | undefined;
|
||||
}
|
||||
|
||||
export default function MessageInput(props: Props) {
|
||||
const [inputText, setInputText] = useState<string>("");
|
||||
const [activeButton, setActiveButton] = useState<boolean>(false);
|
||||
const { waku } = useWaku();
|
||||
const { hasLightPushPeers } = props;
|
||||
const { node } = useWaku<LightNode>();
|
||||
|
||||
const sendMessage = async () => {
|
||||
const [inputText, setInputText] = useState<string>("");
|
||||
const [isActive, setActiveButton] = useState<boolean>(false);
|
||||
|
||||
const onMessage = async () => {
|
||||
if (props.sendMessage) {
|
||||
await props.sendMessage(inputText);
|
||||
setInputText("");
|
||||
}
|
||||
};
|
||||
|
||||
const messageHandler = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputText(event.target.value);
|
||||
};
|
||||
|
||||
const keyPressHandler = async (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
const onKeyDown = async (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.altKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
await sendMessage();
|
||||
await onMessage();
|
||||
}
|
||||
};
|
||||
|
||||
// Enable the button if there are peers available or the user is sending a command
|
||||
useEffect(() => {
|
||||
if (inputText.startsWith("/")) {
|
||||
if (inputText.startsWith("/") || hasLightPushPeers) {
|
||||
setActiveButton(true);
|
||||
} else if (waku) {
|
||||
(async () => {
|
||||
const peers = await waku.lightPush.peers();
|
||||
if (!!peers) {
|
||||
setActiveButton(true);
|
||||
} else {
|
||||
setActiveButton(false);
|
||||
}
|
||||
})();
|
||||
} else if (node) {
|
||||
setActiveButton(false);
|
||||
}
|
||||
}, [activeButton, inputText, waku]);
|
||||
}, [node, inputText, hasLightPushPeers]);
|
||||
|
||||
return (
|
||||
<TextComposer
|
||||
onKeyDown={keyPressHandler}
|
||||
onChange={messageHandler}
|
||||
active={activeButton}
|
||||
onButtonClick={sendMessage}
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={onChange}
|
||||
active={isActive}
|
||||
onButtonClick={onMessage}
|
||||
>
|
||||
<Row align="center">
|
||||
<Fill>
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import type { Message as WakuMessage } from "@waku/interfaces";
|
||||
import { ChatContentTopic } from "./App";
|
||||
import type { LightNode } from "@waku/interfaces";
|
||||
import ChatList from "./ChatList";
|
||||
import MessageInput from "./MessageInput";
|
||||
import { useWaku } from "./WakuContext";
|
||||
import { useWaku, useContentPair, useLightPush, usePeers } from "@waku/react";
|
||||
import { TitleBar } from "@livechat/ui-kit";
|
||||
import { Message } from "./Message";
|
||||
import { ChatMessage } from "./chat_message";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Encoder } from "@waku/core/lib/message/version_0";
|
||||
import { useNodePeers } from "./hooks";
|
||||
|
||||
interface Props {
|
||||
messages: Message[];
|
||||
|
@ -16,50 +14,39 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function Room(props: Props) {
|
||||
const { waku } = useWaku();
|
||||
const { node } = useWaku<LightNode>();
|
||||
const { encoder } = useContentPair();
|
||||
const { push: onPush } = useLightPush({ node, encoder });
|
||||
|
||||
const [storePeers, setStorePeers] = useState(0);
|
||||
const [filterPeers, setFilterPeers] = useState(0);
|
||||
const [lightPushPeers, setLightPushPeers] = useState(0);
|
||||
const { bootstrapPeers, peerExchangePeers } = useNodePeers(node);
|
||||
const { storePeers, filterPeers, lightPushPeers } = usePeers({ node });
|
||||
|
||||
const [bootstrapPeers, setBootstrapPeers] = useState(new Set<string>());
|
||||
const [peerExchangePeers, setPeerExchangePeers] = useState(new Set<string>());
|
||||
const onSend = async (text: string) => {
|
||||
if (!onPush) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ChatEncoder = new Encoder(ChatContentTopic);
|
||||
|
||||
useEffect(() => {
|
||||
if (!waku) return;
|
||||
|
||||
// Update store peer when new peer connected & identified
|
||||
waku.libp2p.peerStore.addEventListener("change:protocols", async (evt) => {
|
||||
const { peerId } = evt.detail;
|
||||
const tags = (await waku.libp2p.peerStore.getTags(peerId)).map(
|
||||
(t) => t.name
|
||||
if (text.startsWith("/")) {
|
||||
props.commandHandler(text);
|
||||
} else {
|
||||
const timestamp = new Date();
|
||||
const chatMessage = ChatMessage.fromUtf8String(
|
||||
timestamp,
|
||||
props.nick,
|
||||
text
|
||||
);
|
||||
if (tags.includes("peer-exchange")) {
|
||||
setPeerExchangePeers((peers) => new Set(peers).add(peerId.toString()));
|
||||
} else {
|
||||
setBootstrapPeers((peers) => new Set(peers).add(peerId.toString()));
|
||||
}
|
||||
const payload = chatMessage.encode();
|
||||
|
||||
const storePeers = await waku.store.peers();
|
||||
setStorePeers(storePeers.length);
|
||||
await onPush({ payload, timestamp });
|
||||
}
|
||||
};
|
||||
|
||||
const filterPeers = await waku.filter.peers();
|
||||
setFilterPeers(filterPeers.length);
|
||||
const lightPushPeersLength = orZero(lightPushPeers?.length);
|
||||
const filterPeersLength = orZero(filterPeers?.length);
|
||||
const storePeersLength = orZero(storePeers?.length);
|
||||
|
||||
const lightPushPeers = await waku.lightPush.peers();
|
||||
setLightPushPeers(lightPushPeers.length);
|
||||
});
|
||||
}, [waku]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Bootstrap Peers:");
|
||||
console.table(bootstrapPeers);
|
||||
|
||||
console.log("Peer Exchange Peers:");
|
||||
console.table(peerExchangePeers);
|
||||
}, [bootstrapPeers, peerExchangePeers]);
|
||||
const peersMessage = `Peers: ${lightPushPeersLength} light push, ${filterPeersLength} filter, ${storePeersLength} store.`;
|
||||
const bootstrapPeersMessage = `Bootstrap (DNS Discovery): ${bootstrapPeers.size}, Peer exchange: ${peerExchangePeers.size}. `;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -67,49 +54,16 @@ export default function Room(props: Props) {
|
|||
style={{ height: "98vh", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<TitleBar
|
||||
leftIcons={[
|
||||
`Peers: ${lightPushPeers} light push, ${filterPeers} filter, ${storePeers} store.`,
|
||||
]}
|
||||
rightIcons={[
|
||||
`Bootstrap (DNS Discovery): ${bootstrapPeers.size}, Peer exchange: ${peerExchangePeers.size}. `,
|
||||
"View console for more details.",
|
||||
]}
|
||||
leftIcons={[peersMessage]}
|
||||
rightIcons={[bootstrapPeersMessage, "View console for more details."]}
|
||||
title="Waku v2 chat app"
|
||||
/>
|
||||
<ChatList messages={props.messages} />
|
||||
<MessageInput
|
||||
sendMessage={
|
||||
waku
|
||||
? async (messageToSend) => {
|
||||
return handleMessage(
|
||||
messageToSend,
|
||||
props.nick,
|
||||
props.commandHandler,
|
||||
async (msg) => {
|
||||
await waku.lightPush.push(ChatEncoder, msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<MessageInput hasLightPushPeers={!!lightPushPeers} sendMessage={onSend} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleMessage(
|
||||
message: string,
|
||||
nick: string,
|
||||
commandHandler: (cmd: string) => void,
|
||||
sender: (wakuMsg: Partial<WakuMessage>) => Promise<void>
|
||||
) {
|
||||
if (message.startsWith("/")) {
|
||||
commandHandler(message);
|
||||
} else {
|
||||
const timestamp = new Date();
|
||||
const chatMessage = ChatMessage.fromUtf8String(timestamp, nick, message);
|
||||
const payload = chatMessage.encode();
|
||||
|
||||
await sender({ payload, timestamp });
|
||||
}
|
||||
function orZero(value: undefined | number): number {
|
||||
return value || 0;
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { createContext, useContext } from "react";
|
||||
import type { WakuLight } from "@waku/interfaces";
|
||||
|
||||
export type WakuContextType = {
|
||||
waku?: WakuLight;
|
||||
};
|
||||
|
||||
export const WakuContext = createContext<WakuContextType>({ waku: undefined });
|
||||
export const useWaku = () => useContext(WakuContext);
|
|
@ -1,5 +1,5 @@
|
|||
import { multiaddr } from "@multiformats/multiaddr";
|
||||
import type { WakuLight } from "@waku/interfaces";
|
||||
import type { LightNode } from "@waku/interfaces";
|
||||
|
||||
function help(): string[] {
|
||||
return [
|
||||
|
@ -21,7 +21,7 @@ function nick(
|
|||
return [`New nick: ${nick}`];
|
||||
}
|
||||
|
||||
function info(waku: WakuLight | undefined): string[] {
|
||||
function info(waku: LightNode | undefined): string[] {
|
||||
if (!waku) {
|
||||
return ["Waku node is starting"];
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ function info(waku: WakuLight | undefined): string[] {
|
|||
|
||||
function connect(
|
||||
peer: string | undefined,
|
||||
waku: WakuLight | undefined
|
||||
waku: LightNode | undefined
|
||||
): string[] {
|
||||
if (!waku) {
|
||||
return ["Waku node is starting"];
|
||||
|
@ -55,7 +55,7 @@ function connect(
|
|||
}
|
||||
}
|
||||
|
||||
async function peers(waku: WakuLight | undefined): Promise<string[]> {
|
||||
async function peers(waku: LightNode | undefined): Promise<string[]> {
|
||||
if (!waku) {
|
||||
return ["Waku node is starting"];
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ async function peers(waku: WakuLight | undefined): Promise<string[]> {
|
|||
return response;
|
||||
}
|
||||
|
||||
function connections(waku: WakuLight | undefined): string[] {
|
||||
function connections(waku: LightNode | undefined): string[] {
|
||||
if (!waku) {
|
||||
return ["Waku node is starting"];
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ function connections(waku: WakuLight | undefined): string[] {
|
|||
|
||||
export default async function handleCommand(
|
||||
input: string,
|
||||
waku: WakuLight | undefined,
|
||||
waku: LightNode | undefined,
|
||||
setNick: (nick: string) => void
|
||||
): Promise<{ command: string; response: string[] }> {
|
||||
let response: string[] = [];
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Protocols } from "@waku/interfaces";
|
||||
|
||||
export const CONTENT_TOPIC = "/toy-chat/2/huilong/proto";
|
||||
|
||||
const PUBLIC_KEY = "AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM";
|
||||
const FQDN = "test.waku.nodes.status.im";
|
||||
export const ENR_TREE = `enrtree://${PUBLIC_KEY}@${FQDN}`;
|
||||
|
||||
export const PROTOCOLS = [
|
||||
Protocols.Filter,
|
||||
Protocols.Store,
|
||||
Protocols.LightPush,
|
||||
];
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { generate } from "server-name-generator";
|
||||
import { Message } from "./Message";
|
||||
import { Decoder } from "@waku/core/lib/message/version_0";
|
||||
import { LightNode, StoreQueryOptions } from "@waku/interfaces";
|
||||
|
||||
import { useFilterMessages, useStoreMessages } from "@waku/react";
|
||||
|
||||
export const usePersistentNick = (): [
|
||||
string,
|
||||
React.Dispatch<React.SetStateAction<string>>
|
||||
] => {
|
||||
const [nick, setNick] = useState<string>(() => {
|
||||
const persistedNick = window.localStorage.getItem("nick");
|
||||
return persistedNick !== null ? persistedNick : generate();
|
||||
});
|
||||
useEffect(() => {
|
||||
localStorage.setItem("nick", nick);
|
||||
}, [nick]);
|
||||
|
||||
return [nick, setNick];
|
||||
};
|
||||
|
||||
type UseMessagesParams = {
|
||||
node: undefined | LightNode;
|
||||
decoder: undefined | Decoder;
|
||||
options: StoreQueryOptions;
|
||||
};
|
||||
|
||||
type UseMessagesResult = [Message[], (v: Message[]) => void];
|
||||
|
||||
export const useMessages = (params: UseMessagesParams): UseMessagesResult => {
|
||||
const { messages: newMessages } = useFilterMessages(params);
|
||||
const { messages: storedMessages } = useStoreMessages(params);
|
||||
const [localMessages, setLocalMessages] = useState<Message[]>([]);
|
||||
|
||||
const pushMessages = (msgs: Message[]) => {
|
||||
if (!msgs || !msgs.length) {
|
||||
return;
|
||||
}
|
||||
setLocalMessages((prev) => [...prev, ...msgs]);
|
||||
};
|
||||
|
||||
const allMessages = React.useMemo((): Message[] => {
|
||||
return [...storedMessages, ...newMessages]
|
||||
.map(Message.fromWakuMessage)
|
||||
.concat(localMessages)
|
||||
.filter((v): v is Message => !!v)
|
||||
.sort(
|
||||
(left, right) => left.timestamp.getTime() - right.timestamp.getTime()
|
||||
);
|
||||
}, [storedMessages, newMessages, localMessages]);
|
||||
|
||||
return [allMessages, pushMessages];
|
||||
};
|
||||
|
||||
// can be safely ignored
|
||||
// this is for experiments on waku side around new discovery options
|
||||
export const useNodePeers = (node: undefined | LightNode) => {
|
||||
const [bootstrapPeers, setBootstrapPeers] = useState(new Set<string>());
|
||||
const [peerExchangePeers, setPeerExchangePeers] = useState(new Set<string>());
|
||||
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
|
||||
// Update store peer when new peer connected & identified
|
||||
node.libp2p.peerStore.addEventListener("change:protocols", async (evt) => {
|
||||
const { peerId } = evt.detail;
|
||||
const tags = (await node.libp2p.peerStore.getTags(peerId)).map(
|
||||
(t) => t.name
|
||||
);
|
||||
if (tags.includes("peer-exchange")) {
|
||||
setPeerExchangePeers((peers) => new Set(peers).add(peerId.toString()));
|
||||
} else {
|
||||
setBootstrapPeers((peers) => new Set(peers).add(peerId.toString()));
|
||||
}
|
||||
});
|
||||
}, [node]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Bootstrap Peers:");
|
||||
console.table(bootstrapPeers);
|
||||
|
||||
console.log("Peer Exchange Peers:");
|
||||
console.table(peerExchangePeers);
|
||||
}, [bootstrapPeers, peerExchangePeers]);
|
||||
|
||||
return {
|
||||
bootstrapPeers,
|
||||
peerExchangePeers,
|
||||
};
|
||||
};
|
|
@ -1,11 +1,65 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { ThemeProvider } from "@livechat/ui-kit";
|
||||
import { LightNodeProvider, ContentPairProvider } from "@waku/react";
|
||||
import { wakuDnsDiscovery } from "@waku/dns-discovery";
|
||||
import { wakuPeerExchangeDiscovery } from "@waku/peer-exchange";
|
||||
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { ENR_TREE, CONTENT_TOPIC, PROTOCOLS } from "./config";
|
||||
|
||||
const NODE_OPTIONS = {
|
||||
libp2p: {
|
||||
peerDiscovery: [
|
||||
wakuDnsDiscovery(ENR_TREE, {
|
||||
store: 1,
|
||||
filter: 2,
|
||||
lightpush: 2,
|
||||
}),
|
||||
wakuPeerExchangeDiscovery(),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const THEMES = {
|
||||
AuthorName: {
|
||||
css: {
|
||||
fontSize: "1.1em",
|
||||
},
|
||||
},
|
||||
Message: {
|
||||
css: {
|
||||
margin: "0em",
|
||||
padding: "0em",
|
||||
fontSize: "0.83em",
|
||||
},
|
||||
},
|
||||
MessageText: {
|
||||
css: {
|
||||
margin: "0em",
|
||||
padding: "0.1em",
|
||||
paddingLeft: "1em",
|
||||
fontSize: "1.1em",
|
||||
},
|
||||
},
|
||||
MessageGroup: {
|
||||
css: {
|
||||
margin: "0em",
|
||||
padding: "0.2em",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider theme={THEMES}>
|
||||
<LightNodeProvider options={NODE_OPTIONS} protocols={PROTOCOLS}>
|
||||
<ContentPairProvider contentTopic={CONTENT_TOPIC}>
|
||||
<App />
|
||||
</ContentPairProvider>
|
||||
</LightNodeProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue