From 1ba266d3935d1e33b2de9a15155b733c4f928566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojtek=20Kalici=C5=84ski?= <146713236+wkal-pubnub@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:54:52 +0100 Subject: [PATCH] Fix Channel type being erased and custom message handling (#154) * Fix Channel type being erased on update * fix: custom events sending/receiving in JS * PubNub js 0.9.6 release. --------- Co-authored-by: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com> --- .gitignore | 1 + gradle.properties | 2 +- js-chat/.pubnub.yml | 9 +++- js-chat/jest.config.js | 2 +- js-chat/package.json | 6 +-- js-chat/package_template.json | 2 +- js-chat/rollup-test.config.mjs | 20 ++++++++ js-chat/tests/channel.test.ts | 36 ++++++++++++- js-chat/tests/message-draft-v2.test.ts | 2 +- js-chat/tests/message-draft.test.ts | 2 +- js-chat/tests/message.test.ts | 2 +- js-chat/tests/testUtils.ts | 6 +-- js-chat/tests/timetoken.test.ts | 2 +- js-chat/tests/typing-indicator.test.ts | 2 +- js-chat/tests/user.test.ts | 4 +- js-chat/tests/utils.ts | 6 +-- .../com/pubnub/chat/internal/utils/json.kt | 4 +- .../src/jsMain/kotlin/ChannelJs.kt | 4 +- pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt | 50 +++++++++++++------ .../src/jsMain/kotlin/MembershipJs.kt | 4 +- .../src/jsMain/kotlin/ThreadChannelJs.kt | 35 +++++++++---- pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt | 2 +- pubnub-chat-impl/src/jsMain/kotlin/types.kt | 4 ++ 23 files changed, 155 insertions(+), 52 deletions(-) create mode 100644 js-chat/rollup-test.config.mjs diff --git a/.gitignore b/.gitignore index b10d3c5a..43e15cd7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ **/*.podspec node_modules js-chat/dist +js-chat/dist-test test.properties ### IntelliJ IDEA ### diff --git a/gradle.properties b/gradle.properties index 7adc8ee9..c78d4d29 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ SONATYPE_HOST=DEFAULT SONATYPE_AUTOMATIC_RELEASE=false GROUP=com.pubnub POM_PACKAGING=jar -VERSION_NAME=0.9.5 +VERSION_NAME=0.9.6 POM_NAME=PubNub Chat SDK POM_DESCRIPTION=This SDK offers a set of handy methods to create your own feature-rich chat or add a chat to your existing application. diff --git a/js-chat/.pubnub.yml b/js-chat/.pubnub.yml index 2c0d5f50..0c6100de 100644 --- a/js-chat/.pubnub.yml +++ b/js-chat/.pubnub.yml @@ -1,11 +1,18 @@ --- name: pubnub-js-chat -version: 0.9.5 +version: 0.9.6 scm: github.com/pubnub/js-chat schema: 1 files: - lib/dist/index.js changelog: + - date: 2025-01-02 + version: 0.9.6 + changes: + - type: bug + text: "Channel type was erased (set to `null`) on edits to other Channel fields." + - type: bug + text: "Custom events sending/receiving in JS." - date: 2024-12-20 version: 0.9.5 changes: diff --git a/js-chat/jest.config.js b/js-chat/jest.config.js index ce7bff10..aee59f1e 100644 --- a/js-chat/jest.config.js +++ b/js-chat/jest.config.js @@ -1,5 +1,5 @@ module.exports = { bail: false, - reporters: [["jest-silent-reporter", { "useDots": true }], "jest-junit", "summary"], + reporters: ["default", "jest-junit", "summary"], testTimeout: 10000 } diff --git a/js-chat/package.json b/js-chat/package.json index 8f0e78f7..69fb45e2 100644 --- a/js-chat/package.json +++ b/js-chat/package.json @@ -15,7 +15,7 @@ "NOTICE" ], "scripts": { - "test": "jest --forceExit", + "test": "cp -r dist/ dist-test && rollup --config rollup-test.config.mjs && jest --forceExit", "build": "rollup -c", "dev": "tsc -w" }, @@ -41,10 +41,10 @@ "module": "dist/index.es.js", "types": "dist/index.d.ts", "react-native": "dist/index.es.js", - "version": "0.9.5", + "version": "0.9.6", "name": "@pubnub/chat", "dependencies": { - "pubnub": "8.2.8", + "pubnub": "8.3.1", "format-util": "^1.0.5" } } \ No newline at end of file diff --git a/js-chat/package_template.json b/js-chat/package_template.json index d7718c2c..0337781b 100644 --- a/js-chat/package_template.json +++ b/js-chat/package_template.json @@ -14,7 +14,7 @@ "dist", "NOTICE" ], "scripts": { - "test": "jest --forceExit", + "test": "cp -r dist/ dist-test && rollup --config rollup-test.config.mjs && jest --forceExit", "build": "rollup -c", "dev": "tsc -w" }, diff --git a/js-chat/rollup-test.config.mjs b/js-chat/rollup-test.config.mjs new file mode 100644 index 00000000..17cb2da7 --- /dev/null +++ b/js-chat/rollup-test.config.mjs @@ -0,0 +1,20 @@ +import pkg from "./package.json" assert { type: "json" } + +export default [ + { + input: "./main.mjs", + external: ["pubnub", "format-util"], + output: [ + { + file: "dist-test/index.js", + format: "cjs", + }, + { + file: "dist-test/index.es.js", + format: "esm", + }, + ], + plugins: [ + ], + }, +] diff --git a/js-chat/tests/channel.test.ts b/js-chat/tests/channel.test.ts index 37e3f51a..aec4b3d3 100644 --- a/js-chat/tests/channel.test.ts +++ b/js-chat/tests/channel.test.ts @@ -5,7 +5,7 @@ import { MessageDraft, INTERNAL_MODERATION_PREFIX, Membership, -} from "../dist" +} from "../dist-test" import { sleep, extractMentionedUserIds, @@ -350,6 +350,7 @@ describe("Channel test", () => { test("should stream channel updates and invoke the callback", async () => { let updatedChannel + channel = await channel.update({ type: "public" }) const name = "Updated Channel" const callback = jest.fn((chanel) => (updatedChannel = chanel)) @@ -360,7 +361,7 @@ describe("Channel test", () => { expect(callback).toHaveBeenCalled() expect(callback).toHaveBeenCalledWith(updatedChannel) expect(updatedChannel.name).toEqual(name) - + expect(updatedChannel.type).toEqual(channel.type) stopUpdates() }) @@ -1229,4 +1230,35 @@ describe("Channel test", () => { let result = await chat.getChannels({ limit: 2, filter: `type == 'public'` }) expect(result.channels.length).toBe(2) }) + + test("send custom event", async () => { + const inviteCallback = jest.fn() + const unsubscribe = chat.listenForEvents({ + channel: channel.id, + type: "custom", + method: "publish", + callback: inviteCallback, + }) + + await sleep(1000) + await chat.emitEvent({ + channel: channel.id, + type: 'custom', + method: 'publish', + payload: { + action: "action", + body: "payload" + } + }) + await sleep(2000) + expect(inviteCallback).toHaveBeenCalledTimes(1) + expect(inviteCallback).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expect.objectContaining({ + action: "action", + body: "payload" + }), + }) + ) + }) }) diff --git a/js-chat/tests/message-draft-v2.test.ts b/js-chat/tests/message-draft-v2.test.ts index e2763f3c..ee0e7d7e 100644 --- a/js-chat/tests/message-draft-v2.test.ts +++ b/js-chat/tests/message-draft-v2.test.ts @@ -1,4 +1,4 @@ -import { Channel, Chat, MessageDraftV2, MixedTextTypedElement } from "../dist" +import { Channel, Chat, MessageDraftV2, MixedTextTypedElement } from "../dist-test" import { createChatInstance, createRandomChannel, diff --git a/js-chat/tests/message-draft.test.ts b/js-chat/tests/message-draft.test.ts index c66f594a..9f230356 100644 --- a/js-chat/tests/message-draft.test.ts +++ b/js-chat/tests/message-draft.test.ts @@ -1,4 +1,4 @@ -import { Channel, Chat } from "../dist" +import { Channel, Chat } from "../dist-test" import { createChatInstance, createRandomChannel, diff --git a/js-chat/tests/message.test.ts b/js-chat/tests/message.test.ts index e8141322..588807a2 100644 --- a/js-chat/tests/message.test.ts +++ b/js-chat/tests/message.test.ts @@ -7,7 +7,7 @@ import { CryptoUtils, CryptoModule, MessageDTOParams, -} from "../dist" +} from "../dist-test" import { createChatInstance, createRandomChannel, diff --git a/js-chat/tests/testUtils.ts b/js-chat/tests/testUtils.ts index 202ca8c9..230356de 100644 --- a/js-chat/tests/testUtils.ts +++ b/js-chat/tests/testUtils.ts @@ -1,9 +1,9 @@ // lib/tests/testUtils.ts -import { Chat } from "../dist" -import { Channel } from "../dist" +import { Chat } from "../dist-test" +import { Channel } from "../dist-test" import * as dotenv from "dotenv" import { nanoid } from "nanoid" -import { User } from "../dist" +import { User } from "../dist-test" dotenv.config() diff --git a/js-chat/tests/timetoken.test.ts b/js-chat/tests/timetoken.test.ts index 3c82070c..f8a9eb78 100644 --- a/js-chat/tests/timetoken.test.ts +++ b/js-chat/tests/timetoken.test.ts @@ -1,4 +1,4 @@ -import { TimetokenUtils } from "../dist" +import { TimetokenUtils } from "../dist-test" describe("Channel test", () => { test("should convert unix timestamp to PubNub timetoken", () => { diff --git a/js-chat/tests/typing-indicator.test.ts b/js-chat/tests/typing-indicator.test.ts index 72b528c9..bfc93d82 100644 --- a/js-chat/tests/typing-indicator.test.ts +++ b/js-chat/tests/typing-indicator.test.ts @@ -1,4 +1,4 @@ -import { Channel, Chat } from "../dist" +import { Channel, Chat } from "../dist-test" import { createChatInstance, createRandomChannel, sleep } from "./utils" describe("Typing indicator test", () => { diff --git a/js-chat/tests/user.test.ts b/js-chat/tests/user.test.ts index 40ed57b5..6f7c9b53 100644 --- a/js-chat/tests/user.test.ts +++ b/js-chat/tests/user.test.ts @@ -1,6 +1,6 @@ -import { Chat, INTERNAL_MODERATION_PREFIX, User } from "../dist" +import { Chat, INTERNAL_MODERATION_PREFIX, User } from "../dist-test" import { createChatInstance, createRandomUser, sleep } from "./utils" -import { INTERNAL_ADMIN_CHANNEL } from "../dist" +import { INTERNAL_ADMIN_CHANNEL } from "../dist-test" describe("User test", () => { let chat: Chat diff --git a/js-chat/tests/utils.ts b/js-chat/tests/utils.ts index f6b2fcc2..2328f04f 100644 --- a/js-chat/tests/utils.ts +++ b/js-chat/tests/utils.ts @@ -1,8 +1,8 @@ // lib/tests/testUtils.ts -import { Chat, MessageDraft, Channel, Message, ChatConfig } from "../dist" +import { Chat, MessageDraft, Channel, Message, ChatConfig } from "../dist-test" import * as dotenv from "dotenv" -import { User } from "../dist" -import { MixedTextTypedElement } from "../dist" +import { User } from "../dist-test" +import { MixedTextTypedElement } from "../dist-test" import PubNub from "pubnub" dotenv.config() diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt index 125a80e6..872cb43e 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt @@ -1,3 +1,5 @@ +import com.pubnub.chat.internal.TYPE_OF_MESSAGE +import com.pubnub.chat.internal.TYPE_OF_MESSAGE_IS_CUSTOM import com.pubnub.chat.internal.defaultGetMessagePublishBody import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.types.EventContent @@ -53,7 +55,7 @@ internal fun EventContent.encodeForSending( var finalMessage = if (this is EventContent.Custom) { buildMap { putAll(data) - put("type", "custom") + put(TYPE_OF_MESSAGE, TYPE_OF_MESSAGE_IS_CUSTOM) } } else { PNDataEncoder.encode(this) as Map diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt index 7c09519c..02863d70 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt @@ -31,10 +31,10 @@ open class ChannelJs internal constructor(internal val channel: Channel, interna fun update(data: ChannelFields): Promise { return channel.update( data.name, - convertToCustomObject(data.custom), + data.custom?.let { convertToCustomObject(it) }, data.description, data.status, - ChannelType.from(data.type) + data.type?.let { ChannelType.from(it) } ).then { it.asJs(chatJs) }.asPromise() diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt index e477e3cb..52d59c2d 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt @@ -5,6 +5,7 @@ import com.pubnub.api.createJsonElement import com.pubnub.chat.internal.ChatImpl import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.PUBNUB_CHAT_VERSION +import com.pubnub.chat.internal.TYPE_OF_MESSAGE_IS_CUSTOM import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.restrictions.Restriction import com.pubnub.chat.types.ChannelMentionData @@ -14,8 +15,10 @@ import com.pubnub.chat.types.CreateGroupConversationResult import com.pubnub.chat.types.EmitEventMethod import com.pubnub.chat.types.EventContent import com.pubnub.chat.types.ThreadMentionData +import com.pubnub.kmp.JsMap import com.pubnub.kmp.createJsObject import com.pubnub.kmp.then +import com.pubnub.kmp.toMap import kotlin.js.Json import kotlin.js.Promise import kotlin.js.json @@ -31,26 +34,39 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig val channel: String = event.channel ?: event.user val type = event.type val payload = event.payload - payload.type = type + + val method = if (event.method == EmitEventMethod.SIGNAL.toJs()) { + EmitEventMethod.SIGNAL + } else { + EmitEventMethod.PUBLISH + } + + val eventContent = if (type == TYPE_OF_MESSAGE_IS_CUSTOM) { + EventContent.Custom((payload as JsMap).toMap(), method) + } else { + payload.type = type + PNDataEncoder.decode(createJsonElement(payload)) + } return chat.emitEvent( channel, - PNDataEncoder.decode(createJsonElement(payload)) + eventContent ).then { it.toPublishResponse() }.asPromise() } fun listenForEvents(event: ListenForEventsParams): () -> Unit { val klass = when (event.type) { - "typing" -> EventContent.Typing::class - "report" -> EventContent.Report::class - "receipt" -> EventContent.Receipt::class - "mention" -> EventContent.Mention::class - "invite" -> EventContent.Invite::class - "custom" -> EventContent.Custom::class - "moderation" -> EventContent.Moderation::class + EventContent.Typing.serializer().descriptor.serialName -> EventContent.Typing::class + EventContent.Report.serializer().descriptor.serialName -> EventContent.Report::class + EventContent.Receipt.serializer().descriptor.serialName -> EventContent.Receipt::class + EventContent.Mention.serializer().descriptor.serialName -> EventContent.Mention::class + EventContent.Invite.serializer().descriptor.serialName -> EventContent.Invite::class + TYPE_OF_MESSAGE_IS_CUSTOM -> EventContent.Custom::class + EventContent.Moderation.serializer().descriptor.serialName -> EventContent.Moderation::class + EventContent.TextMessageContent.serializer().descriptor.serialName -> EventContent.TextMessageContent::class else -> throw IllegalArgumentException("Unknown event type ${event.type}") } val channel: String = event.channel ?: event.user!! - val method = if (event.method == "signal") { + val method = if (event.method == EmitEventMethod.SIGNAL.toJs()) { EmitEventMethod.SIGNAL } else { EmitEventMethod.PUBLISH @@ -89,7 +105,7 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig data.externalId, data.profileUrl, data.email, - convertToCustomObject(data.custom), + data.custom?.let { convertToCustomObject(data.custom) }, data.status, data.type ).then { it.asJs(this@ChatJs) }.asPromise() @@ -102,7 +118,7 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig data.externalId, data.profileUrl, data.email, - convertToCustomObject(data.custom), + data.custom?.let { convertToCustomObject(data.custom) }, data.status, data.type ).then { it.asJs(this@ChatJs) }.asPromise() @@ -142,10 +158,10 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig return chat.updateChannel( id, data.name, - convertToCustomObject(data.custom), + data.custom?.let { convertToCustomObject(it) }, data.description, data.status, - ChannelType.from(data.type) + data.type?.let { ChannelType.from(data.type) } ).then { it.asJs(this@ChatJs) }.asPromise() } @@ -391,3 +407,9 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig } } } + +private fun EmitEventMethod.toJs() = if (this == EmitEventMethod.SIGNAL) { + "signal" +} else { + "publish" +} diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt index 1890228b..154c9f3f 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt @@ -19,8 +19,8 @@ class MembershipJs internal constructor(internal val membership: Membership, int val lastReadMessageTimetoken: String? get() = membership.lastReadMessageTimetoken?.toString() - fun update(custom: dynamic): Promise { - return membership.update(convertToCustomObject(custom?.custom)) + fun update(custom: UpdateMembershipParams?): Promise { + return membership.update(custom?.custom?.let { convertToCustomObject(it) }) .then { it.asJs(chatJs) } .asPromise() } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt index 51a5ab70..418dcbb9 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt @@ -2,6 +2,9 @@ import com.pubnub.chat.Event import com.pubnub.chat.ThreadChannel +import com.pubnub.chat.internal.TYPE_OF_MESSAGE +import com.pubnub.chat.internal.TYPE_OF_MESSAGE_IS_CUSTOM +import com.pubnub.chat.types.EventContent import com.pubnub.kmp.createJsObject import com.pubnub.kmp.then import kotlinx.serialization.ExperimentalSerializationApi @@ -48,16 +51,28 @@ external fun delete(p: dynamic): Boolean @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) internal fun Event<*>.toJs(chatJs: ChatJs): EventJs { - return EventJs( - chatJs, - timetoken.toString(), - payload::class.serializer().descriptor.serialName, - payload.toJsObject().apply { - delete(this.asDynamic()["type"]) - }, - channelId, - userId - ) + val customPayload = payload as? EventContent.Custom + return if (customPayload != null) { + EventJs( + chatJs, + timetoken.toString(), + TYPE_OF_MESSAGE_IS_CUSTOM, + customPayload.data.toJsObject(), + channelId, + userId + ) + } else { + EventJs( + chatJs, + timetoken.toString(), + payload::class.serializer().descriptor.serialName, + payload.toJsObject().apply { + delete(this.asDynamic()[TYPE_OF_MESSAGE]) + }, + channelId, + userId + ) + } } internal fun ThreadChannel.asJs(chat: ChatJs) = ThreadChannelJs(this, chat) diff --git a/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt index a80bfd97..fafa9fef 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt @@ -30,7 +30,7 @@ class UserJs internal constructor(internal val user: User, internal val chatJs: data.externalId, data.profileUrl, data.email, - convertToCustomObject(data.custom), + data.custom?.let { convertToCustomObject(it) }, data.status, data.type ).then { diff --git a/pubnub-chat-impl/src/jsMain/kotlin/types.kt b/pubnub-chat-impl/src/jsMain/kotlin/types.kt index 11ab02e3..4fbaeeda 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/types.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/types.kt @@ -310,3 +310,7 @@ external interface Reaction { var uuid: String var actionTimetoken: String } + +external interface UpdateMembershipParams { + val custom: PubNub.CustomObject? +}