diff --git a/.yarn/cache/@types-chai-npm-4.2.22-557883092e-dca66a263b.zip b/.yarn/cache/@types-chai-npm-4.2.22-557883092e-dca66a263b.zip new file mode 100644 index 00000000..629fd645 Binary files /dev/null and b/.yarn/cache/@types-chai-npm-4.2.22-557883092e-dca66a263b.zip differ diff --git a/packages/status-communities/package.json b/packages/status-communities/package.json index 144fe63a..2e1385e3 100644 --- a/packages/status-communities/package.json +++ b/packages/status-communities/package.json @@ -23,6 +23,7 @@ "proto:build": "buf generate" }, "devDependencies": { + "@types/chai": "^4.2.22", "@types/mocha": "^9.0.0", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", diff --git a/packages/status-communities/src/chat.ts b/packages/status-communities/src/chat.ts index 72738a30..1e84d8a5 100644 --- a/packages/status-communities/src/chat.ts +++ b/packages/status-communities/src/chat.ts @@ -14,19 +14,21 @@ export class Chat { return chatIdToContentTopic(this.id); } - private createMessage(text: string): ChatMessage { - const { timestamp, clock } = this.nextClockAndTimestamp(); + public createMessage(text: string): ChatMessage { + const { timestamp, clock } = this._nextClockAndTimestamp(); - return ChatMessage.createMessage(clock, timestamp, text, this.id); + const message = ChatMessage.createMessage(clock, timestamp, text, this.id); + + this._updateFromMessage(message); + + return message; } public handleNewMessage(message: ChatMessage) { - this.updateFromMessage(message); - - // TODO: emit message + this._updateFromMessage(message); } - private nextClockAndTimestamp(): { clock: number; timestamp: number } { + private _nextClockAndTimestamp(): { clock: number; timestamp: number } { let clock = this.lastClockValue; const timestamp = Date.now(); @@ -39,7 +41,7 @@ export class Chat { return { clock, timestamp }; } - private updateFromMessage(message: ChatMessage): void { + private _updateFromMessage(message: ChatMessage): void { if (!this.lastMessage || this.lastMessage.clock <= message.clock) { this.lastMessage = message; } diff --git a/packages/status-communities/src/chat_message.ts b/packages/status-communities/src/chat_message.ts index 854c3555..3e8a879c 100644 --- a/packages/status-communities/src/chat_message.ts +++ b/packages/status-communities/src/chat_message.ts @@ -46,7 +46,15 @@ export class ChatMessage { return new ChatMessage(protoBuf); } + encode(): Uint8Array { + return proto.ChatMessage.encode(this.proto).finish(); + } + public get clock() { return this.proto.clock; } + + get text(): string | undefined { + return this.proto.text; + } } diff --git a/packages/status-communities/src/messenger.spec.ts b/packages/status-communities/src/messenger.spec.ts index 849f4756..264437f1 100644 --- a/packages/status-communities/src/messenger.spec.ts +++ b/packages/status-communities/src/messenger.spec.ts @@ -1,7 +1,6 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: No types available -import TCP from "libp2p-tcp"; +import { expect } from "chai"; +import { ChatMessage } from "./chat_message"; import { Messenger } from "./messenger"; const testChatId = "test-chat-id"; @@ -38,9 +37,25 @@ describe("Messenger", () => { ]); }); - it("Sends message in public chat", function () { + it("Sends & Receive message in public chat", async function () { messenger1.joinChat(testChatId); messenger2.joinChat(testChatId); + + const text = "This is a message."; + + const receivedMessagePromise: Promise = new Promise( + (resolve) => { + messenger2.addObserver((message) => { + resolve(message); + }, testChatId); + } + ); + + messenger1.sendMessage(text, testChatId); + + const receivedMessage = await receivedMessagePromise; + + expect(receivedMessage?.text).to.eq(text); }); afterEach(async function () { diff --git a/packages/status-communities/src/messenger.ts b/packages/status-communities/src/messenger.ts index ef5cf2f2..8376f43e 100644 --- a/packages/status-communities/src/messenger.ts +++ b/packages/status-communities/src/messenger.ts @@ -1,26 +1,33 @@ -import { Waku, WakuMessage } from "js-waku"; -import {CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku"; +import { Waku, WakuMessage } from "js-waku"; +import { CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku"; import { Chat } from "./chat"; import { ChatMessage } from "./chat_message"; export class Messenger { waku: Waku; - chatsById: Map; + observers: { + [chatId: string]: Set<(chatMessage: ChatMessage) => void>; + }; private constructor(waku: Waku) { this.waku = waku; this.chatsById = new Map(); + this.observers = {}; } public static async create(wakuOptions?: WakuCreateOptions) { const _wakuOptions = Object.assign({ bootstrap: true }, wakuOptions); const waku = await Waku.create(_wakuOptions); - const messenger = new Messenger(waku); - return messenger; + return new Messenger(waku); } + /** + * Joins a public chat. + * + * Use `addListener` to get messages received on this chat. + */ public joinChat(chatId: string) { if (this.chatsById.has(chatId)) throw "Chat already joined"; @@ -33,6 +40,12 @@ export class Messenger { const chatMessage = ChatMessage.decode(wakuMessage.payload); chat.handleNewMessage(chatMessage); + + if (this.observers[chatId]) { + this.observers[chatId].forEach((observer) => { + observer(chatMessage); + }); + } }, [chat.contentTopic] ); @@ -40,6 +53,64 @@ export class Messenger { this.chatsById.set(chatId, chat); } + /** + * Sends a message on the given chat Id. + * + * @param text + * @param chatId + */ + public async sendMessage(text: string, chatId: string): Promise { + const chat = this.chatsById.get(chatId); + if (!chat) throw `Chat not joined: ${chatId}`; + + const message = chat.createMessage(text); + + const wakuMessage = await WakuMessage.fromBytes( + message.encode(), + chat.contentTopic + ); + + await this.waku.relay.send(wakuMessage); + } + + /** + * Add an observer of new messages received on the chat. + * + * @throws string If the chat has not been joined first using [joinChat]. + */ + public addObserver( + observer: (chatMessage: ChatMessage) => void, + chatId: string + ) { + // Not sure this is the best design here. Maybe `addObserver` and `joinChat` should be merged. + + if (!this.chatsById.has(chatId)) + throw "Cannot add observer on a chat that is not joined."; + if (!this.observers[chatId]) { + this.observers[chatId] = new Set(); + } + + this.observers[chatId].add(observer); + } + + /** + * Add an observer of new messages received on the chat. + * + * @throws string If the chat has not been joined first using [joinChat]. + */ + + deleteObserver( + observer: (chatMessage: ChatMessage) => void, + chatId: string + ): void { + if (this.observers[chatId]) { + this.observers[chatId].delete(observer); + } + } + + /** + * Stops the messenger. + */ public async stop(): Promise { await this.waku.stop(); } diff --git a/packages/status-communities/tsconfig.json b/packages/status-communities/tsconfig.json index 23446d37..482825d4 100644 --- a/packages/status-communities/tsconfig.json +++ b/packages/status-communities/tsconfig.json @@ -38,7 +38,11 @@ "skipLibCheck": true, "lib": ["es6", "dom"], - "typeRoots": ["./node_modules/@types", "./src/types", "../../node_modules/@types"] + "typeRoots": [ + "./node_modules/@types", + "./src/types", + "../../node_modules/@types" + ] }, "include": ["src"], "exclude": ["node_modules/**"], diff --git a/yarn.lock b/yarn.lock index 66b78e05..b713840e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -448,6 +448,13 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^4.2.22": + version: 4.2.22 + resolution: "@types/chai@npm:4.2.22" + checksum: dca66a263b25c26112c0a8c6df20316412fa54b557443a108836c07cee961aa56cc5b1763273f69eb450c83ca9f28069ff78b617bffc01806cdd83afc1c20c2a + languageName: node + linkType: hard + "@types/debug@npm:^4.1.5": version: 4.1.7 resolution: "@types/debug@npm:4.1.7" @@ -5531,6 +5538,7 @@ fsevents@~2.3.2: version: 0.0.0-use.local resolution: "status-communities@workspace:packages/status-communities" dependencies: + "@types/chai": ^4.2.22 "@types/mocha": ^9.0.0 "@typescript-eslint/eslint-plugin": ^4.31.1 "@typescript-eslint/parser": ^4.31.1