chore: clone web-chat for ethdenver

This commit is contained in:
danisharora099 2023-02-28 13:06:24 -07:00
parent 9f9fa8646b
commit 408f1f9d3f
No known key found for this signature in database
GPG Key ID: FBD2BF500037F135
33 changed files with 23756 additions and 0 deletions

View File

@ -0,0 +1,131 @@
{
"version": "0.1",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en",
"words": [
"AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM",
"asym",
"backoff",
"backoffs",
"bitjson",
"bitauth",
"bufbuild",
"chainsafe",
"cimg",
"ciphertext",
"circleci",
"codecov",
"commitlint",
"dependabot",
"dingpu",
"Dlazy",
"dnsaddr",
"Dout",
"Dscore",
"ecies",
"editorconfig",
"enr",
"enrs",
"enrtree",
"ephem",
"esnext",
"ethersproject",
"execa",
"exponentiate",
"fanout",
"floodsub",
"fontsource",
"globby",
"gossipsub",
"huilong",
"iasked",
"ihave",
"ihaves",
"ineed",
"ipfs",
"iwant",
"jdev",
"jswaku",
"keccak",
"lastpub",
"libauth",
"libp",
"lightpush",
"livechat",
"mkdir",
"multiaddr",
"multiaddresses",
"multiaddrs",
"multicodec",
"multicodecs",
"multiformats",
"mplex",
"multihashes",
"muxed",
"muxer",
"muxers",
"mvps",
"nodekey",
"nwaku",
"opendns",
"peerhave",
"portfinder",
"prettierignore",
"proto",
"protobuf",
"protoc",
"reactjs",
"recid",
"rlnrelay",
"roadmap",
"sandboxed",
"scanf",
"secio",
"seckey",
"secp",
"sscanf",
"staticnode",
"statusim",
"submodule",
"submodules",
"supercrypto",
"transpiled",
"typedoc",
"unencrypted",
"unmarshal",
"unmount",
"unmounts",
"untracked",
"upgrader",
"vacp",
"varint",
"waku",
"wakuconnect",
"wakuv",
"wakunode",
"webfonts",
"websockets",
"wifi",
"xsalsa20",
"Alives"
],
"flagWords": [],
"ignorePaths": [
"package.json",
"package-lock.json",
"yarn.lock",
"tsconfig.json",
"node_modules/**",
"build",
"gen",
"proto",
"*.spec.ts"
],
"patterns": [
{
"name": "import",
"pattern": "/import .*/"
}
],
"ignoreRegExpList": ["import"]
}

View File

@ -0,0 +1,3 @@
# Remove ReactJS warning about webpack
# because this is not a monorepo, ReactJS projects are examples
SKIP_PREFLIGHT_CHECK=true

23
examples/web-chat-dev/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,3 @@
# package.json is formatted by package managers, so we ignore it here
package.json
gen

View File

@ -0,0 +1,25 @@
# Web Chat App
**Demonstrates**:
- Group chat
- React/TypeScript
- Waku Filter
- Waku Light Push
- Waku Store
- Protobuf using `protons`
A ReactJS chat app is provided as a showcase of the library used in the browser.
It implements [Waku v2 Toy Chat](https://rfc.vac.dev/spec/22/) protocol.
A deployed version is available at https://examples.waku.org/web-chat/.
To run a development version locally, do:
```shell
git clone https://github.com/waku-org/js-waku-examples
cd web-chat
npm install
npm run start
```
Use `/help` to see the available commands.

View File

@ -0,0 +1,6 @@
version: v1beta1
plugins:
- name: ts_proto
out: ./src/proto
opt: grpc_js,esModuleInterop=true

View File

@ -0,0 +1,5 @@
version: v1beta1
build:
roots:
- ./src/proto

22525
examples/web-chat-dev/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
{
"name": "web-chat-dev",
"version": "0.1.0",
"private": true,
"homepage": "/web-chat",
"dependencies": {
"@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.7",
"@waku/peer-exchange": "^0.0.3",
"process": "^0.11.10",
"protons-runtime": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"server-name-generator": "^1.0.5",
"uint8arraylist": "^2.3.3"
},
"devDependencies": {
"@types/jest": "^27.5.2",
"@types/node": "^17.0.45",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"cspell": "^6.14.3",
"gh-pages": "^4.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"protons": "^5.1.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.3",
"url": "^0.11.0"
},
"scripts": {
"start": "GENERATE_SOURCEMAP=false PORT=3003 react-scripts start",
"build": "react-scripts build",
"test:unit": "exit 0",
"fix": "run-s fix:*",
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts --ext .tsx",
"test:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --list-different",
"test:spelling": "cspell \"{README.md,.github/*.md,src/**/*.{ts,tsx},public/**/*.html}\" -c ./.cspell.json",
"fix:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --write",
"fix:lint": "eslint src --ext .ts --ext .tsx --fix",
"proto": "protons src/proto/*.proto",
"js-waku:build": "cd ../; npm run build",
"predeploy": "run-s js-waku:build build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not ie <= 99",
"not android <= 4.4.4",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<meta name="description" content="Chat app powered by js-waku" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Waku v2 chat app</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,20 @@
{
"short_name": "Waku v2 Chat",
"name": "Chat app powered by js-waku",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,40 @@
import "./App.css";
import Room from "./Room";
import { PageDirection, LightNode } from "@waku/interfaces";
import { useWaku, useContentPair } from "@waku/react";
import { useMessages, usePersistentNick } from "./hooks";
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 { node } = useWaku<LightNode>();
const { decoder } = useContentPair();
const [messages] = useMessages({
node,
decoder,
options: {
pageSize: 5,
pageDirection: PageDirection.FORWARD,
timeFilter: {
startTime,
endTime,
},
},
});
const [nick] = usePersistentNick();
return (
<div
className="chat-app"
style={{ height: "100vh", width: "100vw", overflow: "hidden" }}
>
<Room nick={nick} messages={messages} />
</div>
);
}

View File

@ -0,0 +1,57 @@
import { useEffect, useRef } from "react";
import {
Message as LiveMessage,
MessageText,
MessageList,
} from "@livechat/ui-kit";
import { Message } from "./Message";
interface Props {
messages: Message[];
}
export default function ChatList(props: Props) {
const renderedMessages = props.messages.map((message) => (
<LiveMessage
key={
message.nick +
message.payloadAsUtf8 +
message.timestamp.valueOf() +
message.sentTimestamp?.valueOf()
}
authorName={message.nick}
date={formatDisplayDate(message)}
>
<MessageText>{message.payloadAsUtf8}</MessageText>
</LiveMessage>
));
return (
<MessageList active containScrollInSubtree>
{renderedMessages}
<AlwaysScrollToBottom messages={props.messages} />
</MessageList>
);
}
function formatDisplayDate(message: Message): string {
return message.timestamp.toLocaleString([], {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: false,
});
}
const AlwaysScrollToBottom = (props: { messages: Message[] }) => {
const elementRef = useRef<HTMLDivElement>();
useEffect(() => {
// @ts-ignore
elementRef.current.scrollIntoView();
}, [props.messages]);
// @ts-ignore
return <div ref={elementRef} />;
};

View File

@ -0,0 +1,44 @@
import { IDecodedMessage } from "@waku/interfaces";
import { ChatMessage } from "./chat_message";
export class Message {
public chatMessage: ChatMessage;
// WakuMessage timestamp
public sentTimestamp: Date | undefined;
constructor(chatMessage: ChatMessage, sentTimestamp: Date | undefined) {
this.chatMessage = chatMessage;
this.sentTimestamp = sentTimestamp;
}
static fromWakuMessage(wakuMsg: IDecodedMessage): Message | undefined {
if (wakuMsg.payload) {
try {
const chatMsg = ChatMessage.decode(wakuMsg.payload);
if (chatMsg) {
return new Message(chatMsg, wakuMsg.timestamp);
}
} catch (e) {
console.error("Failed to decode chat message", e);
}
}
return;
}
static fromUtf8String(nick: string, text: string): Message {
const now = new Date();
return new Message(ChatMessage.fromUtf8String(now, nick, text), now);
}
get nick() {
return this.chatMessage.nick;
}
get timestamp() {
return this.chatMessage.timestamp;
}
get payloadAsUtf8() {
return this.chatMessage.payloadAsUtf8;
}
}

View File

@ -0,0 +1,73 @@
import { ChangeEvent, KeyboardEvent, useEffect, useState } from "react";
import { useWaku } from "@waku/react";
import { LightNode } from "@waku/interfaces";
import {
TextInput,
TextComposer,
Row,
Fill,
Fit,
SendButton,
} from "@livechat/ui-kit";
interface Props {
hasLightPushPeers: boolean;
sendMessage: ((msg: string) => Promise<void>) | undefined;
}
export default function MessageInput(props: Props) {
const { hasLightPushPeers } = props;
const { node } = useWaku<LightNode>();
const [inputText, setInputText] = useState<string>("");
const [isActive, setActiveButton] = useState<boolean>(false);
const sendMessage = async () => {
if (props.sendMessage) {
await props.sendMessage(inputText);
setInputText("");
}
};
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value);
};
const onKeyDown = async (event: KeyboardEvent<HTMLInputElement>) => {
if (
event.key === "Enter" &&
!event.altKey &&
!event.ctrlKey &&
!event.shiftKey
) {
await onMessage();
}
};
// Enable the button if there are peers available or the user is sending a command
useEffect(() => {
if (inputText.startsWith("/") || hasLightPushPeers) {
setActiveButton(true);
} else if (node) {
setActiveButton(false);
}
}, [node, inputText, hasLightPushPeers]);
return (
<TextComposer
onKeyDown={onKeyDown}
onChange={onChange}
active={isActive}
onButtonClick={sendMessage}
>
<Row align="center">
<Fill>
<TextInput value={inputText} />
</Fill>
<Fit>
<SendButton />
</Fit>
</Row>
</TextComposer>
);
}

View File

@ -0,0 +1,64 @@
import type { LightNode } from "@waku/interfaces";
import ChatList from "./ChatList";
import MessageInput from "./MessageInput";
import { useWaku, useContentPair, useLightPush, usePeers } from "@waku/react";
import { TitleBar } from "@livechat/ui-kit";
import { Message } from "./Message";
import { ChatMessage } from "./chat_message";
import { useNodePeers } from "./hooks";
interface Props {
messages: Message[];
nick: string;
}
export default function Room(props: Props) {
const { node } = useWaku<LightNode>();
const { storePeers, filterPeers, lightPushPeers } = usePeers({ node });
const { bootstrapPeers, peerExchangePeers } = useNodePeers(node);
const { encoder } = useContentPair();
const { push: _sendMessage } = useLightPush({ node, encoder });
const sendMessage = async (text: string) => {
if (!_sendMessage) {
return;
}
const timestamp = new Date();
const chatMessage = ChatMessage.fromUtf8String(timestamp, props.nick, text);
const payload = chatMessage.encode();
await _sendMessage({ payload, timestamp });
};
const lightPushPeersLength = orZero(lightPushPeers?.length);
const filterPeersLength = orZero(filterPeers?.length);
const storePeersLength = orZero(storePeers?.length);
const peersMessage = `Peers: ${lightPushPeersLength} light push, ${filterPeersLength} filter, ${storePeersLength} store.`;
const bootstrapPeersMessage = `Bootstrap (DNS Discovery): ${bootstrapPeers.size}, Peer exchange: ${peerExchangePeers.size}. `;
return (
<div
className="chat-container"
style={{ height: "98vh", display: "flex", flexDirection: "column" }}
>
<TitleBar
leftIcons={[peersMessage]}
rightIcons={[bootstrapPeersMessage, "View console for more details."]}
title="Waku v2 chat app"
/>
<ChatList messages={props.messages} />
<MessageInput
hasLightPushPeers={!!lightPushPeers}
sendMessage={sendMessage}
/>
</div>
);
}
function orZero(value: undefined | number): number {
return value || 0;
}

View File

@ -0,0 +1,64 @@
import { utf8ToBytes, bytesToUtf8 } from "@waku/byte-utils";
import * as proto from "./proto/chat_message";
/**
* ChatMessage is used by the various show case waku apps that demonstrates
* waku used as the network layer for chat group applications.
*
* This is included to help building PoC and MVPs. Apps that aim to be
* production ready should use a more appropriate data structure.
*/
export class ChatMessage {
public constructor(public proto: proto.ChatMessage) {}
/**
* Create Chat Message with a utf-8 string as payload.
*/
static fromUtf8String(
timestamp: Date,
nick: string,
text: string
): ChatMessage {
const timestampNumber = BigInt(Math.floor(timestamp.valueOf() / 1000));
const payload = utf8ToBytes(text);
return new ChatMessage({
timestamp: timestampNumber,
nick,
payload,
});
}
/**
* Decode a protobuf payload to a ChatMessage.
* @param bytes The payload to decode.
*/
static decode(bytes: Uint8Array): ChatMessage {
const protoMsg = proto.ChatMessage.decode(bytes);
return new ChatMessage(protoMsg);
}
/**
* Encode this ChatMessage to a byte array, to be used as a protobuf payload.
* @returns The encoded payload.
*/
encode(): Uint8Array {
return proto.ChatMessage.encode(this.proto);
}
get timestamp(): Date {
return new Date(Number(BigInt(this.proto.timestamp) * BigInt(1000)));
}
get nick(): string {
return this.proto.nick;
}
get payloadAsUtf8(): string {
if (!this.proto.payload) {
return "";
}
return bytesToUtf8(this.proto.payload);
}
}

View File

@ -0,0 +1,140 @@
import { multiaddr } from "@multiformats/multiaddr";
import type { LightNode } from "@waku/interfaces";
function help(): string[] {
return [
"/nick <nickname>: set a new nickname",
"/info: some information about the node",
"/connect <Multiaddr>: connect to the given peer",
"/help: Display this help",
];
}
function nick(
nick: string | undefined,
setNick: (nick: string) => void
): string[] {
if (!nick) {
return ["No nick provided"];
}
setNick(nick);
return [`New nick: ${nick}`];
}
function info(waku: LightNode | undefined): string[] {
if (!waku) {
return ["Waku node is starting"];
}
return [`PeerId: ${waku.libp2p.peerId.toString()}`];
}
function connect(
peer: string | undefined,
waku: LightNode | undefined
): string[] {
if (!waku) {
return ["Waku node is starting"];
}
if (!peer) {
return ["No peer provided"];
}
try {
const peerMultiaddr = multiaddr(peer);
const peerId = peerMultiaddr.getPeerId();
if (!peerId) {
return ["Peer Id needed to dial"];
}
waku
.dial(peerMultiaddr)
.catch((e) => console.error(`Failed to dial ${peerMultiaddr}`, e));
return [
`${peerId}: ${peerMultiaddr.toString()} added to address book, autodial in progress`,
];
} catch (e) {
return ["Invalid multiaddr: " + e];
}
}
async function peers(waku: LightNode | undefined): Promise<string[]> {
if (!waku) {
return ["Waku node is starting"];
}
let response: string[] = [];
const peers = await waku.libp2p.peerStore.all();
Array.from(peers).forEach((peer) => {
response.push(peer.id.toString() + ":");
let addresses = " addresses: [";
peer.addresses.forEach(({ multiaddr }) => {
addresses += " " + multiaddr.toString() + ",";
});
addresses = addresses.replace(/,$/, "");
addresses += "]";
response.push(addresses);
let protocols = " protocols: [";
protocols += peer.protocols;
protocols += "]";
response.push(protocols);
});
if (response.length === 0) {
response.push("Not connected to any peer.");
}
return response;
}
function connections(waku: LightNode | undefined): string[] {
if (!waku) {
return ["Waku node is starting"];
}
let response: string[] = [];
let strConnections = " connections: \n";
waku.libp2p.connectionManager.getConnections().forEach((connection) => {
strConnections += connection.remotePeer.toString() + ", ";
strConnections += JSON.stringify(connection.stat);
strConnections += "; " + JSON.stringify(connection.streams);
strConnections += "\n";
});
response.push(strConnections);
if (response.length === 0) {
response.push("Not connected to any peer.");
}
return response;
}
export default async function handleCommand(
input: string,
waku: LightNode | undefined,
setNick: (nick: string) => void
): Promise<{ command: string; response: string[] }> {
let response: string[] = [];
const args = parseInput(input);
const command = args.shift()!;
switch (command) {
case "/help":
help().map((str) => response.push(str));
break;
case "/nick":
nick(args.shift(), setNick).map((str) => response.push(str));
break;
case "/info":
info(waku).map((str) => response.push(str));
break;
case "/connect":
connect(args.shift(), waku).map((str) => response.push(str));
break;
case "/peers":
(await peers(waku)).map((str) => response.push(str));
break;
case "/connections":
connections(waku).map((str) => response.push(str));
break;
default:
response.push(`Unknown Command '${command}'`);
}
return { command, response };
}
export function parseInput(input: string): string[] {
const clean = input.trim().replaceAll(/\s\s+/g, " ");
return clean.split(" ");
}

View File

@ -0,0 +1,13 @@
import { Protocols } from "@waku/interfaces";
export const CONTENT_TOPIC = "/toy-chat/2/ethdenver/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,
];

View File

@ -0,0 +1,46 @@
import { wakuDnsDiscovery } from "@waku/dns-discovery";
import { wakuPeerExchangeDiscovery } from "@waku/peer-exchange";
import { ENR_TREE } from "./config";
export const NODE_OPTIONS = {
libp2p: {
peerDiscovery: [
wakuDnsDiscovery(ENR_TREE, {
store: 1,
filter: 2,
lightpush: 2,
}),
wakuPeerExchangeDiscovery(),
],
},
};
export 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",
},
},
};

View File

@ -0,0 +1,107 @@
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;
node.libp2p.peerStore.all().then(async (peers) => {
for (const peer of peers) {
const tags = (await node.libp2p.peerStore.getTags(peer.id)).map(
(t) => t.name
);
if (tags.includes("peer-exchange")) {
setPeerExchangePeers((peers) =>
new Set(peers).add(peer.id.toString())
);
} else {
setBootstrapPeers((peers) => new Set(peers).add(peer.id.toString()));
}
}
});
// 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,
};
};

View File

@ -0,0 +1,30 @@
@import-normalize; /* bring in normalize.css styles */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.room-row {
text-align: left;
margin-left: 20px;
}
.room-row:after {
clear: both;
content: "";
display: table;
}
.chat-room {
margin: 2px;
}

View File

@ -0,0 +1,22 @@
import React from "react";
import ReactDOM from "react-dom";
import { ThemeProvider } from "@livechat/ui-kit";
import { LightNodeProvider, ContentPairProvider } from "@waku/react";
import "./index.css";
import App from "./App";
import { CONTENT_TOPIC, PROTOCOLS } from "./config";
import { NODE_OPTIONS, THEMES } from "./constants";
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={THEMES}>
<LightNodeProvider options={NODE_OPTIONS} protocols={PROTOCOLS}>
<ContentPairProvider contentTopic={CONTENT_TOPIC}>
<App />
</ContentPairProvider>
</LightNodeProvider>
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);

View File

@ -0,0 +1,7 @@
syntax = "proto3";
message ChatMessage {
uint64 timestamp = 1;
string nick = 2;
bytes payload = 3;
}

View File

@ -0,0 +1,117 @@
/* eslint-disable import/export */
/* eslint-disable @typescript-eslint/no-namespace */
import { encodeMessage, decodeMessage, message } from "protons-runtime";
import type { Uint8ArrayList } from "uint8arraylist";
import type { Codec } from "protons-runtime";
export interface ChatMessage {
timestamp: bigint;
nick: string;
payload: Uint8Array;
}
export namespace ChatMessage {
let _codec: Codec<ChatMessage>;
export const codec = (): Codec<ChatMessage> => {
if (_codec == null) {
_codec = message<ChatMessage>(
(obj, writer, opts = {}) => {
if (opts.lengthDelimited !== false) {
writer.fork();
}
if (obj.timestamp != null) {
writer.uint32(8);
writer.uint64(obj.timestamp);
} else {
throw new Error(
'Protocol error: required field "timestamp" was not found in object'
);
}
if (obj.nick != null) {
writer.uint32(18);
writer.string(obj.nick);
} else {
throw new Error(
'Protocol error: required field "nick" was not found in object'
);
}
if (obj.payload != null) {
writer.uint32(26);
writer.bytes(obj.payload);
} else {
throw new Error(
'Protocol error: required field "payload" was not found in object'
);
}
if (opts.lengthDelimited !== false) {
writer.ldelim();
}
},
(reader, length) => {
const obj: any = {
timestamp: 0n,
nick: "",
payload: new Uint8Array(0),
};
const end = length == null ? reader.len : reader.pos + length;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
obj.timestamp = reader.uint64();
break;
case 2:
obj.nick = reader.string();
break;
case 3:
obj.payload = reader.bytes();
break;
default:
reader.skipType(tag & 7);
break;
}
}
if (obj.timestamp == null) {
throw new Error(
'Protocol error: value for required field "timestamp" was not found in protobuf'
);
}
if (obj.nick == null) {
throw new Error(
'Protocol error: value for required field "nick" was not found in protobuf'
);
}
if (obj.payload == null) {
throw new Error(
'Protocol error: value for required field "payload" was not found in protobuf'
);
}
return obj;
}
);
}
return _codec;
};
export const encode = (obj: ChatMessage): Uint8Array => {
return encodeMessage(obj, ChatMessage.codec());
};
export const decode = (buf: Uint8Array | Uint8ArrayList): ChatMessage => {
return decodeMessage(buf, ChatMessage.codec());
};
}

View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";

View File

@ -0,0 +1,2 @@
declare module "@livechat/ui-kit";
declare module "@waku/dns-discovery";

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"typeRoots": ["node_modules/@types", "src/types"]
},
"include": ["src"]
}