diff --git a/src/app/components/Waku.tsx b/src/app/components/Waku.tsx index 30f18b8..65c7138 100644 --- a/src/app/components/Waku.tsx +++ b/src/app/components/Waku.tsx @@ -1,13 +1,27 @@ import React from "react"; -import { Block, BlockTypes } from "@/components/Block"; +import { Block } from "@/components/Block"; import { Subtitle } from "@/components/Subtitle"; import { Button } from "@/components/Button"; 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 { + onSend, + messages, + contentTopic: activeContentTopic, + onContentTopicChange: onActiveContentTopicChange + } = useWaku(); + const { + nick, + text, + onNickChange, + onMessageChange, + resetText, + } = useMessage(); + const { + contentTopic, + onContentTopicChange, + } = useContentTopic(activeContentTopic); const onSendClick = async () => { await onSend(nick, text); @@ -26,7 +40,20 @@ export const Waku: React.FunctionComponent<{}> = () => { Waku -

Content topic: {CONTENT_TOPIC}

+ + + @@ -76,6 +103,23 @@ export const Waku: React.FunctionComponent<{}> = () => { ); }; +function useContentTopic(globalContentTopic: string) { + const [contentTopic, setContentTopic] = React.useState(globalContentTopic); + + React.useEffect(() => { + setContentTopic(globalContentTopic); + }, [globalContentTopic]); + + const onContentTopicChange = (e: React.SyntheticEvent) => { + setContentTopic(e.currentTarget.value || ""); + }; + + return { + contentTopic, + onContentTopicChange, + }; +} + function useMessage() { const [nick, setNick] = React.useState(""); const [text, setText] = React.useState(""); diff --git a/src/hooks/useWaku.ts b/src/hooks/useWaku.ts index 85e9fb8..361e83a 100644 --- a/src/hooks/useWaku.ts +++ b/src/hooks/useWaku.ts @@ -5,10 +5,11 @@ import { Message, waku } from "@/services/waku"; export type MessageContent = { nick: string; text: string; - time: string; + timestamp: number; }; export const useWaku = () => { + const [contentTopic, setContentTopic] = React.useState(CONTENT_TOPIC); const [messages, setMessages] = React.useState>(new Map()); React.useEffect(() => { @@ -19,7 +20,7 @@ export const useWaku = () => { newMessages.forEach((m) => { const payload = JSON.parse(atob(m.payload)); - const message = { + const message: MessageContent = { nick: payload?.nick || "unknown", text: payload?.text || "empty", timestamp: m.timestamp || Date.now(), @@ -30,12 +31,12 @@ export const useWaku = () => { 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); }; - }, [messages, setMessages]); + }, [messages, setMessages, contentTopic]); const onSend = React.useCallback( async (nick: string, text: string) => { @@ -62,5 +63,18 @@ export const useWaku = () => { [setMessages] ); - return { onSend, messages: Array.from(messages.values()) }; + const onContentTopicChange = async (nextContentTopic: string) => { + if (nextContentTopic === contentTopic) { + return; + } + + setContentTopic(nextContentTopic); + }; + + return { + onSend, + contentTopic, + onContentTopicChange, + messages: Array.from(messages.values()) + }; }; diff --git a/src/services/waku.ts b/src/services/waku.ts index bf8f8e3..7ade39b 100644 --- a/src/services/waku.ts +++ b/src/services/waku.ts @@ -18,16 +18,13 @@ const buildURL = (endpoint: string) => `${LOCAL_NODE}${endpoint}`; class Relay { private readonly subscriptionsEmitter = new EventTarget(); - - private contentTopicListeners: Map = 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(contentTopic); return this.subscriptionsEmitter.addEventListener(contentTopic, fn as any); } @@ -38,12 +35,8 @@ class Relay { ); } - 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(contentTopic: string) { + if (this.subscriptionRoutine) { return; } @@ -53,22 +46,13 @@ class Relay { this.subscriptionRoutine = window.setInterval(async () => { await this.fetchMessages(); }, 5 * SECOND); - - this.contentTopicListeners.set(contentTopic, 1); } catch (error) { console.error(`Failed to subscribe node ${contentTopic}:`, error); } } - 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(contentTopic: string) { + if (!this.subscriptionRoutine) { return; } @@ -79,16 +63,9 @@ class Relay { } clearInterval(this.subscriptionRoutine); - this.contentTopicListeners.delete(contentTopic); } private async fetchMessages(): Promise { - const contentTopic = Array.from(this.contentTopicListeners.keys())[0]; - - if (!contentTopic) { - return; - } - const response = await http.get( buildURL(`${RELAY}/messages/${encodeURIComponent(PUBSUB_TOPIC)}`) ); @@ -98,9 +75,27 @@ class Relay { return; } - this.subscriptionsEmitter.dispatchEvent( - new CustomEvent(contentTopic, { detail: body }) - ); + const messagesPerContentTopic = new Map(); + 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 {