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: {