mirror of
https://github.com/logos-messaging/logos-messaging-frontend.git
synced 2026-01-22 07:33:05 +00:00
Merge pull request #16 from waku-org/weboko/follow-up-2
feat: add ability to subscribe to custom contentTopic + other stuff
This commit is contained in:
commit
226e3327ec
59
package-lock.json
generated
59
package-lock.json
generated
@ -8,7 +8,7 @@
|
||||
"name": "rln-js",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@waku/rln": "0.1.1-7e8cb89",
|
||||
"@waku/rln": "0.1.1-60a5070",
|
||||
"@waku/utils": "^0.0.12",
|
||||
"ethers": "^5.7.2",
|
||||
"next": "13.5.6",
|
||||
@ -1424,13 +1424,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@waku/rln": {
|
||||
"version": "0.1.1-7e8cb89",
|
||||
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-7e8cb89.tgz",
|
||||
"integrity": "sha512-ToHwjodXoc1BrllRkIqC8/D73LqMlL4AHRdaSk4ttXjzdlqELdZYaJEwMtEBlupydNPf813RZ6V6hVuE6ttbTg==",
|
||||
"version": "0.1.1-60a5070",
|
||||
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-60a5070.tgz",
|
||||
"integrity": "sha512-c3rXqzuTTQVIn2l4gZKDG+/2UY2yb0XbumyrsuvX9fQhQxopCAsSi16CYePdG5HCpImXPNJEC3C3JakuiA3v9A==",
|
||||
"dependencies": {
|
||||
"@chainsafe/bls-keystore": "^3.0.0",
|
||||
"@waku/utils": "^0.0.12",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.10",
|
||||
"@waku/utils": "^0.0.13",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
||||
"ethereum-cryptography": "^2.1.2",
|
||||
"ethers": "^5.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
@ -1441,6 +1441,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/rln/node_modules/@waku/utils": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.13.tgz",
|
||||
"integrity": "sha512-sGZRJyYr7+QZpV2tlGJF48gKmwNdFha6rPKPgXiKDsz2YMhPlg70ffbGcND3bEfOwWmX4g/x5i3Oqwwl+HzwJw==",
|
||||
"dependencies": {
|
||||
"chai": "^4.3.8",
|
||||
"debug": "^4.3.4",
|
||||
"uint8arrays": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/utils": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.12.tgz",
|
||||
@ -1455,9 +1468,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@waku/zerokit-rln-wasm": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.10.tgz",
|
||||
"integrity": "sha512-qegIK1P54mxEp59uTa8C0/zidUffLc2Iee61yiKRIuGJDui2mQ+0V+KzPSPImKpIoqfVLT192EqgZkqPmj8VEw=="
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
|
||||
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA=="
|
||||
},
|
||||
"node_modules/@zeit/schemas": {
|
||||
"version": "2.29.0",
|
||||
@ -7796,18 +7809,30 @@
|
||||
"dev": true
|
||||
},
|
||||
"@waku/rln": {
|
||||
"version": "0.1.1-7e8cb89",
|
||||
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-7e8cb89.tgz",
|
||||
"integrity": "sha512-ToHwjodXoc1BrllRkIqC8/D73LqMlL4AHRdaSk4ttXjzdlqELdZYaJEwMtEBlupydNPf813RZ6V6hVuE6ttbTg==",
|
||||
"version": "0.1.1-60a5070",
|
||||
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.1-60a5070.tgz",
|
||||
"integrity": "sha512-c3rXqzuTTQVIn2l4gZKDG+/2UY2yb0XbumyrsuvX9fQhQxopCAsSi16CYePdG5HCpImXPNJEC3C3JakuiA3v9A==",
|
||||
"requires": {
|
||||
"@chainsafe/bls-keystore": "^3.0.0",
|
||||
"@waku/utils": "^0.0.12",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.10",
|
||||
"@waku/utils": "^0.0.13",
|
||||
"@waku/zerokit-rln-wasm": "^0.0.13",
|
||||
"ethereum-cryptography": "^2.1.2",
|
||||
"ethers": "^5.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"rlnjs": "^3.2.3",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/utils": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.13.tgz",
|
||||
"integrity": "sha512-sGZRJyYr7+QZpV2tlGJF48gKmwNdFha6rPKPgXiKDsz2YMhPlg70ffbGcND3bEfOwWmX4g/x5i3Oqwwl+HzwJw==",
|
||||
"requires": {
|
||||
"chai": "^4.3.8",
|
||||
"debug": "^4.3.4",
|
||||
"uint8arrays": "^4.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@waku/utils": {
|
||||
@ -7821,9 +7846,9 @@
|
||||
}
|
||||
},
|
||||
"@waku/zerokit-rln-wasm": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.10.tgz",
|
||||
"integrity": "sha512-qegIK1P54mxEp59uTa8C0/zidUffLc2Iee61yiKRIuGJDui2mQ+0V+KzPSPImKpIoqfVLT192EqgZkqPmj8VEw=="
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
|
||||
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA=="
|
||||
},
|
||||
"@zeit/schemas": {
|
||||
"version": "2.29.0",
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/rln": "0.1.1-7e8cb89",
|
||||
"@waku/rln": "0.1.1-60a5070",
|
||||
"@waku/utils": "^0.0.12",
|
||||
"ethers": "^5.7.2",
|
||||
"next": "13.5.6",
|
||||
|
||||
24
src/app/components/Header.tsx
Normal file
24
src/app/components/Header.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Block, BlockTypes } from "@/components/Block";
|
||||
import { Title } from "@/components/Title";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
type HeaderProps = {
|
||||
children?: React.ReactNode;
|
||||
onWalletConnect?: () => void;
|
||||
}
|
||||
|
||||
export const Header: React.FunctionComponent<HeaderProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
|
||||
<Title>Waku</Title>
|
||||
{props.onWalletConnect && (
|
||||
<Button onClick={props.onWalletConnect}>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
)}
|
||||
</Block>
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
12
src/app/components/Menu.tsx
Normal file
12
src/app/components/Menu.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import { Block } from "@/components/Block";
|
||||
|
||||
export const Menu: React.FunctionComponent<{}> = () => {
|
||||
return (
|
||||
<Block className="m-5 flex text-lg">
|
||||
<p className="mr-5">{">"}</p>
|
||||
<p className="mr-5"><Link href="/">Chat</Link></p>
|
||||
<p><Link href="/keystore">Keystore</Link></p>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
162
src/app/components/Waku.tsx
Normal file
162
src/app/components/Waku.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React from "react";
|
||||
import { Block } from "@/components/Block";
|
||||
import { Subtitle } from "@/components/Subtitle";
|
||||
import { Button } from "@/components/Button";
|
||||
import { MessageContent } from "@/hooks";
|
||||
|
||||
type WakuProps = {
|
||||
onSend: (nick: string, text: string) => Promise<void>;
|
||||
activeContentTopic: string;
|
||||
messages: MessageContent[];
|
||||
onActiveContentTopicChange: (contentTopic: string) => void;
|
||||
}
|
||||
|
||||
export const Waku: React.FunctionComponent<WakuProps> = (props) => {
|
||||
const {
|
||||
nick,
|
||||
text,
|
||||
onNickChange,
|
||||
onMessageChange,
|
||||
resetText,
|
||||
} = useMessage();
|
||||
const {
|
||||
contentTopic,
|
||||
onContentTopicChange,
|
||||
} = useContentTopic(props.activeContentTopic);
|
||||
|
||||
const onSendClick = async () => {
|
||||
await props.onSend(nick, text);
|
||||
resetText();
|
||||
};
|
||||
|
||||
const renderedMessages = React.useMemo(
|
||||
() => props.messages.map(renderMessage),
|
||||
[props.messages]
|
||||
);
|
||||
|
||||
return (
|
||||
<Block className="mt-10 flex flex-col md:flex-row lg:flex-row">
|
||||
<Block>
|
||||
<Block>
|
||||
<Subtitle>
|
||||
Waku
|
||||
</Subtitle>
|
||||
<label
|
||||
htmlFor="contentTopic-input"
|
||||
className="block mb-2 mt-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Content topic
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="contentTopic-input"
|
||||
value={contentTopic}
|
||||
onChange={onContentTopicChange}
|
||||
className="w-96 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
<Button className="mt-1" onClick={() => { props.onActiveContentTopicChange(contentTopic); }}>Change</Button>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-4 mr-10 min-w-fit">
|
||||
<label
|
||||
htmlFor="nick-input"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Your nickname
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nick-input"
|
||||
placeholder="Choose a nickname"
|
||||
value={nick}
|
||||
onChange={onNickChange}
|
||||
className="w-96 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-4">
|
||||
<Block className="mb-2">
|
||||
<label
|
||||
htmlFor="message-input"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message-input"
|
||||
value={text}
|
||||
onChange={onMessageChange}
|
||||
placeholder="Text your message here"
|
||||
className="w-96 h-60 mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
</Block>
|
||||
<Button onClick={onSendClick}>Send</Button>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
<Block className="max-w-screen-md mt-10 md:mt-0">
|
||||
<p className="text-l mb-4">Messages</p>
|
||||
<div>
|
||||
<ul>{renderedMessages}</ul>
|
||||
</div>
|
||||
</Block>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
function useContentTopic(globalContentTopic: string) {
|
||||
const [contentTopic, setContentTopic] = React.useState<string>(globalContentTopic);
|
||||
|
||||
React.useEffect(() => {
|
||||
setContentTopic(globalContentTopic);
|
||||
}, [globalContentTopic]);
|
||||
|
||||
const onContentTopicChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
setContentTopic(e.currentTarget.value || "");
|
||||
};
|
||||
|
||||
return {
|
||||
contentTopic,
|
||||
onContentTopicChange,
|
||||
};
|
||||
}
|
||||
|
||||
function useMessage() {
|
||||
const [nick, setNick] = React.useState<string>("");
|
||||
const [text, setText] = React.useState<string>("");
|
||||
|
||||
const onNickChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
setNick(e.currentTarget.value || "");
|
||||
};
|
||||
|
||||
const onMessageChange = (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
|
||||
setText(e.currentTarget.value || "");
|
||||
};
|
||||
|
||||
const resetText = () => {
|
||||
setText("");
|
||||
};
|
||||
|
||||
return {
|
||||
nick,
|
||||
text,
|
||||
resetText,
|
||||
onNickChange,
|
||||
onMessageChange,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMessage(content: MessageContent) {
|
||||
return (
|
||||
<li key={`${content.nick}-${content.timestamp}-${content.text}`} className="mb-4">
|
||||
<p>
|
||||
<span className="text-lg">{content.nick}</span>
|
||||
<span className="text-sm font-bold">
|
||||
({(new Date(content.timestamp)).toDateString()})
|
||||
</span>
|
||||
:
|
||||
</p>
|
||||
<p className="break-words">{content.text}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 4.2 KiB |
@ -1,23 +0,0 @@
|
||||
import { Block, BlockTypes } from "@/components/Block";
|
||||
import { Title } from "@/components/Title";
|
||||
import { Status } from "@/components/Status";
|
||||
import { useStore, useWallet } from "@/hooks";
|
||||
import { Button } from "@/components/Button";
|
||||
|
||||
export const Header: React.FunctionComponent<{}> = () => {
|
||||
const { appStatus, wallet } = useStore();
|
||||
const { onWalletConnect } = useWallet();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className="mb-5" type={BlockTypes.FlexHorizontal}>
|
||||
<Title>Waku RLN</Title>
|
||||
<Button onClick={onWalletConnect}>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</Block>
|
||||
<Status text="Application status" mark={appStatus} />
|
||||
{wallet && <p className="mt-3 text-sm">Wallet connected: {wallet}</p> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,117 +0,0 @@
|
||||
import React from "react";
|
||||
import { Block } from "@/components/Block";
|
||||
import { Subtitle } from "@/components/Subtitle";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Status } from "@/components/Status";
|
||||
import { MessageContent, useWaku } from "@/hooks";
|
||||
import { CONTENT_TOPIC } from "@/constants";
|
||||
|
||||
export const Waku: React.FunctionComponent<{}> = () => {
|
||||
const { onSend, messages } = useWaku();
|
||||
const { nick, text, onNickChange, onMessageChange, resetText } = useMessage();
|
||||
|
||||
const onSendClick = async () => {
|
||||
await onSend(nick, text);
|
||||
resetText();
|
||||
};
|
||||
|
||||
const renderedMessages = React.useMemo(
|
||||
() => messages.map(renderMessage),
|
||||
[messages]
|
||||
);
|
||||
|
||||
return (
|
||||
<Block className="mt-10">
|
||||
<Block>
|
||||
<Subtitle>
|
||||
Waku
|
||||
</Subtitle>
|
||||
<p className="text-sm">Content topic: {CONTENT_TOPIC}</p>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-4">
|
||||
<label
|
||||
htmlFor="nick-input"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Your nickname
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nick-input"
|
||||
placeholder="Choose a nickname"
|
||||
value={nick}
|
||||
onChange={onNickChange}
|
||||
className="w-full mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-4">
|
||||
<Block className="mb-2">
|
||||
<label
|
||||
htmlFor="message-input"
|
||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Message
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="message-input"
|
||||
value={text}
|
||||
onChange={onMessageChange}
|
||||
placeholder="Text your message here"
|
||||
className="w-full mr-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
</Block>
|
||||
<Button onClick={onSendClick}>Send</Button>
|
||||
</Block>
|
||||
|
||||
<Block className="mt-8">
|
||||
<p className="text-l mb-4">Messages</p>
|
||||
<div>
|
||||
<ul>{renderedMessages}</ul>
|
||||
</div>
|
||||
</Block>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
function useMessage() {
|
||||
const [nick, setNick] = React.useState<string>("");
|
||||
const [text, setText] = React.useState<string>("");
|
||||
|
||||
const onNickChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
setNick(e.currentTarget.value || "");
|
||||
};
|
||||
|
||||
const onMessageChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
setText(e.currentTarget.value || "");
|
||||
};
|
||||
|
||||
const resetText = () => {
|
||||
setText("");
|
||||
};
|
||||
|
||||
return {
|
||||
nick,
|
||||
text,
|
||||
resetText,
|
||||
onNickChange,
|
||||
onMessageChange,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMessage(content: MessageContent) {
|
||||
return (
|
||||
<li key={`${content.nick}-${content.time}`} className="mb-4">
|
||||
<p>
|
||||
<span className="text-lg">{content.nick}</span>
|
||||
<span className="text-sm font-bold">
|
||||
({content.time})
|
||||
</span>
|
||||
:
|
||||
</p>
|
||||
<p>{content.text}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
import { Header } from "./components/Header";
|
||||
import { Waku } from "./components/Waku";
|
||||
import { Keystore } from "./components/Keystore";
|
||||
import { KeystoreDetails } from "./components/KeystoreDetails";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col p-24 font-mono max-w-screen-lg m-auto">
|
||||
<Header />
|
||||
<Keystore />
|
||||
<KeystoreDetails />
|
||||
<Waku />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
32
src/app/keystore/page.tsx
Normal file
32
src/app/keystore/page.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
import { Header } from "@/app/components/Header";
|
||||
import { Keystore } from "@/app/components/Keystore";
|
||||
import { KeystoreDetails } from "@/app/components/KeystoreDetails";
|
||||
import { useWallet } from "@/hooks";
|
||||
import { Status } from "@/components/Status";
|
||||
import { useStore } from "@/hooks";
|
||||
|
||||
export default function KeystorePage() {
|
||||
const { onWalletConnect } = useWallet();
|
||||
const { appStatus, wallet } = useStore();
|
||||
|
||||
if (typeof window !== "undefined" && !window?.ethereum) {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col p-6 font-mono max-w-screen-lg">
|
||||
<Header />
|
||||
<p className="text-xl">Seems you don't have MetaMask installed. Please, install and reload the page.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col p-6 font-mono max-w-screen-lg">
|
||||
<Header onWalletConnect={onWalletConnect}>
|
||||
<Status text="Application status" mark={appStatus} />
|
||||
{wallet && <p className="mt-3 text-sm">Wallet connected: {wallet}</p> }
|
||||
</Header>
|
||||
<Keystore />
|
||||
<KeystoreDetails />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Menu } from "@/app/components/Menu";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "RLN Example",
|
||||
description: "Showcases RLN, Keystore and generation of proofs",
|
||||
title: "nwaku front-end",
|
||||
description: "Send messages through you local node, register to RLN, read and export Keystore",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -16,7 +17,10 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<body className={inter.className}>
|
||||
<Menu />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,58 @@
|
||||
import Home from "@/app/home/page";
|
||||
"use client";
|
||||
import { Header } from "@/app/components/Header";
|
||||
import { Waku } from "@/app/components/Waku";
|
||||
import { useWaku } from "@/hooks";
|
||||
import { DebugInfo } from "@/services/waku";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
export default Home;
|
||||
export default function Home() {
|
||||
const {
|
||||
onSend,
|
||||
messages,
|
||||
debugInfo,
|
||||
contentTopic,
|
||||
onContentTopicChange
|
||||
} = useWaku();
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col p-6 font-mono max-w-screen-lg">
|
||||
<Header>
|
||||
<DebugInfo value={debugInfo} />
|
||||
</Header>
|
||||
<Waku
|
||||
onSend={onSend}
|
||||
messages={messages}
|
||||
activeContentTopic={contentTopic}
|
||||
onActiveContentTopicChange={onContentTopicChange}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
type DebugInfoProps = {
|
||||
value?: DebugInfo;
|
||||
}
|
||||
|
||||
const DebugInfo: React.FunctionComponent<DebugInfoProps> = (props) => {
|
||||
if (!props.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<details className="border rounded p-2">
|
||||
<summary className="cursor-pointer bg-gray-300 p-2 rounded-md">
|
||||
<span className="font-bold">Show node info</span>
|
||||
</summary>
|
||||
<div className="mt-2 text-sm break-words">
|
||||
<p className="mb-2">Health: {props.value.health}</p>
|
||||
<p className="mb-2">Version: {props.value.version}</p>
|
||||
<p className="mb-2">ENR URI: {props.value.enrUri}</p>
|
||||
<p className="mb-2">Listen Addresses:</p>
|
||||
<ul>
|
||||
{props.value.listenAddresses.map((address, index) => (
|
||||
<li key={index}>{address}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,16 +12,16 @@ export const useContract = (): UseContractResult => {
|
||||
|
||||
const onFetchContract = React.useCallback(async () => {
|
||||
const fetchAccounts = new Promise<void>(async (resolve) => {
|
||||
if (!rln) {
|
||||
if (!rln || !rln?.ethProvider) {
|
||||
console.log("Cannot fetch wallet, not provider found.");
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const accounts = await rln.ethProvider.send("eth_requestAccounts", []);
|
||||
const accounts = await rln?.ethProvider.send("eth_requestAccounts", []);
|
||||
setEthAccount(accounts[0] || "");
|
||||
const network = await rln.ethProvider.getNetwork();
|
||||
const network = await rln?.ethProvider.getNetwork();
|
||||
setChainID(network.chainId);
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to account: ", error);
|
||||
|
||||
@ -25,7 +25,7 @@ export const useKeystore = (): UseKeystoreResult => {
|
||||
return;
|
||||
}
|
||||
|
||||
const signer = rln.ethProvider.getSigner();
|
||||
const signer = rln?.ethProvider.getSigner();
|
||||
const signature = await signer.signMessage(
|
||||
`${SIGNATURE_MESSAGE}. Nonce: ${randomNumber()}`
|
||||
);
|
||||
|
||||
@ -1,55 +1,101 @@
|
||||
import React from "react";
|
||||
import { CONTENT_TOPIC } from "@/constants";
|
||||
import { Message, waku } from "@/services/waku";
|
||||
import { DebugInfo, Message, waku } from "@/services/waku";
|
||||
|
||||
export type MessageContent = {
|
||||
nick: string;
|
||||
text: string;
|
||||
time: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export const useWaku = () => {
|
||||
const [messages, setMessages] = React.useState<MessageContent[]>([]);
|
||||
const [contentTopic, setContentTopic] = React.useState<string>(CONTENT_TOPIC);
|
||||
const [messages, setMessages] = React.useState<Map<string, MessageContent>>(new Map());
|
||||
const [debugInfo, setDebugInfo] = React.useState<undefined | DebugInfo>();
|
||||
|
||||
React.useEffect(() => {
|
||||
const messageListener = (event: CustomEvent) => {
|
||||
const messages: Message[] = event.detail;
|
||||
const parsedMessaged = messages.map((message) => {
|
||||
const time = new Date(message.timestamp);
|
||||
const payload = JSON.parse(atob(message.payload));
|
||||
const nextMessages = new Map(messages);
|
||||
const newMessages: Message[] = event.detail;
|
||||
|
||||
return {
|
||||
newMessages.forEach((m) => {
|
||||
const payload = JSON.parse(atob(m.payload));
|
||||
|
||||
const message: MessageContent = {
|
||||
nick: payload?.nick || "unknown",
|
||||
text: payload?.text || "empty",
|
||||
time: time.toDateString(),
|
||||
timestamp: m.timestamp || Date.now(),
|
||||
};
|
||||
nextMessages.set(`${message.nick}-${message.timestamp}-${message.text}`, message);
|
||||
});
|
||||
|
||||
setMessages((prev) => [...prev, ...parsedMessaged]);
|
||||
setMessages(nextMessages);
|
||||
};
|
||||
|
||||
waku.relay.addEventListener(CONTENT_TOPIC, messageListener);
|
||||
waku.relay.addEventListener(contentTopic, messageListener);
|
||||
|
||||
return () => {
|
||||
waku.relay.removeEventListener(CONTENT_TOPIC, messageListener);
|
||||
waku.relay.removeEventListener(contentTopic, messageListener);
|
||||
};
|
||||
}, [setMessages]);
|
||||
}, [messages, setMessages, contentTopic]);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
const debugInfoListener = (event: CustomEvent) => {
|
||||
const debugInfo = event.detail;
|
||||
|
||||
if (!debugInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDebugInfo(debugInfo);
|
||||
};
|
||||
|
||||
waku.debug.addEventListener("debug", debugInfoListener);
|
||||
|
||||
return () => {
|
||||
waku.debug.removeEventListener("debug", debugInfoListener);
|
||||
};
|
||||
}, [debugInfo, setDebugInfo]);
|
||||
|
||||
const onSend = React.useCallback(
|
||||
async (nick: string, text: string) => {
|
||||
const timestamp = Date.now();
|
||||
await waku.relay.send({
|
||||
version: 0,
|
||||
timestamp: Date.now(),
|
||||
timestamp,
|
||||
contentTopic: CONTENT_TOPIC,
|
||||
payload: btoa(JSON.stringify({
|
||||
nick,
|
||||
text
|
||||
})),
|
||||
});
|
||||
const id = `${nick}-${timestamp}-${text}`;
|
||||
setMessages((prev) => {
|
||||
if (prev.has(id)) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.set(id, { nick, timestamp, text });
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[]
|
||||
[setMessages]
|
||||
);
|
||||
|
||||
return { onSend, messages };
|
||||
};
|
||||
const onContentTopicChange = async (nextContentTopic: string) => {
|
||||
if (nextContentTopic === contentTopic) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContentTopic(nextContentTopic);
|
||||
};
|
||||
|
||||
return {
|
||||
onSend,
|
||||
debugInfo,
|
||||
contentTopic,
|
||||
onContentTopicChange,
|
||||
messages: Array.from(messages.values())
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,7 +37,7 @@ type IRLN = {
|
||||
|
||||
export class RLN implements IRLN {
|
||||
private readonly emitter = new EventTarget();
|
||||
public readonly ethProvider: ethers.providers.Web3Provider;
|
||||
public ethProvider: ethers.providers.Web3Provider | undefined;
|
||||
|
||||
public rlnInstance: undefined | RLNInstance;
|
||||
public rlnContract: undefined | RLNContract;
|
||||
@ -47,14 +47,6 @@ export class RLN implements IRLN {
|
||||
private initializing = false;
|
||||
|
||||
public constructor() {
|
||||
const ethereum =
|
||||
window.ethereum as unknown as ethers.providers.ExternalProvider;
|
||||
if (!isBrowserProviderValid(ethereum)) {
|
||||
throw Error(
|
||||
"Invalid Ethereum provider present on the page. Check if MetaMask is connected."
|
||||
);
|
||||
}
|
||||
this.ethProvider = new ethers.providers.Web3Provider(ethereum, "any");
|
||||
this.keystore = this.initKeystore();
|
||||
}
|
||||
|
||||
@ -64,6 +56,8 @@ export class RLN implements IRLN {
|
||||
}
|
||||
|
||||
this.initializing = true;
|
||||
|
||||
this.initProvider();
|
||||
await this.initRLNWasm();
|
||||
|
||||
// emit keystore keys once app is ready
|
||||
@ -73,6 +67,21 @@ export class RLN implements IRLN {
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
private initProvider() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const ethereum =
|
||||
window.ethereum as unknown as ethers.providers.ExternalProvider;
|
||||
if (!isBrowserProviderValid(ethereum)) {
|
||||
throw Error(
|
||||
"Invalid Ethereum provider present on the page. Check if MetaMask is connected."
|
||||
);
|
||||
}
|
||||
this.ethProvider = new ethers.providers.Web3Provider(ethereum, "any");
|
||||
}
|
||||
|
||||
private async initRLNWasm(): Promise<void> {
|
||||
this.emitStatusEvent(StatusEventPayload.WASM_LOADING);
|
||||
try {
|
||||
@ -89,7 +98,7 @@ export class RLN implements IRLN {
|
||||
}
|
||||
|
||||
public async initRLNContract(rlnInstance: RLNInstance): Promise<void> {
|
||||
if (this.rlnContract) {
|
||||
if (this.rlnContract || !this.ethProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,9 +118,13 @@ export class RLN implements IRLN {
|
||||
|
||||
private initKeystore(): Keystore {
|
||||
const localKeystoreString = localStorage.getItem("keystore");
|
||||
|
||||
|
||||
if (!localKeystoreString) {
|
||||
return Keystore.create();
|
||||
}
|
||||
|
||||
try {
|
||||
return Keystore.fromString(localKeystoreString || "");
|
||||
return Keystore.fromString(localKeystoreString || "") || Keystore.create();
|
||||
} catch(error) {
|
||||
return Keystore.create();
|
||||
}
|
||||
|
||||
@ -17,79 +17,59 @@ const RELAY = "/relay/v1";
|
||||
const buildURL = (endpoint: string) => `${LOCAL_NODE}${endpoint}`;
|
||||
|
||||
class Relay {
|
||||
private subscribing = false;
|
||||
private readonly subscriptionsEmitter = new EventTarget();
|
||||
|
||||
private contentTopicListeners: Map<string, number> = new Map();
|
||||
|
||||
// only one content topic subscriptions is possible now
|
||||
private subscriptionRoutine: undefined | number;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public addEventListener(contentTopic: string, fn: EventListener) {
|
||||
this.handleSubscribed(contentTopic);
|
||||
this.subscribe();
|
||||
return this.subscriptionsEmitter.addEventListener(contentTopic, fn as any);
|
||||
}
|
||||
|
||||
public removeEventListener(contentTopic: string, fn: EventListener) {
|
||||
this.handleUnsubscribed(contentTopic);
|
||||
return this.subscriptionsEmitter.removeEventListener(
|
||||
contentTopic,
|
||||
fn as any
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSubscribed(contentTopic: string) {
|
||||
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
|
||||
|
||||
// if nwaku node already subscribed to this content topic
|
||||
if (numberOfListeners) {
|
||||
this.contentTopicListeners.set(contentTopic, numberOfListeners + 1);
|
||||
private async subscribe() {
|
||||
if (this.subscriptionRoutine || this.subscribing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribing = true;
|
||||
try {
|
||||
await http.post(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
|
||||
|
||||
this.subscriptionRoutine = window.setInterval(async () => {
|
||||
await this.fetchMessages();
|
||||
}, 10 * SECOND);
|
||||
|
||||
this.contentTopicListeners.set(contentTopic, 1);
|
||||
}, 5 * SECOND);
|
||||
} catch (error) {
|
||||
console.error(`Failed to subscribe node ${contentTopic}:`, error);
|
||||
console.error(`Failed to subscribe node ${PUBSUB_TOPIC}:`, error);
|
||||
}
|
||||
this.subscribing = false;
|
||||
}
|
||||
|
||||
private async handleUnsubscribed(contentTopic: string) {
|
||||
const numberOfListeners = this.contentTopicListeners.get(contentTopic);
|
||||
|
||||
if (!numberOfListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (numberOfListeners - 1 > 0) {
|
||||
this.contentTopicListeners.set(contentTopic, numberOfListeners - 1);
|
||||
public async unsubscribe() {
|
||||
if (!this.subscriptionRoutine) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await http.delete(buildURL(`${RELAY}/subscriptions`), [PUBSUB_TOPIC]);
|
||||
} catch (error) {
|
||||
console.error(`Failed to unsubscribe node from ${contentTopic}:`, error);
|
||||
console.error(`Failed to unsubscribe node from ${PUBSUB_TOPIC}:`, error);
|
||||
}
|
||||
|
||||
clearInterval(this.subscriptionRoutine);
|
||||
this.contentTopicListeners.delete(contentTopic);
|
||||
this.subscriptionRoutine = undefined;
|
||||
}
|
||||
|
||||
private async fetchMessages(): Promise<void> {
|
||||
const contentTopic = Array.from(this.contentTopicListeners.keys())[0];
|
||||
|
||||
if (!contentTopic) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await http.get(
|
||||
buildURL(`${RELAY}/messages/${encodeURIComponent(PUBSUB_TOPIC)}`)
|
||||
);
|
||||
@ -99,9 +79,27 @@ class Relay {
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscriptionsEmitter.dispatchEvent(
|
||||
new CustomEvent(contentTopic, { detail: body })
|
||||
);
|
||||
const messagesPerContentTopic = new Map<string, Message[]>();
|
||||
body.forEach((m) => {
|
||||
const contentTopic = m.contentTopic;
|
||||
if (!contentTopic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let messages = messagesPerContentTopic.get(contentTopic);
|
||||
if (!messages) {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
messages.push(m);
|
||||
messagesPerContentTopic.set(contentTopic, messages);
|
||||
});
|
||||
|
||||
Array.from(messagesPerContentTopic.entries()).forEach(([contentTopic, messages]) => {
|
||||
this.subscriptionsEmitter.dispatchEvent(
|
||||
new CustomEvent(contentTopic, { detail: messages })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async send(message: Message): Promise<void> {
|
||||
@ -109,6 +107,92 @@ class Relay {
|
||||
}
|
||||
}
|
||||
|
||||
type DebugInfoResponse = {
|
||||
enrUri: string;
|
||||
listenAddresses: string[];
|
||||
}
|
||||
|
||||
export type DebugInfo = {
|
||||
health: string;
|
||||
version: string;
|
||||
} & DebugInfoResponse;
|
||||
|
||||
class Debug {
|
||||
private subscribing = false;
|
||||
private readonly subscriptionsEmitter = new EventTarget();
|
||||
private subscriptionRoutine: undefined | number;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public addEventListener(event: string, fn: EventListener) {
|
||||
this.subscribe();
|
||||
return this.subscriptionsEmitter.addEventListener(event, fn as any);
|
||||
}
|
||||
|
||||
public removeEventListener(event: string, fn: EventListener) {
|
||||
return this.subscriptionsEmitter.removeEventListener(
|
||||
event,
|
||||
fn as any
|
||||
);
|
||||
}
|
||||
|
||||
private async subscribe() {
|
||||
if (this.subscriptionRoutine || this.subscribing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribing = true;
|
||||
try {
|
||||
await this.fetchParameters();
|
||||
this.subscriptionRoutine = window.setInterval(async () => {
|
||||
await this.fetchParameters();
|
||||
}, 30 * SECOND);
|
||||
} catch(error) {
|
||||
console.error("Failed to fetch debug info:", error);
|
||||
}
|
||||
this.subscribing = false;
|
||||
}
|
||||
|
||||
private async unsubscribe() {
|
||||
if (!this.subscriptionRoutine) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this.subscriptionRoutine);
|
||||
this.subscriptionRoutine = undefined;
|
||||
}
|
||||
|
||||
private async fetchParameters(): Promise<void> {
|
||||
const health = await this.fetchHealth();
|
||||
const debug = await this.fetchDebugInfo();
|
||||
const version = await this.fetchDebugVersion();
|
||||
|
||||
this.subscriptionsEmitter.dispatchEvent(
|
||||
new CustomEvent("debug", { detail: {
|
||||
health,
|
||||
version,
|
||||
...debug,
|
||||
} })
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchHealth(): Promise<string> {
|
||||
const response = await http.get(buildURL(`/health`));
|
||||
return response.text();
|
||||
}
|
||||
|
||||
private async fetchDebugInfo(): Promise<DebugInfoResponse> {
|
||||
const response = await http.get(buildURL(`/debug/v1/info`));
|
||||
const body: DebugInfoResponse = await response.json();
|
||||
return body;
|
||||
}
|
||||
|
||||
private async fetchDebugVersion(): Promise<string> {
|
||||
const response = await http.get(buildURL(`/debug/v1/version`));
|
||||
return response.text();
|
||||
}
|
||||
}
|
||||
|
||||
export const waku = {
|
||||
relay: new Relay(),
|
||||
debug: new Debug(),
|
||||
};
|
||||
|
||||
@ -13,8 +13,6 @@ export const http = {
|
||||
delete(url: string, body: any) {
|
||||
return fetch(new URL(url), {
|
||||
method: "DELETE",
|
||||
mode: "no-cors",
|
||||
referrerPolicy: "no-referrer",
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user