From 0e5764139a796f4dcc6bb08989d4371551641429 Mon Sep 17 00:00:00 2001 From: Sasha Date: Mon, 30 Oct 2023 13:20:14 +0100 Subject: [PATCH] add messages functionality --- .../rln-js/src/app/home/components/Waku.tsx | 56 ++++++----- examples/rln-js/src/constants.ts | 8 +- examples/rln-js/src/hooks/useWaku.ts | 14 ++- examples/rln-js/src/services/waku.ts | 95 ++++++++++++++++++- 4 files changed, 143 insertions(+), 30 deletions(-) diff --git a/examples/rln-js/src/app/home/components/Waku.tsx b/examples/rln-js/src/app/home/components/Waku.tsx index 42e1673..4012b3a 100644 --- a/examples/rln-js/src/app/home/components/Waku.tsx +++ b/examples/rln-js/src/app/home/components/Waku.tsx @@ -4,12 +4,18 @@ import { Subtitle } from "@/components/Subtitle"; import { Status } from "@/components/Status"; import { Button } from "@/components/Button"; import { useStore, useWaku } from "@/hooks"; +import { MessageContent } from "@/services/waku"; export const Waku: React.FunctionComponent<{}> = () => { const { wakuStatus } = useStore(); const { onSend, messages } = useWaku(); - const { nick, message, onNickChange, onMessageChange } = useMessage(); + const { nick, text, onNickChange, onMessageChange, resetText } = useMessage(); + + const onSendClick = async () => { + await onSend(nick, text); + resetText(); + }; const renderedMessages = React.useMemo( () => messages.map(renderMessage), @@ -18,7 +24,9 @@ export const Waku: React.FunctionComponent<{}> = () => { return ( - Waku + + Waku

(select credentials to initialize)

+
@@ -49,16 +57,16 @@ export const Waku: React.FunctionComponent<{}> = () => { - +
- +

Messages

    {renderedMessages}
@@ -70,36 +78,40 @@ export const Waku: React.FunctionComponent<{}> = () => { function useMessage() { const [nick, setNick] = React.useState(""); - const [message, setMessage] = React.useState(""); + const [text, setText] = React.useState(""); const onNickChange = (e: React.SyntheticEvent) => { setNick(e.currentTarget.value || ""); }; const onMessageChange = (e: React.SyntheticEvent) => { - setMessage(e.currentTarget.value || ""); + setText(e.currentTarget.value || ""); + }; + + const resetText = () => { + setText(""); }; return { nick, - message, + text, + resetText, onNickChange, onMessageChange, }; } -function renderMessage() { - /* - wakuMessage.proofState = !!wakuMessage.rateLimitProof - ? "verifying..." - : "no proof attached"; - - wakuMessage.msg = ` - (${nick}) - ${utils.bytesToUtf8(text)} - [${time.toISOString()}] - `; - messagesList.innerHTML += `
  • ${message.msg} - [epoch: ${message.epoch}, proof: ${message.proofState} ]
  • `; - */ - return <>; +function renderMessage(content: MessageContent) { + return ( +
  • +

    + {content.nick} + + ({content.proofStatus}, {content.time}) + + : +

    +

    {content.text}

    +
  • + ); } diff --git a/examples/rln-js/src/constants.ts b/examples/rln-js/src/constants.ts index eb9a242..8382b98 100644 --- a/examples/rln-js/src/constants.ts +++ b/examples/rln-js/src/constants.ts @@ -1,9 +1,15 @@ import protobuf from "protobufjs"; +export type ProtoChatMessageType = { + timestamp: number; + nick: string; + text: string; +}; + export const ProtoChatMessage = new protobuf.Type("ChatMessage") .add(new protobuf.Field("timestamp", 1, "uint64")) .add(new protobuf.Field("nick", 2, "string")) - .add(new protobuf.Field("text", 3, "bytes")); + .add(new protobuf.Field("text", 3, "string")); export const CONTENT_TOPIC = "/toy-chat/2/luzhou/proto"; diff --git a/examples/rln-js/src/hooks/useWaku.ts b/examples/rln-js/src/hooks/useWaku.ts index 7cddb2f..4823b5e 100644 --- a/examples/rln-js/src/hooks/useWaku.ts +++ b/examples/rln-js/src/hooks/useWaku.ts @@ -1,11 +1,11 @@ import React from "react"; -import { waku, Waku, WakuEventsNames } from "@/services/waku"; +import { waku, Waku, WakuEventsNames, MessageContent } from "@/services/waku"; import { useStore } from "./useStore"; import { useRLN } from "./useRLN"; export const useWaku = () => { - const messages: string[] = []; const wakuRef = React.useRef(); + const [messages, setMessages] = React.useState([]); const { rln } = useRLN(); const { activeMembershipID, credentials, setWakuStatus } = useStore(); @@ -20,6 +20,11 @@ export const useWaku = () => { }; waku.addEventListener(WakuEventsNames.Status, statusListener); + const messagesListener = (event: CustomEvent) => { + setMessages((prev) => [...prev, event.detail as MessageContent]); + }; + waku.addEventListener(WakuEventsNames.Message, messagesListener); + let terminated = false; const run = async () => { if (terminated) { @@ -44,15 +49,16 @@ export const useWaku = () => { return () => { terminated = true; waku.removeEventListener(WakuEventsNames.Status, statusListener); + waku.removeEventListener(WakuEventsNames.Message, messagesListener); }; }, [activeMembershipID, credentials, rln, setWakuStatus]); const onSend = React.useCallback( - async (nick: string, message: string) => { + async (nick: string, text: string) => { if (!wakuRef.current) { return; } - // await wakuRef.current.node?.lightPush.send() + await wakuRef.current.sendMessage(nick, text); }, [wakuRef] ); diff --git a/examples/rln-js/src/services/waku.ts b/examples/rln-js/src/services/waku.ts index b7a640c..4d1a13c 100644 --- a/examples/rln-js/src/services/waku.ts +++ b/examples/rln-js/src/services/waku.ts @@ -6,8 +6,18 @@ import { LightNode, waitForRemotePeer, } from "@waku/sdk"; -import { CONTENT_TOPIC } from "@/constants"; -import { RLNDecoder, RLNEncoder, IdentityCredential } from "@waku/rln"; +import { + CONTENT_TOPIC, + ProtoChatMessage, + ProtoChatMessageType, +} from "@/constants"; +import { + RLNDecoder, + RLNEncoder, + IdentityCredential, + RLNInstance, + RLNContract, +} from "@waku/rln"; import { RLN } from "@/services/rln"; type InitOptions = { @@ -16,8 +26,22 @@ type InitOptions = { rln: RLN; }; +export type MessageContent = { + nick: string; + text: string; + time: string; + proofStatus: string; +}; + +type SubscribeOptions = { + rlnContract: RLNContract; + node: LightNode; + decoder: RLNDecoder; +}; + export enum WakuEventsNames { Status = "status", + Message = "message", } export enum WakuStatusEventPayload { @@ -51,7 +75,6 @@ export class Waku implements IWaku { constructor() {} public async init(options: InitOptions) { - const { rln } = options; if (this.initialized || this.initializing || !options.rln.rlnInstance) { return; } @@ -72,6 +95,14 @@ export class Waku implements IWaku { this.emitStatusEvent(WakuStatusEventPayload.WAITING_FOR_PEERS); await waitForRemotePeer(this.node); this.emitStatusEvent(WakuStatusEventPayload.READY); + + if (options.rln.rlnContract) { + await this.subscribeToMessages({ + node: this.node, + decoder: this.decoder, + rlnContract: options.rln.rlnContract, + }); + } } this.initialized = true; @@ -95,6 +126,58 @@ export class Waku implements IWaku { ); } + public async sendMessage(nick: string, text: string): Promise { + if (!this.node || !this.encoder) { + return; + } + + const timestamp = new Date(); + const msg = ProtoChatMessage.create({ + text, + nick, + timestamp: Math.floor(timestamp.valueOf() / 1000), + }); + const payload = ProtoChatMessage.encode(msg).finish(); + console.log("Sending message with proof..."); + + await this.node.lightPush.send(this.encoder, { payload, timestamp }); + console.log("Message sent!"); + } + + private async subscribeToMessages(options: SubscribeOptions) { + await options.node.filter.subscribe(options.decoder, (message) => { + try { + const { timestamp, nick, text } = ProtoChatMessage.decode( + message.payload + ) as unknown as ProtoChatMessageType; + + let proofStatus = "no proof"; + if (message.rateLimitProof) { + console.log("Proof received: ", message.rateLimitProof); + + try { + console.time("Proof verification took:"); + const res = message.verify(options.rlnContract.roots()); + console.timeEnd("Proof verification took:"); + proofStatus = res ? "verified" : "not verified"; + } catch (error) { + proofStatus = "invalid"; + console.error("Failed to verify proof: ", error); + } + } + + this.emitMessageEvent({ + nick, + text, + proofStatus, + time: new Date(timestamp).toDateString(), + }); + } catch (error) { + console.error("Failed in subscription listener: ", error); + } + }); + } + public addEventListener(name: WakuEventsNames, fn: EventListener) { return this.emitter.addEventListener(name, fn as any); } @@ -108,6 +191,12 @@ export class Waku implements IWaku { new CustomEvent(WakuEventsNames.Status, { detail: payload }) ); } + + private emitMessageEvent(payload: MessageContent) { + this.emitter.dispatchEvent( + new CustomEvent(WakuEventsNames.Message, { detail: payload }) + ); + } } export const waku = new Waku();