Merge pull request #215 from waku-org/chore/eth-denver
chore: clone web-chat for ethdenver
This commit is contained in:
commit
faf8cf83c8
|
@ -18,8 +18,9 @@ jobs:
|
||||||
relay-reactjs-chat,
|
relay-reactjs-chat,
|
||||||
store-reactjs-chat,
|
store-reactjs-chat,
|
||||||
web-chat,
|
web-chat,
|
||||||
|
web-chat-dev,
|
||||||
noise-js,
|
noise-js,
|
||||||
noise-rtc
|
noise-rtc,
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -44,7 +45,7 @@ jobs:
|
||||||
- name: test
|
- name: test
|
||||||
run: npm run test --if-present
|
run: npm run test --if-present
|
||||||
working-directory: "examples/${{ matrix.example }}"
|
working-directory: "examples/${{ matrix.example }}"
|
||||||
|
|
||||||
release_create_waku:
|
release_create_waku:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [examples_build_and_test]
|
needs: [examples_build_and_test]
|
||||||
|
|
|
@ -39,6 +39,7 @@ pipeline {
|
||||||
stage('relay-reactjs-chat') { steps { script { buildExample() } } }
|
stage('relay-reactjs-chat') { steps { script { buildExample() } } }
|
||||||
stage('store-reactjs-chat') { steps { script { buildExample() } } }
|
stage('store-reactjs-chat') { steps { script { buildExample() } } }
|
||||||
stage('web-chat') { steps { script { buildExample() } } }
|
stage('web-chat') { steps { script { buildExample() } } }
|
||||||
|
stage('web-chat-dev') { steps { script { buildExample() } } }
|
||||||
stage('noise-js') { steps { script { buildExample() } } }
|
stage('noise-js') { steps { script { buildExample() } } }
|
||||||
stage('noise-rtc') { steps { script { buildExample() } } }
|
stage('noise-rtc') { steps { script { buildExample() } } }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
{
|
||||||
|
"version": "0.2",
|
||||||
|
"$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",
|
||||||
|
"ethdenver",
|
||||||
|
"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"]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Remove ReactJS warning about webpack
|
||||||
|
# because this is not a monorepo, ReactJS projects are examples
|
||||||
|
SKIP_PREFLIGHT_CHECK=true
|
|
@ -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*
|
|
@ -0,0 +1,3 @@
|
||||||
|
# package.json is formatted by package managers, so we ignore it here
|
||||||
|
package.json
|
||||||
|
gen
|
|
@ -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.
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: v1beta1
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- name: ts_proto
|
||||||
|
out: ./src/proto
|
||||||
|
opt: grpc_js,esModuleInterop=true
|
|
@ -0,0 +1,5 @@
|
||||||
|
version: v1beta1
|
||||||
|
|
||||||
|
build:
|
||||||
|
roots:
|
||||||
|
- ./src/proto
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"name": "web-chat-dev",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "/web-chat-dev",
|
||||||
|
"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 |
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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} />;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 sendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(" ");
|
||||||
|
}
|
|
@ -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,
|
||||||
|
];
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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")
|
||||||
|
);
|
|
@ -0,0 +1,7 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
message ChatMessage {
|
||||||
|
uint64 timestamp = 1;
|
||||||
|
string nick = 2;
|
||||||
|
bytes payload = 3;
|
||||||
|
}
|
|
@ -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());
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
|
@ -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";
|
|
@ -0,0 +1,2 @@
|
||||||
|
declare module "@livechat/ui-kit";
|
||||||
|
declare module "@waku/dns-discovery";
|
|
@ -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"]
|
||||||
|
}
|
Loading…
Reference in New Issue