diff --git a/.pubnub.yml b/.pubnub.yml index 3c4eca9f..d7368f5e 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,5 @@ name: kmp-chat -version: 0.9.0 +version: 0.9.2 schema: 1 scm: github.com/pubnub/kmp-chat sdks: @@ -21,8 +21,8 @@ sdks: - distribution-type: library distribution-repository: maven - package-name: pubnub-chat-0.9.0 - location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-chat/0.9.0/ + package-name: pubnub-chat-0.9.2 + location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-chat/0.9.2/ supported-platforms: supported-operating-systems: Android: @@ -77,6 +77,17 @@ sdks: license-url: https://github.com/pubnub/kotlin/blob/master/LICENSE is-required: Required changelog: + - date: 2024-12-12 + version: v0.9.2 + changes: + - type: feature + text: "Lock moderated messages from editing ." + - type: bug + text: "Wrong user suggestion source for message draft created on ThreadChannel." + - type: bug + text: "Wrong type of last user activity time stored on server (precision)." + - type: improvement + text: "Moderation events are now sent to a channel prefixed with `PUBNUB_INTERNAL_MODERATION.`." - date: 2024-11-06 version: v0.9.0 changes: diff --git a/Package.swift b/Package.swift index 32e6b4c4..489dd560 100644 --- a/Package.swift +++ b/Package.swift @@ -18,8 +18,8 @@ let package = Package( targets: [ .binaryTarget( name: "PubNubChatRemoteBinaryPackage", - url: "https://github.com/pubnub/kmp-chat/releases/download/kotlin-v0.9.0/PubNubChat.xcframework.zip", - checksum: "e043957bd849c7243085368c0e64607cec6dc2e8db6c863f1a80c025d11f6497" + url: "https://github.com/pubnub/kmp-chat/releases/download/kotlin-v0.9.2/PubNubChat.xcframework.zip", + checksum: "ba90004881639c1ae7e05cf93d68554a77717f8f7c89515a1894cce03a557122" ) ] ) diff --git a/README.md b/README.md index 58b05b31..281447f1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ You will need the publish and subscribe keys to authenticate your app. Get your com.pubnub pubnub-chat - 0.9.0 + 0.9.2 ``` diff --git a/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt b/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt index e8e37e3f..8b95d4ed 100644 --- a/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt +++ b/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt @@ -2,6 +2,7 @@ package com.pubnub.gradle import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.plugins.ExtensionAware import org.gradle.api.tasks.testing.AbstractTestTask import org.gradle.api.tasks.testing.logging.TestExceptionFormat @@ -38,9 +39,16 @@ class PubNubKotlinMultiplatformPlugin : Plugin { } pod("PubNubSwift") { -// val swiftPath = project.findProperty("SWIFT_PATH") as? String ?: "swift" -// source = path(rootProject.file(swiftPath)) - version = "8.1.0" + val swiftPath = project.findProperty("SWIFT_PATH") as? String + if (swiftPath != null) { + source = path(rootProject.file(swiftPath)) + } else { + version = project.rootProject + .extensions + .getByType(VersionCatalogsExtension::class.java) + .named("libs") + .findVersion("pubnub.swift").get().requiredVersion + } moduleName = "PubNubSDK" extraOpts += listOf("-compiler-option", "-fmodules") } diff --git a/build.gradle.kts b/build.gradle.kts index 10633c3e..33ae92f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,7 +78,6 @@ kotlin { compilerOptions { target.set("es2015") -// moduleKind.set(JsModuleKind.MODULE_UMD) } binaries.library() } diff --git a/gradle.properties b/gradle.properties index 4af72bc1..bedce09e 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.1 +VERSION_NAME=0.9.2 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. @@ -30,7 +30,7 @@ POM_DEVELOPER_NAME=PubNub POM_DEVELOPER_URL=support@pubnub.com IOS_SIMULATOR_ID=iPhone 15 Pro -SWIFT_PATH=pubnub-kotlin/swift +#SWIFT_PATH=../swift ENABLE_TARGET_JS=true ENABLE_TARGET_IOS_OTHER=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1be46100..8b3b706e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,8 @@ ktlint = "12.1.0" dokka = "1.9.20" kotlinx_serialization = "1.7.3" kotlinx_coroutines = "1.9.0" -pubnub = "10.3.0" +pubnub = "10.3.1" +pubnub_swift = "8.2.2" [libraries] pubnub-kotlin-api = { module = "com.pubnub:pubnub-kotlin-api", version.ref = "pubnub" } diff --git a/js-chat/main.mjs b/js-chat/main.mjs index b8325cb0..c9d62c85 100644 --- a/js-chat/main.mjs +++ b/js-chat/main.mjs @@ -1,4 +1,8 @@ export * from "../build/dist/js/productionLibrary/pubnub-chat.mjs" export const INTERNAL_MODERATION_PREFIX = "PUBNUB_INTERNAL_MODERATION_" +export const MESSAGE_THREAD_ID_PREFIX = "PUBNUB_INTERNAL_THREAD"; +export const INTERNAL_ADMIN_CHANNEL = "PUBNUB_INTERNAL_ADMIN_CHANNEL"; +export const ERROR_LOGGER_KEY_PREFIX = "PUBNUB_INTERNAL_ERROR_LOGGER"; + import PubNub from "pubnub" export let CryptoModule = PubNub.CryptoModule \ No newline at end of file diff --git a/js-chat/tests/message-draft-v2.test.ts b/js-chat/tests/message-draft-v2.test.ts new file mode 100644 index 00000000..bf255c7e --- /dev/null +++ b/js-chat/tests/message-draft-v2.test.ts @@ -0,0 +1,166 @@ +import { Channel, Chat, MessageDraftV2, MixedTextTypedElement } from "../dist" +import { + createChatInstance, + createRandomChannel, + createRandomUser, + renderMessagePart, + sleep, + makeid +} from "./utils" +import { jest } from "@jest/globals" + +describe("MessageDraft", function () { + jest.retryTimes(2) + let chat: Chat + let channel: Channel + let messageDraft: MessageDraftV2 + + beforeAll(async () => { + chat = await createChatInstance() + }) + + beforeEach(async () => { + channel = await createRandomChannel() + messageDraft = channel.createMessageDraftV2({ userSuggestionSource: "global" }) + }) + + test("should mention 2 users", async () => { + const [user1, user2] = await Promise.all([createRandomUser(), createRandomUser()]) + + messageDraft.update("Hello @user1 and @user2") + messageDraft.addMention(6, 6, "mention", user1.id) + messageDraft.addMention(17, 6, "mention", user2.id) + const messagePreview = messageDraft.getMessagePreview() + expect(messagePreview.length).toBe(4) + expect(messagePreview[0].type).toBe("text") + expect(messagePreview[1].type).toBe("mention") + expect(messagePreview[2].type).toBe("text") + expect(messagePreview[3].type).toBe("mention") + expect(messageDraft.value).toBe(`Hello @user1 and @user2`) + expect(messagePreview.map(renderMessagePart).join("")).toBe( + `Hello @@user1 and @@user2` + ) + await Promise.all([user1.delete({ soft: false }), user2.delete({ soft: false })]) + }) + + test("should mention 2 - 3 users next to each other", async () => { + const [user1, user2, user3] = await Promise.all([ + createRandomUser(), + createRandomUser(), + createRandomUser(), + ]) + + let elements: MixedTextTypedElement[][] = [] + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + messageDraft.addChangeListener(async function(state) { + elements.push(state.messageElements) + if (elements.length == 3) { + resolve() + return + } + let mentions = await state.suggestedMentions + messageDraft.insertSuggestedMention(mentions[0], mentions[0].replaceWith) + }) + + messageDraft.update("Hello @Te @Tes @Test") + await promise + + const messagePreview = messageDraft.getMessagePreview() + expect(messagePreview.length).toBe(4) + expect(messagePreview[0].type).toBe("text") + expect(messagePreview[1].type).toBe("mention") + expect(messagePreview[2].type).toBe("text") + expect(messagePreview[3].type).toBe("mention") + expect(messagePreview.map(renderMessagePart).join("")).toBe( + elements[2].map(renderMessagePart).join("") + ) + await Promise.all([ + user1.delete({ soft: false }), + user2.delete({ soft: false }), + user3.delete({ soft: false }), + ]) + }) + + test("should mix every type of message part", async () => { + const [channel1, channel2] = await Promise.all([createRandomChannel(makeid()), createRandomChannel(makeid())]) + const [user1, user2, user4, user5] = await Promise.all([ + createRandomUser(makeid()), + createRandomUser(makeid()), + createRandomUser(makeid()), + createRandomUser(makeid()), + ]) + messageDraft.update("Hello ") + messageDraft.addLinkedText({ + text: "pubnub", + link: "https://pubnub.com", + positionInInput: messageDraft.value.length, + }) + messageDraft.update("Hello pubnub at https://pubnub.com! Hello to ") + messageDraft.addLinkedText({ + text: "google", + link: "https://google.com", + positionInInput: messageDraft.value.length, + }) + + let elements: MixedTextTypedElement[][] = [] + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + messageDraft.addChangeListener(async function(state) { + elements.push(state.messageElements) + let mentions = await state.suggestedMentions + if (mentions.length == 0) { + resolve() + return + } + messageDraft.insertSuggestedMention(mentions[0], mentions[0].replaceWith) + }) + + + messageDraft.update( + `Hello pubnub at https://pubnub.com! Hello to google at https://google.com. Referencing #${channel1.name.substring(0,8)}, #${channel2.name.substring(0,8)}, #blankchannel, @${user1.name.substring(0,8)}, @${user2.name.substring(0,8)}, and mentioning @blankuser3 @${user4.name.substring(0,8)} @${user5.name.substring(0,8)}` + ) + await promise + + const messagePreview = messageDraft.getMessagePreview() + + expect(messagePreview.length).toBe(16) + expect(messagePreview[0].type).toBe("text") + expect(messagePreview[1].type).toBe("textLink") + expect(messagePreview[2].type).toBe("text") + expect(messagePreview[3].type).toBe("textLink") + expect(messagePreview[4].type).toBe("text") + expect(messagePreview[5].type).toBe("channelReference") + expect(messagePreview[6].type).toBe("text") + expect(messagePreview[7].type).toBe("channelReference") + expect(messagePreview[8].type).toBe("text") + expect(messagePreview[9].type).toBe("mention") + expect(messagePreview[10].type).toBe("text") + expect(messagePreview[11].type).toBe("mention") + expect(messagePreview[12].type).toBe("text") + expect(messagePreview[13].type).toBe("mention") + expect(messagePreview[14].type).toBe("text") + expect(messagePreview[15].type).toBe("mention") + expect(messagePreview.map(renderMessagePart).join("")).toBe( + `Hello pubnub at https://pubnub.com! Hello to google at https://google.com. Referencing #${channel1.name}, #${channel2.name}, #blankchannel, @${user1.name}, @${user2.name}, and mentioning @blankuser3 @${user4.name} @${user5.name}` + ) + expect(messageDraft.value).toBe( + `Hello pubnub at https://pubnub.com! Hello to google at https://google.com. Referencing ${channel1.name}, ${channel2.name}, #blankchannel, ${user1.name}, ${user2.name}, and mentioning @blankuser3 ${user4.name} ${user5.name}` + ) + await Promise.all([channel1.delete({ soft: false }), channel2.delete({ soft: false })]) + await Promise.all([ + user1.delete({ soft: false }), + user2.delete({ soft: false }), + user4.delete({ soft: false }), + ]) + }) + +}) diff --git a/js-chat/tests/message.test.ts b/js-chat/tests/message.test.ts index 7181a64e..e8141322 100644 --- a/js-chat/tests/message.test.ts +++ b/js-chat/tests/message.test.ts @@ -1037,11 +1037,11 @@ describe("Send message test", () => { const firstThreadMessage = (await thread.getHistory()).messages[0] - const messageDraft = thread.createMessageDraft() + const messageDraft = thread.createMessageDraftV2() messageDraft.addQuote(firstThreadMessage) - await messageDraft.onChange("This is a forwarded message.") + await messageDraft.update("This is a forwarded message.") await messageDraft.send() await sleep(500) diff --git a/js-chat/tests/utils.ts b/js-chat/tests/utils.ts index 561bcbfc..f6b2fcc2 100644 --- a/js-chat/tests/utils.ts +++ b/js-chat/tests/utils.ts @@ -64,16 +64,18 @@ export async function createChatInstance( return chat } -export function createRandomChannel() { - return chat.createChannel(`channel_${makeid()}`, { - name: "Test Channel", +export function createRandomChannel(prefix?: string) { + if (!prefix) prefix = "" + return chat.createChannel(`${prefix}channel_${makeid()}`, { + name: `${prefix}Test Channel`, description: "This is a test channel", }) } -export function createRandomUser() { - return chat.createUser(`user_${makeid()}`, { - name: "Test User", +export function createRandomUser(prefix?: string) { + if (!prefix) prefix = "" + return chat.createUser(`${prefix}user_${makeid()}`, { + name: `${prefix}Test User`, }) } diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/UuidTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/UuidTest.kt new file mode 100644 index 00000000..196969fd --- /dev/null +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/UuidTest.kt @@ -0,0 +1,13 @@ +package com.pubnub.kmp + +import com.pubnub.chat.internal.generateRandomUuid +import kotlin.test.Test +import kotlin.test.assertTrue + +class UuidTest { + @Test + fun generateUuid() { + val uuid = generateRandomUuid() + assertTrue { uuid.matches(Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")) } + } +} diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt index abe175af..7c09519c 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt @@ -200,6 +200,7 @@ open class ChannelJs internal constructor(internal val channel: Channel, interna fun createMessageDraftV2(config: MessageDraftConfig?): MessageDraftV2Js { return MessageDraftV2Js( + this.chatJs, MessageDraftImpl( this.channel, config?.userSuggestionSource?.let { @@ -209,7 +210,12 @@ open class ChannelJs internal constructor(internal val channel: Channel, interna config?.userLimit ?: 10, config?.channelLimit ?: 10 ), - config + createJsObject { + this.userSuggestionSource = config?.userSuggestionSource ?: "channel" + this.isTypingIndicatorTriggered = config?.isTypingIndicatorTriggered ?: (channel.type != ChannelType.PUBLIC) + this.userLimit = config?.userLimit ?: 10 + this.channelLimit = config?.channelLimit ?: 10 + } ) } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt b/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt index bc15f1ea..bdca582b 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt @@ -1,12 +1,14 @@ @file:OptIn(ExperimentalJsExport::class) import com.pubnub.chat.MentionTarget +import com.pubnub.chat.MessageDraftChangeListener import com.pubnub.chat.MessageElement +import com.pubnub.chat.SuggestedMention import com.pubnub.chat.internal.MessageDraftImpl import com.pubnub.chat.types.InputFile import com.pubnub.kmp.JsMap +import com.pubnub.kmp.PNFuture import com.pubnub.kmp.UploadableImpl -import com.pubnub.kmp.createJsObject import com.pubnub.kmp.then import com.pubnub.kmp.toMap import kotlin.js.Promise @@ -14,9 +16,11 @@ import kotlin.js.Promise @JsExport @JsName("MessageDraftV2") class MessageDraftV2Js internal constructor( + private val chat: ChatJs, private val messageDraft: MessageDraftImpl, - val config: MessageDraftConfig?, + val config: MessageDraftConfig, ) { + val channel: ChannelJs get() = messageDraft.channel.asJs(chat) val value: String get() = messageDraft.value.toString() var quotedMessage: MessageJs? = null var files: Any? = null @@ -41,40 +45,8 @@ class MessageDraftV2Js internal constructor( messageDraft.removeMention(positionOnInput) } - fun getMessagePreview(): Array { - return messageDraft.getMessageElements().map { element -> - when (element) { - is MessageElement.Link -> when (val target = element.target) { - is MentionTarget.Channel -> createJsObject { - this.type = "channelReference" - this.content = createJsObject { - this.name = element.text.substring(1) - this.id = target.channelId - } - } - is MentionTarget.Url -> createJsObject { - this.type = "textLink" - this.content = createJsObject { - this.text = element.text - this.link = target.url - } - } - is MentionTarget.User -> createJsObject { - this.type = "mention" - this.content = createJsObject { - this.name = element.text.substring(1) - this.id = target.userId - } - } - } - is MessageElement.PlainText -> createJsObject { - this.type = "text" - this.content = createJsObject { - this.text = element.text - } - } - } - }.toTypedArray() + fun getMessagePreview(): Array { + return messageDraft.getMessageElements().toJs() } fun send(options: PubNub.PublishParameters?): Promise { @@ -96,30 +68,120 @@ class MessageDraftV2Js internal constructor( options?.ttl?.toInt() ).then { it.toPublishResponse() }.asPromise() } -} -external interface MessageElementJs { - var type: String - var content: MessageElementPayloadJs -} + fun addChangeListener(listener: (MessageDraftState) -> Unit) { + messageDraft.addChangeListener(MessageDraftListenerJs(listener)) + } -external interface MessageElementPayloadJs { - interface Text : MessageElementPayloadJs { - var text: String + fun removeChangeListener(listener: (MessageDraftState) -> Unit) { + messageDraft.removeChangeListener(MessageDraftListenerJs(listener)) } - interface User : MessageElementPayloadJs { - var name: String - var id: String + fun insertText(offset: Int, text: String) = messageDraft.insertText(offset, text) + + fun removeText(offset: Int, length: Int) = messageDraft.removeText(offset, length) + + fun insertSuggestedMention(mention: SuggestedMentionJs, text: String) { + return messageDraft.insertSuggestedMention( + SuggestedMention( + mention.offset, + mention.replaceFrom, + mention.replaceWith, + when (mention.type) { + TYPE_MENTION -> MentionTarget.User(mention.target) + TYPE_CHANNEL_REFERENCE -> MentionTarget.Channel(mention.target) + TYPE_TEXT_LINK -> MentionTarget.Url(mention.target) + else -> throw IllegalStateException("Unknown target type") + } + ), + text + ) } - interface Link : MessageElementPayloadJs { - var text: String - var link: String + fun addMention(offset: Int, length: Int, mentionType: String, mentionTarget: String) { + return messageDraft.addMention( + offset, + length, + when (mentionType) { + TYPE_MENTION -> MentionTarget.User(mentionTarget) + TYPE_CHANNEL_REFERENCE -> MentionTarget.Channel(mentionTarget) + TYPE_TEXT_LINK -> MentionTarget.Url(mentionTarget) + else -> throw IllegalStateException("Unknown target type") + } + ) } - interface Channel : MessageElementPayloadJs { - var name: String - var id: String + fun removeMention(offset: Int) = messageDraft.removeMention(offset) + + fun update(text: String) = messageDraft.update(text) +} + +@JsExport +class MessageDraftState internal constructor( + val messageElements: Array, + suggestedMentionsFuture: PNFuture> +) { + val suggestedMentions: Promise> by lazy { + suggestedMentionsFuture.then { + it.map { + SuggestedMentionJs( + it.offset, + it.replaceFrom, + it.replaceWith, + when (it.target) { + is MentionTarget.Channel -> TYPE_CHANNEL_REFERENCE + is MentionTarget.Url -> TYPE_TEXT_LINK + is MentionTarget.User -> TYPE_MENTION + }, + when (val link = it.target) { + is MentionTarget.Channel -> link.channelId + is MentionTarget.Url -> link.url + is MentionTarget.User -> link.userId + } + ) + }.toTypedArray() + }.asPromise() + } +} + +data class MessageDraftListenerJs(val listener: (MessageDraftState) -> Unit) : MessageDraftChangeListener { + override fun onChange( + messageElements: List, + suggestedMentions: PNFuture>, + ) { + listener( + MessageDraftState( + messageElements.toJs(), + suggestedMentions + ) + ) } } + +@JsExport +@JsName("SuggestedMention") +class SuggestedMentionJs( + val offset: Int, + val replaceFrom: String, + val replaceWith: String, + val type: String, + val target: String, +) + +private const val TYPE_CHANNEL_REFERENCE = "channelReference" +private const val TYPE_TEXT_LINK = "textLink" +private const val TYPE_MENTION = "mention" +private const val TYPE_TEXT = "text" + +fun List.toJs() = map { element -> + when (element) { + is MessageElement.Link -> when (val target = element.target) { + is MentionTarget.Channel -> MixedTextTypedElement.ChannelReference( + ChannelReferenceContent(target.channelId, element.text) + ) + is MentionTarget.Url -> MixedTextTypedElement.TextLink(TextLinkContent(target.url, element.text)) + is MentionTarget.User -> MixedTextTypedElement.Mention(MentionContent(target.userId, element.text)) + } + is MessageElement.PlainText -> MixedTextTypedElement.Text(TextContent(element.text)) + } +}.toTypedArray() diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt index f1a8dd20..a808a23b 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt @@ -3,6 +3,7 @@ import com.pubnub.api.PubNubError import com.pubnub.api.adjustCollectionTypes import com.pubnub.chat.Message +import com.pubnub.chat.internal.MessageDraftImpl import com.pubnub.chat.internal.message.BaseMessage import com.pubnub.chat.types.EventContent import com.pubnub.chat.types.MessageMentionedUser @@ -67,13 +68,24 @@ open class MessageJs internal constructor(internal val message: Message, interna return message.streamUpdates { it.asJs(chatJs) }::close } + fun getLinkedText() = getMessageElements() + fun getMessageElements(): Array { - return MessageElementsUtils.getMessageElements( - text, - mentionedUsers?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), - textLinks?.toList() ?: emptyList(), - referencedChannels?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), - ) + // data from v1 message draft + if (mentionedUsers?.toMap()?.isNotEmpty() == true || + textLinks?.isNotEmpty() == true || + referencedChannels?.toMap()?.isNotEmpty() == true + ) { + return MessageElementsUtils.getMessageElements( + text, + mentionedUsers?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), + textLinks?.toList() ?: emptyList(), + referencedChannels?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), + ) + } else { + // use v2 message draft + return MessageDraftImpl.getMessageElements(text).toJs() + } } fun editText(newText: String): Promise { diff --git a/pubnub-chat-impl/src/jsMain/kotlin/com/pubnub/chat/internal/ChatImpl.js.kt b/pubnub-chat-impl/src/jsMain/kotlin/com/pubnub/chat/internal/ChatImpl.js.kt index e734ab91..8300bea1 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/com/pubnub/chat/internal/ChatImpl.js.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/com/pubnub/chat/internal/ChatImpl.js.kt @@ -1,16 +1,26 @@ package com.pubnub.chat.internal +import kotlin.experimental.and +import kotlin.experimental.or +import kotlin.math.floor +import kotlin.random.Random import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid external val globalThis: dynamic @OptIn(ExperimentalUuidApi::class) actual fun generateRandomUuid(): String { - val process = js("process") - if (process !== undefined && process.versions && process.versions.node && globalThis.crypto === undefined) { - // Node.js environment detected - globalThis.crypto = js("require('crypto')") + val uuid = ByteArray(32) + for (i in 0 until 32) { + uuid[i] = floor(Random.nextDouble() * 16).toInt().toByte() } - return Uuid.random().toString() + uuid[12] = 4; // set bits 12-15 of time-high-and-version to 0100 + uuid[16] = uuid[19] and (1 shl 2).inv().toByte() // set bit 6 of clock-seq-and-reserved to zero + uuid[16] = uuid[19] or (1 shl 3).toByte(); // set bit 7 of clock-seq-and-reserved to one + val uuidString = uuid.joinToString("") { it.toString(16) } + return uuidString.substring(0, 8) + + "-" + uuidString.substring(8, 12) + + "-" + uuidString.substring(12, 16) + + "-" + uuidString.substring(16, 20) + + "-" + uuidString.substring(20) } diff --git a/src/jsMain/resources/index.d.ts b/src/jsMain/resources/index.d.ts index be0c3e00..27ea9eb1 100644 --- a/src/jsMain/resources/index.d.ts +++ b/src/jsMain/resources/index.d.ts @@ -421,6 +421,43 @@ type AddLinkedTextParams = { link: string; positionInInput: number; }; + +export declare class MessageDraftV2 { + get channel(): Channel; + get value(): string; + quotedMessage: Message | undefined; + readonly config: MessageDraftConfig; + files?: FileList | File[] | SendFileParameters["file"][]; + addQuote(message: Message): void; + removeQuote(): void; + addLinkedText(params: AddLinkedTextParams): void; + removeLinkedText(positionInInput: number): void; + getMessagePreview(): MixedTextTypedElement[]; + send(params?: MessageDraftOptions): Promise; + addChangeListener(listener: (p0: MessageDraftState) => void): void; + removeChangeListener(listener: (p0: MessageDraftState) => void): void; + insertText(offset: number, text: string): void; + removeText(offset: number, length: number): void; + insertSuggestedMention(mention: SuggestedMention, text: string): void; + addMention(offset: number, length: number, mentionType: TextTypes, mentionTarget: string): void; + removeMention(offset: number): void; + update(text: string): void; +} + +export declare class MessageDraftState { + private constructor(); + get messageElements(): Array; + get suggestedMentions(): Promise>; +} + +export declare class SuggestedMention { + offset: number; + replaceFrom: string; + replaceWith: string; + type: TextTypes; + target: string; +} + declare class MessageDraft { private chat; value: string; @@ -527,6 +564,7 @@ declare class Channel { limit: number; }): Promise; createMessageDraft(config?: Partial): MessageDraft; + createMessageDraftV2(config?: Partial): MessageDraftV2; registerForPush(): Promise; unregisterFromPush(): Promise; streamReadReceipts(callback: (receipts: {