From 58c09685767119669b5efe0792d0a8bd4da140d9 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 14 Jan 2026 14:35:36 +0530 Subject: [PATCH 1/3] Added missing chat swift/kotlin documentation for media and replies --- src/pages/docs/chat/rooms/history.mdx | 2 +- src/pages/docs/chat/rooms/media.mdx | 610 ++++++++++++++++++ .../docs/chat/rooms/message-reactions.mdx | 36 ++ src/pages/docs/chat/rooms/replies.mdx | 238 +++++++ 4 files changed, 885 insertions(+), 1 deletion(-) diff --git a/src/pages/docs/chat/rooms/history.mdx b/src/pages/docs/chat/rooms/history.mdx index 2673214b58..c6985e2d79 100644 --- a/src/pages/docs/chat/rooms/history.mdx +++ b/src/pages/docs/chat/rooms/history.mdx @@ -54,7 +54,7 @@ const MyComponent = () => { ``` ```swift -let paginatedResult = try await room.messages.history(withOptions: .init(orderBy: .newestFirst)) +let paginatedResult = try await room.messages.history(withParams: .init(orderBy: .newestFirst)) print(paginatedResult.items) if let next = try await paginatedResult.next { print(next.items) diff --git a/src/pages/docs/chat/rooms/media.mdx b/src/pages/docs/chat/rooms/media.mdx index a4f72b2e3c..4c009243e2 100644 --- a/src/pages/docs/chat/rooms/media.mdx +++ b/src/pages/docs/chat/rooms/media.mdx @@ -77,6 +77,75 @@ const ChatComponent = () => { ); }; ``` + +```swift +struct MediaData { + let id: String + let title: String + let width: Int + let height: Int +} + +class ChatViewController { + var mediaToAttach: [MediaData] = [] + + func onMediaAttach() async { + let mediaData = await uploadMedia() + mediaToAttach.append(mediaData) + } +} +``` + +```kotlin +data class MediaData( + val id: String, + val title: String, + val width: Int, + val height: Int +) + +var mediaToAttach = mutableListOf() + +suspend fun onMediaAttach() { + val mediaData = uploadMedia() + mediaToAttach.add(mediaData) +} +``` + +```jetpack +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import kotlinx.coroutines.launch + +data class MediaData( + val id: String, + val title: String, + val width: Int, + val height: Int +) + +@Composable +fun MediaAttachmentComponent() { + var mediaToAttach by remember { mutableStateOf>(emptyList()) } + val coroutineScope = rememberCoroutineScope() + + Column { + Button(onClick = { + coroutineScope.launch { + val mediaData = uploadMedia() + mediaToAttach = mediaToAttach + mediaData + } + }) { + Text("Attach Media") + } + + mediaToAttach.forEach { mediaData -> + Text("Media to attach: ${mediaData.id} (${mediaData.title}, ${mediaData.width}x${mediaData.height})") + } + } +} +``` ## Send a message @@ -142,6 +211,122 @@ const MessageSender = () => { ); }; ``` + +```swift +func send(text: String, mediaToAttach: [MediaData]) async throws { + var metadata: MessageMetadata = [:] + if !mediaToAttach.isEmpty { + let mediaArray: [JSONValue] = mediaToAttach.map { media in + .object([ + "id": .string(media.id), + "title": .string(media.title), + "width": .number(Double(media.width)), + "height": .number(Double(media.height)) + ]) + } + metadata["media"] = .array(mediaArray) + } + + try await room.messages.send(params: .init( + text: text, + metadata: metadata + )) +} +``` + +```kotlin +suspend fun send(text: String, mediaToAttach: List) { + val metadata = if (mediaToAttach.isNotEmpty()) { + buildJsonObject { + put("media", buildJsonArray { + mediaToAttach.forEach { media -> + add(buildJsonObject { + put("id", media.id) + put("title", media.title) + put("width", media.width) + put("height", media.height) + }) + } + }) + } + } else { + null + } + + room.messages.send( + text = text, + metadata = metadata + ) +} +``` + +```jetpack +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import kotlinx.coroutines.launch +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +@Composable +fun MessageSenderComponent(room: Room) { + var mediaToAttach by remember { mutableStateOf>(emptyList()) } + var messageText by remember { mutableStateOf("") } + val coroutineScope = rememberCoroutineScope() + + Column { + TextField( + value = messageText, + onValueChange = { messageText = it }, + placeholder = { Text("Type a message...") } + ) + + Button(onClick = { + coroutineScope.launch { + val mediaData = uploadMedia() + mediaToAttach = mediaToAttach + mediaData + } + }) { + Text("Attach Media") + } + + Button(onClick = { + coroutineScope.launch { + val metadata = if (mediaToAttach.isNotEmpty()) { + buildJsonObject { + put("media", buildJsonArray { + mediaToAttach.forEach { media -> + add(buildJsonObject { + put("id", media.id) + put("title", media.title) + put("width", media.width) + put("height", media.height) + }) + } + }) + } + } else null + + room.messages.send( + text = messageText, + metadata = metadata + ) + + mediaToAttach = emptyList() + messageText = "" + } + }) { + Text("Send") + } + + mediaToAttach.forEach { mediaData -> + Text("Media to attach: ${mediaData.id} (${mediaData.title}, ${mediaData.width}x${mediaData.height})") + } + } +} +``` Be aware that message `metadata` is not validated by the server. Always treat it as untrusted user input. @@ -178,6 +363,91 @@ const getValidMedia = (message) => { return []; }; ``` + +```swift +import Foundation + +// assume IDs are 10-15 characters long and alphanumeric +let mediaIdRegex = try! NSRegularExpression(pattern: "^[a-z0-9]{10,15}$") + +func getValidMedia(message: Message) -> [MediaData] { + guard case let .array(mediaArray) = message.metadata["media"] else { + return [] + } + + return mediaArray.compactMap { mediaValue -> MediaData? in + guard case let .object(mediaObj) = mediaValue, + case let .string(id) = mediaObj["id"], + case let .string(title) = mediaObj["title"], + case let .number(width) = mediaObj["width"], + case let .number(height) = mediaObj["height"] else { + return nil + } + + let range = NSRange(location: 0, length: id.utf16.count) + guard mediaIdRegex.firstMatch(in: id, options: [], range: range) != nil else { + return nil + } + + return MediaData(id: id, title: title, width: Int(width), height: Int(height)) + } +} +``` + +```kotlin +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +// assume IDs are 10-15 characters long and alphanumeric +val mediaIdRegex = Regex("^[a-z0-9]{10,15}$") + +fun getValidMedia(message: Message): List { + val mediaArray = message.metadata["media"]?.jsonArray ?: return emptyList() + + return mediaArray.mapNotNull { mediaValue -> + val mediaObj = mediaValue.jsonObject + val id = mediaObj["id"]?.jsonPrimitive?.content ?: return@mapNotNull null + val title = mediaObj["title"]?.jsonPrimitive?.content ?: return@mapNotNull null + val width = mediaObj["width"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null + val height = mediaObj["height"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null + + if (mediaIdRegex.matches(id)) { + MediaData(id, title, width, height) + } else { + null + } + } +} +``` + +```jetpack +import com.ably.chat.Message +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +// assume IDs are 10-15 characters long and alphanumeric +val mediaIdRegex = Regex("^[a-z0-9]{10,15}$") + +fun getValidMedia(message: Message): List { + val mediaArray = message.metadata["media"]?.jsonArray ?: return emptyList() + + return mediaArray.mapNotNull { mediaValue -> + val mediaObj = mediaValue.jsonObject + val id = mediaObj["id"]?.jsonPrimitive?.content ?: return@mapNotNull null + val title = mediaObj["title"]?.jsonPrimitive?.content ?: return@mapNotNull null + val width = mediaObj["width"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null + val height = mediaObj["height"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null + + if (mediaIdRegex.matches(id)) { + MediaData(id, title, width, height) + } else { + null + } + } +} +``` Use a function or component to display the message and its media: @@ -242,6 +512,115 @@ const MessageDisplay = ({ message }) => { ); }; ``` + +```swift +import UIKit + +func createMessageView(message: Message) -> UIView { + let container = UIStackView() + container.axis = .vertical + container.spacing = 8 + + let textLabel = UILabel() + textLabel.text = message.text + container.addArrangedSubview(textLabel) + + let validMedia = getValidMedia(message: message) + if !validMedia.isEmpty { + let mediaContainer = UIStackView() + mediaContainer.axis = .vertical + mediaContainer.spacing = 4 + + for media in validMedia { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + + if let url = URL(string: "https://example.com/images/\(media.id)") { + // Load image from URL (using URLSession or an image loading library) + // imageView.load(url: url) + } + + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: CGFloat(media.width)), + imageView.heightAnchor.constraint(equalToConstant: CGFloat(media.height)) + ]) + + mediaContainer.addArrangedSubview(imageView) + } + + container.addArrangedSubview(mediaContainer) + } + + return container +} +``` + +```kotlin +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView + +fun createMessageView(message: Message, context: android.content.Context): View { + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + } + + val textView = TextView(context).apply { + text = message.text + } + container.addView(textView) + + val validMedia = getValidMedia(message) + if (validMedia.isNotEmpty()) { + val mediaContainer = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + } + + validMedia.forEach { media -> + val imageView = ImageView(context).apply { + // Load image from URL (using Coil, Glide, or Picasso) + // load("https://example.com/images/${media.id}") + } + + mediaContainer.addView(imageView) + } + + container.addView(mediaContainer) + } + + return container +} +``` + +```jetpack +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import coil.compose.AsyncImage +import com.ably.chat.Message + +@Composable +fun MessageDisplayComponent(message: Message) { + val validMedia = getValidMedia(message) + + Column { + Text(text = message.text) + + if (validMedia.isNotEmpty()) { + Column { + validMedia.forEach { media -> + AsyncImage( + model = "https://example.com/images/${media.id}", + contentDescription = media.title, + ) + } + } + } + } +} +``` ### Add media to an existing message @@ -287,6 +666,118 @@ const AddMediaToMessage = ({ message }) => { ); }; ``` + +```swift +func addMediaToMessage(message: Message, mediaData: MediaData) async throws { + var newMetadata = message.metadata + + var mediaArray: [JSONValue] + if case let .array(existingArray) = newMetadata["media"] { + mediaArray = existingArray + } else { + mediaArray = [] + } + + mediaArray.append(.object([ + "id": .string(mediaData.id), + "title": .string(mediaData.title), + "width": .number(Double(mediaData.width)), + "height": .number(Double(mediaData.height)) + ])) + + newMetadata["media"] = .array(mediaArray) + + try await room.messages.update( + serial: message.serial, + params: .init( + text: message.text, + metadata: newMetadata + ) + ) +} +``` + +```kotlin +import kotlinx.serialization.json.* + +suspend fun addMediaToMessage(message: Message, mediaData: MediaData) { + val existingMedia = message.metadata["media"]?.jsonArray ?: buildJsonArray { } + + val newMediaArray = buildJsonArray { + existingMedia.forEach { add(it) } + add(buildJsonObject { + put("id", mediaData.id) + put("title", mediaData.title) + put("width", mediaData.width) + put("height", mediaData.height) + }) + } + + val newMetadata = buildJsonObject { + message.metadata.forEach { (key, value) -> + if (key != "media") { + put(key, value) + } + } + put("media", newMediaArray) + } + + room.messages.update( + serial = message.serial, + text = message.text, + metadata = newMetadata + ) +} +``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.coroutines.launch +import kotlinx.serialization.json.* + +@Composable +fun AddMediaToMessageComponent(room: Room, message: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + val mediaData = MediaData("abcd123abcd", "A beautiful image", 1024, 768) + + val existingMedia = message.metadata["media"]?.jsonArray ?: buildJsonArray { } + + val newMediaArray = buildJsonArray { + existingMedia.forEach { add(it) } + add(buildJsonObject { + put("id", mediaData.id) + put("title", mediaData.title) + put("width", mediaData.width) + put("height", mediaData.height) + }) + } + + val newMetadata = buildJsonObject { + message.metadata.forEach { (key, value) -> + if (key != "media") { + put(key, value) + } + } + put("media", newMediaArray) + } + + room.messages.update( + serial = message.serial, + text = message.text, + metadata = newMetadata + ) + } + }) { + Text("Add Media to Message") + } +} +``` ### Remove media from an existing message @@ -335,6 +826,125 @@ const RemoveMediaFromMessage = ({ message }) => { ); }; ``` + +```swift +func removeMediaFromMessage(message: Message, mediaIdToRemove: String) async throws { + guard case let .array(mediaArray) = message.metadata["media"], + !mediaArray.isEmpty else { + // do nothing if there is no media + return + } + + let newMediaArray = mediaArray.filter { mediaValue in + guard case let .object(mediaObj) = mediaValue, + case let .string(id) = mediaObj["id"] else { + return true + } + return id != mediaIdToRemove + } + + var newMetadata = message.metadata + newMetadata["media"] = .array(newMediaArray) + + try await room.messages.update( + serial: message.serial, + params: .init( + text: message.text, + metadata: newMetadata + ) + ) +} +``` + +```kotlin +import kotlinx.serialization.json.* + +suspend fun removeMediaFromMessage(message: Message, mediaIdToRemove: String) { + val existingMedia = message.metadata["media"]?.jsonArray + if (existingMedia == null || existingMedia.isEmpty()) { + // do nothing if there is no media + return + } + + val newMediaArray = buildJsonArray { + existingMedia.forEach { mediaValue -> + val mediaObj = mediaValue.jsonObject + val id = mediaObj["id"]?.jsonPrimitive?.content + if (id != mediaIdToRemove) { + add(mediaValue) + } + } + } + + val newMetadata = buildJsonObject { + message.metadata.forEach { (key, value) -> + if (key != "media") { + put(key, value) + } + } + put("media", newMediaArray) + } + + room.messages.update( + serial = message.serial, + text = message.text, + metadata = newMetadata + ) +} +``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.coroutines.launch +import kotlinx.serialization.json.* + +@Composable +fun RemoveMediaFromMessageComponent(room: Room, message: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + val mediaIdToRemove = "abcd123abcd" + + val existingMedia = message.metadata["media"]?.jsonArray + if (existingMedia == null || existingMedia.isEmpty()) { + // do nothing if there is no media + return@launch + } + + val newMediaArray = buildJsonArray { + existingMedia.forEach { mediaValue -> + val mediaObj = mediaValue.jsonObject + val id = mediaObj["id"]?.jsonPrimitive?.content + if (id != mediaIdToRemove) { + add(mediaValue) + } + } + } + + val newMetadata = buildJsonObject { + message.metadata.forEach { (key, value) -> + if (key != "media") { + put(key, value) + } + } + put("media", newMediaArray) + } + + room.messages.update( + serial = message.serial, + text = message.text, + metadata = newMetadata + ) + } + }) { + Text("Remove Media from Message") + } +} +``` ## Media moderation diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index 6424bb50fa..8975cbea0d 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -315,6 +315,42 @@ const MyComponent = () => { }; ``` +```swift +// Remove a 👍 reaction using the default type +await room.messages.reactions.delete(forMessageWithSerial: message.serial, params: .init(name: "👍")) + +// Remove a :love: reaction using the Unique type +await room.messages.reactions.delete(forMessageWithSerial: message.serial, params: .init( + name: ":love:", + type: .unique +)) + +// Remove a ❤️ reaction with count 50 using the Multiple type +await room.messages.reactions.delete(forMessageWithSerial: message.serial, params: .init( + name: "❤️", + type: .multiple, + count: 50 +)) +``` + +```kotlin +// Remove a 👍 reaction using the default type +room.messages.reactions.delete(message, name = "👍") + +// Remove a :love: reaction using the Unique type +room.messages.reactions.delete(message, + name = ":love:", + type = MessageReactionType.Unique, +) + +// Remove a ❤️ reaction with count 50 using the Multiple type +room.messages.reactions.delete(message, + name = "❤️", + type = MessageReactionType.Multiple, + count = 50, +) +``` + ```jetpack import androidx.compose.material.* import androidx.compose.runtime.* diff --git a/src/pages/docs/chat/rooms/replies.mdx b/src/pages/docs/chat/rooms/replies.mdx index ae5b54ab3f..89161cee7f 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -62,6 +62,77 @@ const ReplyComponent = ({ messageToReplyTo }) => { ); }; ``` + +```swift +func sendReply(replyToMessage: Message, replyText: String) async throws { + let metadata: MessageMetadata = [ + "reply": .object([ + "serial": .string(replyToMessage.serial), + "timestamp": .number(Double(replyToMessage.timestamp.timeIntervalSince1970 * 1000)), + "clientId": .string(replyToMessage.clientID), + "previewText": .string(String(replyToMessage.text.prefix(140))) + ]) + ] + + try await room.messages.send(params: .init( + text: replyText, + metadata: metadata + )) +} +``` + +```kotlin +suspend fun sendReply(replyToMessage: Message, replyText: String) { + val metadata = buildJsonObject { + put("reply", buildJsonObject { + put("serial", replyToMessage.serial) + put("timestamp", replyToMessage.timestamp) + put("clientId", replyToMessage.clientId) + put("previewText", replyToMessage.text.take(140)) + }) + } + + room.messages.send( + text = replyText, + metadata = metadata + ) +} +``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.coroutines.launch +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +@Composable +fun SendReplyComponent(room: Room, messageToReplyTo: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + val metadata = buildJsonObject { + put("reply", buildJsonObject { + put("serial", messageToReplyTo.serial) + put("timestamp", messageToReplyTo.timestamp) + put("clientId", messageToReplyTo.clientId) + put("previewText", messageToReplyTo.text.take(140)) + }) + } + + room.messages.send( + text = "My reply", + metadata = metadata + ) + } + }) { + Text("Send Reply") + } +} +``` ## Subscribe to message replies @@ -96,6 +167,39 @@ const prepareReply = (parentMessage) => { }; }; ``` + +```swift +func prepareReply(parentMessage: Message) -> JSONObject { + return [ + "serial": .string(parentMessage.serial), + "timestamp": .number(Double(parentMessage.timestamp.timeIntervalSince1970 * 1000)), + "clientId": .string(parentMessage.clientID), + "previewText": .string(String(parentMessage.text.prefix(140))) + ] +} +``` + +```kotlin +fun prepareReply(parentMessage: Message) = buildJsonObject { + put("serial", parentMessage.serial) + put("timestamp", parentMessage.timestamp) + put("clientId", parentMessage.clientId) + put("previewText", parentMessage.text.take(140)) +} +``` + +```jetpack +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import com.ably.chat.Message + +fun prepareReply(parentMessage: Message) = buildJsonObject { + put("serial", parentMessage.serial) + put("timestamp", parentMessage.timestamp) + put("clientId", parentMessage.clientId) + put("previewText", parentMessage.text.take(140)) +} +``` If a parent message isn't in local state, fetch it directly using its `serial`: @@ -126,6 +230,49 @@ const FetchParentMessage = ({ replyData }) => { ) : null; }; ``` + +```swift +func fetchParentMessage(replyData: JSONObject) async throws -> Message { + guard case let .string(serial) = replyData["serial"] else { + throw NSError(domain: "ReplyError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid serial"]) + } + let message = try await room.messages.get(serial: serial) + return message +} +``` + +```kotlin +suspend fun fetchParentMessage(replyData: JsonObject): Message { + val serial = replyData["serial"]?.jsonPrimitive?.content + ?: throw IllegalArgumentException("Invalid serial") + return room.messages.get(serial) +} +``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive + +@Composable +fun FetchParentMessageComponent(room: Room, replyData: JsonObject) { + var parentMessage by remember { mutableStateOf(null) } + + LaunchedEffect(replyData) { + val serial = replyData["serial"]?.jsonPrimitive?.content + if (serial != null) { + parentMessage = room.messages.get(serial) + } + } + + parentMessage?.let { message -> + Text(text = message.text) + } +} +``` ### Display replies @@ -187,6 +334,97 @@ const MessageList = () => { ); }; ``` + +```swift +// Subscribe to messages and handle replies +var localMessages: [Message] = [] + +room.messages.subscribe { event in + let message = event.message + + if let replyMetadata = message.metadata["reply"], + case let .object(replyData) = replyMetadata { + if case let .string(replySerial) = replyData["serial"] { + if let parentMessage = localMessages.first(where: { $0.serial == replySerial }) { + print("Reply to \(parentMessage.clientID): \(parentMessage.text)") + } else if case let .string(replyClientId) = replyData["clientId"], + case let .string(previewText) = replyData["previewText"] { + print("Reply to \(replyClientId): \(previewText)") + } + } + } + + print("Message: \(message.text)") + localMessages.append(message) +} +``` + +```kotlin +// Subscribe to messages and handle replies +val localMessages = mutableListOf() + +room.messages.subscribe { event -> + val message = event.message + + val replyData = message.metadata["reply"]?.jsonObject + if (replyData != null) { + val replySerial = replyData["serial"]?.jsonPrimitive?.content + val parentMessage = localMessages.find { it.serial == replySerial } + + if (parentMessage != null) { + println("Reply to ${parentMessage.clientId}: ${parentMessage.text}") + } else { + val replyClientId = replyData["clientId"]?.jsonPrimitive?.content + val previewText = replyData["previewText"]?.jsonPrimitive?.content + println("Reply to $replyClientId: $previewText") + } + } + + println("Message: ${message.text}") + localMessages.add(message) +} +``` + +```jetpack +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +@Composable +fun MessageListComponent(room: Room) { + var messages by remember { mutableStateOf>(emptyList()) } + + DisposableEffect(room) { + val (unsubscribe) = room.messages.subscribe { event -> + messages = messages + event.message + } + + onDispose { + unsubscribe() + } + } + + Column { + messages.forEach { message -> + Column { + // Display reply information if present + val replyData = message.metadata["reply"]?.jsonObject + if (replyData != null) { + val previewText = replyData["previewText"]?.jsonPrimitive?.content + Text(text = "Replying to: $previewText") + } + + // Display the message text + Text(text = message.text) + } + } + } +} +``` ## Considerations From 55e03e274cc74332385402953e237ffa8a5bd62a Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 14 Jan 2026 22:24:56 +0530 Subject: [PATCH 2/3] - Updated message-reactions, media and replies with correct swift/kotlin snippets - Added uploadMedia method to swift/kotlin and jetpack --- src/pages/docs/chat/rooms/media.mdx | 111 ++++++++++++++---- .../docs/chat/rooms/message-reactions.mdx | 33 +++--- src/pages/docs/chat/rooms/replies.mdx | 4 +- 3 files changed, 105 insertions(+), 43 deletions(-) diff --git a/src/pages/docs/chat/rooms/media.mdx b/src/pages/docs/chat/rooms/media.mdx index 4c009243e2..ca117e232e 100644 --- a/src/pages/docs/chat/rooms/media.mdx +++ b/src/pages/docs/chat/rooms/media.mdx @@ -43,6 +43,84 @@ async function uploadMedia() { return { id: mediaId, title, width, height }; } ``` + +```swift +struct MediaData { + let id: String + let title: String + let width: Int + let height: Int +} + +func uploadMedia() async -> MediaData { + // ask the user to choose their media + // upload the media to your storage service + // return a unique identifier for the media + + // mock implementation: + let mediaId = "abcd123abcd" + + // Some media metadata, useful for displaying the media in the UI + let title = "A beautiful image" + let width = 1024 + let height = 768 + + // Return the object + return MediaData(id: mediaId, title: title, width: width, height: height) +} +``` + +```kotlin +data class MediaData( + val id: String, + val title: String, + val width: Int, + val height: Int +) + +suspend fun uploadMedia(): MediaData { + // ask the user to choose their media + // upload the media to your storage service + // return a unique identifier for the media + + // mock implementation: + val mediaId = "abcd123abcd" + + // Some media metadata, useful for displaying the media in the UI + val title = "A beautiful image" + val width = 1024 + val height = 768 + + // Return the object + return MediaData(id = mediaId, title = title, width = width, height = height) +} +``` + +```jetpack +data class MediaData( + val id: String, + val title: String, + val width: Int, + val height: Int +) + +suspend fun uploadMedia(): MediaData { + // ask the user to choose their media + // upload the media to your storage service + // return a unique identifier for the media + + // mock implementation: + val mediaId = "abcd123abcd" + + // Some media metadata, useful for displaying the media in the UI + val title = "A beautiful image" + val width = 1024 + val height = 768 + + // Return the object + return MediaData(id = mediaId, title = title, width = width, height = height) +} +``` Use the `uploadMedia()` flow to save the resulting object. In your UI, the `mediaToAttach` array should be displayed so that users can see which which media will be attached to their message. It also enables users to add or remove selected media. @@ -79,12 +157,7 @@ const ChatComponent = () => { ``` ```swift -struct MediaData { - let id: String - let title: String - let width: Int - let height: Int -} +// MediaData struct is defined in the uploadMedia() snippet above class ChatViewController { var mediaToAttach: [MediaData] = [] @@ -97,12 +170,7 @@ class ChatViewController { ``` ```kotlin -data class MediaData( - val id: String, - val title: String, - val width: Int, - val height: Int -) +// MediaData data class is defined in the uploadMedia() snippet above var mediaToAttach = mutableListOf() @@ -118,12 +186,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import kotlinx.coroutines.launch -data class MediaData( - val id: String, - val title: String, - val width: Int, - val height: Int -) +// MediaData data class is defined in the uploadMedia() snippet above @Composable fun MediaAttachmentComponent() { @@ -227,7 +290,7 @@ func send(text: String, mediaToAttach: [MediaData]) async throws { metadata["media"] = .array(mediaArray) } - try await room.messages.send(params: .init( + try await room.messages.send(withParams: .init( text: text, metadata: metadata )) @@ -688,11 +751,12 @@ func addMediaToMessage(message: Message, mediaData: MediaData) async throws { newMetadata["media"] = .array(mediaArray) try await room.messages.update( - serial: message.serial, + withSerial: message.serial, params: .init( text: message.text, metadata: newMetadata - ) + ), + details: nil ) } ``` @@ -847,11 +911,12 @@ func removeMediaFromMessage(message: Message, mediaIdToRemove: String) async thr newMetadata["media"] = .array(newMediaArray) try await room.messages.update( - serial: message.serial, + withSerial: message.serial, params: .init( text: message.text, metadata: newMetadata - ) + ), + details: nil ) } ``` diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index 8975cbea0d..d5858e00be 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -120,21 +120,21 @@ await room.messages.reactions.send(message, { ```swift // Send a 👍 reaction using the default type -await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: "👍")) +try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: "👍")) // The reaction can be anything, not just UTF-8 emojis: -await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: ":like:")) -await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: "+1")) +try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: ":like:")) +try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init(name: "+1")) // Send a :love: reaction using the Unique type -await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init( - reaction: ":love:", +try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init( + name: ":love:", type: .unique )) // Send a ❤️ reaction with count 100 using the Multiple type -await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init( - reaction: "❤️", +try await room.messages.reactions.send(forMessageWithSerial: message.serial, params: .init( + name: "❤️", type: .multiple, count: 100 )) @@ -317,19 +317,18 @@ const MyComponent = () => { ```swift // Remove a 👍 reaction using the default type -await room.messages.reactions.delete(forMessageWithSerial: message.serial, params: .init(name: "👍")) +try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init(name: "👍")) // Remove a :love: reaction using the Unique type -await room.messages.reactions.delete(forMessageWithSerial: message.serial, params: .init( +try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init( name: ":love:", type: .unique )) -// Remove a ❤️ reaction with count 50 using the Multiple type -await room.messages.reactions.delete(forMessageWithSerial: message.serial, params: .init( +// Remove a ❤️ reaction using the Multiple type +try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init( name: "❤️", - type: .multiple, - count: 50 + type: .multiple )) ``` @@ -343,11 +342,10 @@ room.messages.reactions.delete(message, type = MessageReactionType.Unique, ) -// Remove a ❤️ reaction with count 50 using the Multiple type +// Remove a ❤️ reaction using the Multiple type room.messages.reactions.delete(message, name = "❤️", type = MessageReactionType.Multiple, - count = 50, ) ``` @@ -374,16 +372,15 @@ fun RemoveMessageReactionComponent(room: Room, message: Message) { Button(onClick = { coroutineScope.launch { - // Remove a ❤️ reaction with count 50 using the Multiple type + // Remove a ❤️ reaction using the Multiple type room.messages.reactions.delete( message, name = "❤️", type = MessageReactionType.Multiple, - count = 50, ) } }) { - Text("Remove ❤️ x50") + Text("Remove ❤️") } } ``` diff --git a/src/pages/docs/chat/rooms/replies.mdx b/src/pages/docs/chat/rooms/replies.mdx index 89161cee7f..2224e8d72a 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -74,7 +74,7 @@ func sendReply(replyToMessage: Message, replyText: String) async throws { ]) ] - try await room.messages.send(params: .init( + try await room.messages.send(withParams: .init( text: replyText, metadata: metadata )) @@ -236,7 +236,7 @@ func fetchParentMessage(replyData: JSONObject) async throws -> Message { guard case let .string(serial) = replyData["serial"] else { throw NSError(domain: "ReplyError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid serial"]) } - let message = try await room.messages.get(serial: serial) + let message = try await room.messages.get(withSerial: serial) return message } ``` From a6181a122a3a5fe72651a237c21a19abedb000f3 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 15 Jan 2026 19:05:12 +0530 Subject: [PATCH 3/3] - Simplified snippets media.mdx and replies.mdx as per recommended standards - Fixed swift snippet method signatuee for messages.history in message-reactions --- src/pages/docs/chat/rooms/media.mdx | 370 ++++++++---------- .../docs/chat/rooms/message-reactions.mdx | 2 +- src/pages/docs/chat/rooms/replies.mdx | 56 +-- 3 files changed, 193 insertions(+), 235 deletions(-) diff --git a/src/pages/docs/chat/rooms/media.mdx b/src/pages/docs/chat/rooms/media.mdx index ca117e232e..b521af0e91 100644 --- a/src/pages/docs/chat/rooms/media.mdx +++ b/src/pages/docs/chat/rooms/media.mdx @@ -45,14 +45,7 @@ async function uploadMedia() { ``` ```swift -struct MediaData { - let id: String - let title: String - let width: Int - let height: Int -} - -func uploadMedia() async -> MediaData { +func uploadMedia() async -> JSONObject { // ask the user to choose their media // upload the media to your storage service // return a unique identifier for the media @@ -66,19 +59,20 @@ func uploadMedia() async -> MediaData { let height = 768 // Return the object - return MediaData(id: mediaId, title: title, width: width, height: height) + return [ + "id": .string(mediaId), + "title": .string(title), + "width": .number(Double(width)), + "height": .number(Double(height)) + ] } ``` ```kotlin -data class MediaData( - val id: String, - val title: String, - val width: Int, - val height: Int -) - -suspend fun uploadMedia(): MediaData { +import com.ably.chat.json.JsonObject +import com.ably.chat.json.jsonObject + +suspend fun uploadMedia(): JsonObject { // ask the user to choose their media // upload the media to your storage service // return a unique identifier for the media @@ -92,19 +86,20 @@ suspend fun uploadMedia(): MediaData { val height = 768 // Return the object - return MediaData(id = mediaId, title = title, width = width, height = height) + return jsonObject { + put("id", mediaId) + put("title", title) + put("width", width) + put("height", height) + } } ``` ```jetpack -data class MediaData( - val id: String, - val title: String, - val width: Int, - val height: Int -) - -suspend fun uploadMedia(): MediaData { +import com.ably.chat.json.JsonObject +import com.ably.chat.json.jsonObject + +suspend fun uploadMedia(): JsonObject { // ask the user to choose their media // upload the media to your storage service // return a unique identifier for the media @@ -118,7 +113,12 @@ suspend fun uploadMedia(): MediaData { val height = 768 // Return the object - return MediaData(id = mediaId, title = title, width = width, height = height) + return jsonObject { + put("id", mediaId) + put("title", title) + put("width", width) + put("height", height) + } } ``` @@ -157,22 +157,18 @@ const ChatComponent = () => { ``` ```swift -// MediaData struct is defined in the uploadMedia() snippet above - -class ChatViewController { - var mediaToAttach: [MediaData] = [] +var mediaToAttach: [JSONObject] = [] - func onMediaAttach() async { - let mediaData = await uploadMedia() - mediaToAttach.append(mediaData) - } +func onMediaAttach() async { + let mediaData = await uploadMedia() + mediaToAttach.append(mediaData) } ``` ```kotlin -// MediaData data class is defined in the uploadMedia() snippet above +import com.ably.chat.json.JsonObject -var mediaToAttach = mutableListOf() +var mediaToAttach = mutableListOf() suspend fun onMediaAttach() { val mediaData = uploadMedia() @@ -184,13 +180,14 @@ suspend fun onMediaAttach() { import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString +import com.ably.chat.json.JsonNumber import kotlinx.coroutines.launch -// MediaData data class is defined in the uploadMedia() snippet above - @Composable fun MediaAttachmentComponent() { - var mediaToAttach by remember { mutableStateOf>(emptyList()) } + var mediaToAttach by remember { mutableStateOf>(emptyList()) } val coroutineScope = rememberCoroutineScope() Column { @@ -203,8 +200,12 @@ fun MediaAttachmentComponent() { Text("Attach Media") } - mediaToAttach.forEach { mediaData -> - Text("Media to attach: ${mediaData.id} (${mediaData.title}, ${mediaData.width}x${mediaData.height})") + mediaToAttach.forEach { media -> + val id = (media["id"] as? JsonString)?.value ?: "" + val title = (media["title"] as? JsonString)?.value ?: "" + val width = (media["width"] as? JsonNumber)?.value?.toInt() ?: 0 + val height = (media["height"] as? JsonNumber)?.value?.toInt() ?: 0 + Text("Media to attach: $id ($title, ${width}x${height})") } } } @@ -276,18 +277,10 @@ const MessageSender = () => { ``` ```swift -func send(text: String, mediaToAttach: [MediaData]) async throws { +func send(text: String, mediaToAttach: [JSONObject]) async throws { var metadata: MessageMetadata = [:] if !mediaToAttach.isEmpty { - let mediaArray: [JSONValue] = mediaToAttach.map { media in - .object([ - "id": .string(media.id), - "title": .string(media.title), - "width": .number(Double(media.width)), - "height": .number(Double(media.height)) - ]) - } - metadata["media"] = .array(mediaArray) + metadata["media"] = .array(mediaToAttach.map { .object($0) }) } try await room.messages.send(withParams: .init( @@ -298,19 +291,14 @@ func send(text: String, mediaToAttach: [MediaData]) async throws { ``` ```kotlin -suspend fun send(text: String, mediaToAttach: List) { +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.jsonObject + +suspend fun send(text: String, mediaToAttach: List) { val metadata = if (mediaToAttach.isNotEmpty()) { - buildJsonObject { - put("media", buildJsonArray { - mediaToAttach.forEach { media -> - add(buildJsonObject { - put("id", media.id) - put("title", media.title) - put("width", media.width) - put("height", media.height) - }) - } - }) + jsonObject { + put("media", JsonArray(mediaToAttach)) } } else { null @@ -328,14 +316,16 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import com.ably.chat.Room +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.JsonString +import com.ably.chat.json.JsonNumber +import com.ably.chat.json.jsonObject import kotlinx.coroutines.launch -import kotlinx.serialization.json.buildJsonArray -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put @Composable fun MessageSenderComponent(room: Room) { - var mediaToAttach by remember { mutableStateOf>(emptyList()) } + var mediaToAttach by remember { mutableStateOf>(emptyList()) } var messageText by remember { mutableStateOf("") } val coroutineScope = rememberCoroutineScope() @@ -358,17 +348,8 @@ fun MessageSenderComponent(room: Room) { Button(onClick = { coroutineScope.launch { val metadata = if (mediaToAttach.isNotEmpty()) { - buildJsonObject { - put("media", buildJsonArray { - mediaToAttach.forEach { media -> - add(buildJsonObject { - put("id", media.id) - put("title", media.title) - put("width", media.width) - put("height", media.height) - }) - } - }) + jsonObject { + put("media", JsonArray(mediaToAttach)) } } else null @@ -384,8 +365,12 @@ fun MessageSenderComponent(room: Room) { Text("Send") } - mediaToAttach.forEach { mediaData -> - Text("Media to attach: ${mediaData.id} (${mediaData.title}, ${mediaData.width}x${mediaData.height})") + mediaToAttach.forEach { media -> + val id = (media["id"] as? JsonString)?.value ?: "" + val title = (media["title"] as? JsonString)?.value ?: "" + val width = (media["width"] as? JsonNumber)?.value?.toInt() ?: 0 + val height = (media["height"] as? JsonNumber)?.value?.toInt() ?: 0 + Text("Media to attach: $id ($title, ${width}x${height})") } } } @@ -433,17 +418,14 @@ import Foundation // assume IDs are 10-15 characters long and alphanumeric let mediaIdRegex = try! NSRegularExpression(pattern: "^[a-z0-9]{10,15}$") -func getValidMedia(message: Message) -> [MediaData] { +func getValidMedia(message: Message) -> [JSONObject] { guard case let .array(mediaArray) = message.metadata["media"] else { return [] } - return mediaArray.compactMap { mediaValue -> MediaData? in + return mediaArray.compactMap { mediaValue -> JSONObject? in guard case let .object(mediaObj) = mediaValue, - case let .string(id) = mediaObj["id"], - case let .string(title) = mediaObj["title"], - case let .number(width) = mediaObj["width"], - case let .number(height) = mediaObj["height"] else { + case let .string(id) = mediaObj["id"] else { return nil } @@ -452,31 +434,28 @@ func getValidMedia(message: Message) -> [MediaData] { return nil } - return MediaData(id: id, title: title, width: Int(width), height: Int(height)) + return mediaObj } } ``` ```kotlin -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.JsonString // assume IDs are 10-15 characters long and alphanumeric val mediaIdRegex = Regex("^[a-z0-9]{10,15}$") -fun getValidMedia(message: Message): List { - val mediaArray = message.metadata["media"]?.jsonArray ?: return emptyList() +fun getValidMedia(message: Message): List { + val mediaArray = message.metadata["media"] as? JsonArray ?: return emptyList() return mediaArray.mapNotNull { mediaValue -> - val mediaObj = mediaValue.jsonObject - val id = mediaObj["id"]?.jsonPrimitive?.content ?: return@mapNotNull null - val title = mediaObj["title"]?.jsonPrimitive?.content ?: return@mapNotNull null - val width = mediaObj["width"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null - val height = mediaObj["height"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null + val mediaObj = mediaValue as? JsonObject ?: return@mapNotNull null + val id = (mediaObj["id"] as? JsonString)?.value ?: return@mapNotNull null if (mediaIdRegex.matches(id)) { - MediaData(id, title, width, height) + mediaObj } else { null } @@ -486,25 +465,22 @@ fun getValidMedia(message: Message): List { ```jetpack import com.ably.chat.Message -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.JsonString // assume IDs are 10-15 characters long and alphanumeric val mediaIdRegex = Regex("^[a-z0-9]{10,15}$") -fun getValidMedia(message: Message): List { - val mediaArray = message.metadata["media"]?.jsonArray ?: return emptyList() +fun getValidMedia(message: Message): List { + val mediaArray = message.metadata["media"] as? JsonArray ?: return emptyList() return mediaArray.mapNotNull { mediaValue -> - val mediaObj = mediaValue.jsonObject - val id = mediaObj["id"]?.jsonPrimitive?.content ?: return@mapNotNull null - val title = mediaObj["title"]?.jsonPrimitive?.content ?: return@mapNotNull null - val width = mediaObj["width"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null - val height = mediaObj["height"]?.jsonPrimitive?.content?.toIntOrNull() ?: return@mapNotNull null + val mediaObj = mediaValue as? JsonObject ?: return@mapNotNull null + val id = (mediaObj["id"] as? JsonString)?.value ?: return@mapNotNull null if (mediaIdRegex.matches(id)) { - MediaData(id, title, width, height) + mediaObj } else { null } @@ -577,45 +553,34 @@ const MessageDisplay = ({ message }) => { ``` ```swift -import UIKit - -func createMessageView(message: Message) -> UIView { - let container = UIStackView() - container.axis = .vertical - container.spacing = 8 - - let textLabel = UILabel() - textLabel.text = message.text - container.addArrangedSubview(textLabel) - - let validMedia = getValidMedia(message: message) - if !validMedia.isEmpty { - let mediaContainer = UIStackView() - mediaContainer.axis = .vertical - mediaContainer.spacing = 4 - - for media in validMedia { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - - if let url = URL(string: "https://example.com/images/\(media.id)") { - // Load image from URL (using URLSession or an image loading library) - // imageView.load(url: url) +import SwiftUI + +struct MessageView: View { + let message: Message + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(message.text) + + let validMedia = getValidMedia(message: message) + if !validMedia.isEmpty { + VStack(spacing: 4) { + ForEach(validMedia.indices, id: \.self) { index in + let media = validMedia[index] + if case let .string(id) = media["id"], + case let .string(title) = media["title"] { + AsyncImage(url: URL(string: "https://example.com/images/\(id)")) { image in + image.resizable().aspectRatio(contentMode: .fit) + } placeholder: { + ProgressView() + } + .accessibilityLabel(title) + } + } + } } - - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: CGFloat(media.width)), - imageView.heightAnchor.constraint(equalToConstant: CGFloat(media.height)) - ]) - - mediaContainer.addArrangedSubview(imageView) } - - container.addArrangedSubview(mediaContainer) } - - return container } ``` @@ -624,6 +589,7 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import com.ably.chat.json.JsonString fun createMessageView(message: Message, context: android.content.Context): View { val container = LinearLayout(context).apply { @@ -642,9 +608,10 @@ fun createMessageView(message: Message, context: android.content.Context): View } validMedia.forEach { media -> + val id = (media["id"] as? JsonString)?.value ?: "" val imageView = ImageView(context).apply { // Load image from URL (using Coil, Glide, or Picasso) - // load("https://example.com/images/${media.id}") + // load("https://example.com/images/$id") } mediaContainer.addView(imageView) @@ -663,6 +630,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import coil.compose.AsyncImage import com.ably.chat.Message +import com.ably.chat.json.JsonString @Composable fun MessageDisplayComponent(message: Message) { @@ -674,9 +642,11 @@ fun MessageDisplayComponent(message: Message) { if (validMedia.isNotEmpty()) { Column { validMedia.forEach { media -> + val id = (media["id"] as? JsonString)?.value ?: "" + val title = (media["title"] as? JsonString)?.value ?: "" AsyncImage( - model = "https://example.com/images/${media.id}", - contentDescription = media.title, + model = "https://example.com/images/$id", + contentDescription = title, ) } } @@ -731,7 +701,7 @@ const AddMediaToMessage = ({ message }) => { ``` ```swift -func addMediaToMessage(message: Message, mediaData: MediaData) async throws { +func addMediaToMessage(message: Message, mediaData: JSONObject) async throws { var newMetadata = message.metadata var mediaArray: [JSONValue] @@ -741,12 +711,7 @@ func addMediaToMessage(message: Message, mediaData: MediaData) async throws { mediaArray = [] } - mediaArray.append(.object([ - "id": .string(mediaData.id), - "title": .string(mediaData.title), - "width": .number(Double(mediaData.width)), - "height": .number(Double(mediaData.height)) - ])) + mediaArray.append(.object(mediaData)) newMetadata["media"] = .array(mediaArray) @@ -762,22 +727,16 @@ func addMediaToMessage(message: Message, mediaData: MediaData) async throws { ``` ```kotlin -import kotlinx.serialization.json.* - -suspend fun addMediaToMessage(message: Message, mediaData: MediaData) { - val existingMedia = message.metadata["media"]?.jsonArray ?: buildJsonArray { } - - val newMediaArray = buildJsonArray { - existingMedia.forEach { add(it) } - add(buildJsonObject { - put("id", mediaData.id) - put("title", mediaData.title) - put("width", mediaData.width) - put("height", mediaData.height) - }) - } +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.jsonObject + +suspend fun addMediaToMessage(message: Message, mediaData: JsonObject) { + val existingMedia = message.metadata["media"] as? JsonArray ?: JsonArray(emptyList()) - val newMetadata = buildJsonObject { + val newMediaArray = JsonArray(existingMedia + mediaData) + + val newMetadata = jsonObject { message.metadata.forEach { (key, value) -> if (key != "media") { put(key, value) @@ -799,8 +758,9 @@ import androidx.compose.material.* import androidx.compose.runtime.* import com.ably.chat.Message import com.ably.chat.Room +import com.ably.chat.json.JsonArray +import com.ably.chat.json.jsonObject import kotlinx.coroutines.launch -import kotlinx.serialization.json.* @Composable fun AddMediaToMessageComponent(room: Room, message: Message) { @@ -808,21 +768,13 @@ fun AddMediaToMessageComponent(room: Room, message: Message) { Button(onClick = { coroutineScope.launch { - val mediaData = MediaData("abcd123abcd", "A beautiful image", 1024, 768) - - val existingMedia = message.metadata["media"]?.jsonArray ?: buildJsonArray { } - - val newMediaArray = buildJsonArray { - existingMedia.forEach { add(it) } - add(buildJsonObject { - put("id", mediaData.id) - put("title", mediaData.title) - put("width", mediaData.width) - put("height", mediaData.height) - }) - } + val mediaData = uploadMedia() - val newMetadata = buildJsonObject { + val existingMedia = message.metadata["media"] as? JsonArray ?: JsonArray(emptyList()) + + val newMediaArray = JsonArray(existingMedia + mediaData) + + val newMetadata = jsonObject { message.metadata.forEach { (key, value) -> if (key != "media") { put(key, value) @@ -922,26 +874,25 @@ func removeMediaFromMessage(message: Message, mediaIdToRemove: String) async thr ``` ```kotlin -import kotlinx.serialization.json.* +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.JsonString +import com.ably.chat.json.jsonObject suspend fun removeMediaFromMessage(message: Message, mediaIdToRemove: String) { - val existingMedia = message.metadata["media"]?.jsonArray + val existingMedia = message.metadata["media"] as? JsonArray if (existingMedia == null || existingMedia.isEmpty()) { // do nothing if there is no media return } - val newMediaArray = buildJsonArray { - existingMedia.forEach { mediaValue -> - val mediaObj = mediaValue.jsonObject - val id = mediaObj["id"]?.jsonPrimitive?.content - if (id != mediaIdToRemove) { - add(mediaValue) - } - } - } + val newMediaArray = JsonArray(existingMedia.filter { mediaValue -> + val mediaObj = mediaValue as? JsonObject + val id = (mediaObj?.get("id") as? JsonString)?.value + id != mediaIdToRemove + }) - val newMetadata = buildJsonObject { + val newMetadata = jsonObject { message.metadata.forEach { (key, value) -> if (key != "media") { put(key, value) @@ -963,8 +914,11 @@ import androidx.compose.material.* import androidx.compose.runtime.* import com.ably.chat.Message import com.ably.chat.Room +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonArray +import com.ably.chat.json.JsonString +import com.ably.chat.json.jsonObject import kotlinx.coroutines.launch -import kotlinx.serialization.json.* @Composable fun RemoveMediaFromMessageComponent(room: Room, message: Message) { @@ -974,23 +928,19 @@ fun RemoveMediaFromMessageComponent(room: Room, message: Message) { coroutineScope.launch { val mediaIdToRemove = "abcd123abcd" - val existingMedia = message.metadata["media"]?.jsonArray + val existingMedia = message.metadata["media"] as? JsonArray if (existingMedia == null || existingMedia.isEmpty()) { // do nothing if there is no media return@launch } - val newMediaArray = buildJsonArray { - existingMedia.forEach { mediaValue -> - val mediaObj = mediaValue.jsonObject - val id = mediaObj["id"]?.jsonPrimitive?.content - if (id != mediaIdToRemove) { - add(mediaValue) - } - } - } + val newMediaArray = JsonArray(existingMedia.filter { mediaValue -> + val mediaObj = mediaValue as? JsonObject + val id = (mediaObj?.get("id") as? JsonString)?.value + id != mediaIdToRemove + }) - val newMetadata = buildJsonObject { + val newMetadata = jsonObject { message.metadata.forEach { (key, value) -> if (key != "media") { put(key, value) diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index 250f8188a0..bf46139751 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -648,7 +648,7 @@ room.messages.reactions.subscribe((event) => { ```swift // init messages, in practice this should be updated with a message subscription -var messages = (await room.messages.history(withOptions: .init(limit: 50))).items +var messages = (await room.messages.history(withParams: .init(limit: 50))).items // subscribe to message reactions summary events room.messages.reactions.subscribe { event in diff --git a/src/pages/docs/chat/rooms/replies.mdx b/src/pages/docs/chat/rooms/replies.mdx index 2224e8d72a..577fe2dd98 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -82,14 +82,16 @@ func sendReply(replyToMessage: Message, replyText: String) async throws { ``` ```kotlin +import com.ably.chat.json.jsonObject + suspend fun sendReply(replyToMessage: Message, replyText: String) { - val metadata = buildJsonObject { - put("reply", buildJsonObject { + val metadata = jsonObject { + putObject("reply") { put("serial", replyToMessage.serial) put("timestamp", replyToMessage.timestamp) put("clientId", replyToMessage.clientId) put("previewText", replyToMessage.text.take(140)) - }) + } } room.messages.send( @@ -104,9 +106,8 @@ import androidx.compose.material.* import androidx.compose.runtime.* import com.ably.chat.Message import com.ably.chat.Room +import com.ably.chat.json.jsonObject import kotlinx.coroutines.launch -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put @Composable fun SendReplyComponent(room: Room, messageToReplyTo: Message) { @@ -114,13 +115,13 @@ fun SendReplyComponent(room: Room, messageToReplyTo: Message) { Button(onClick = { coroutineScope.launch { - val metadata = buildJsonObject { - put("reply", buildJsonObject { + val metadata = jsonObject { + putObject("reply") { put("serial", messageToReplyTo.serial) put("timestamp", messageToReplyTo.timestamp) put("clientId", messageToReplyTo.clientId) put("previewText", messageToReplyTo.text.take(140)) - }) + } } room.messages.send( @@ -180,7 +181,9 @@ func prepareReply(parentMessage: Message) -> JSONObject { ``` ```kotlin -fun prepareReply(parentMessage: Message) = buildJsonObject { +import com.ably.chat.json.jsonObject + +fun prepareReply(parentMessage: Message) = jsonObject { put("serial", parentMessage.serial) put("timestamp", parentMessage.timestamp) put("clientId", parentMessage.clientId) @@ -189,11 +192,10 @@ fun prepareReply(parentMessage: Message) = buildJsonObject { ``` ```jetpack -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put import com.ably.chat.Message +import com.ably.chat.json.jsonObject -fun prepareReply(parentMessage: Message) = buildJsonObject { +fun prepareReply(parentMessage: Message) = jsonObject { put("serial", parentMessage.serial) put("timestamp", parentMessage.timestamp) put("clientId", parentMessage.clientId) @@ -242,8 +244,11 @@ func fetchParentMessage(replyData: JSONObject) async throws -> Message { ``` ```kotlin +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString + suspend fun fetchParentMessage(replyData: JsonObject): Message { - val serial = replyData["serial"]?.jsonPrimitive?.content + val serial = (replyData["serial"] as? JsonString)?.value ?: throw IllegalArgumentException("Invalid serial") return room.messages.get(serial) } @@ -254,15 +259,15 @@ import androidx.compose.material.* import androidx.compose.runtime.* import com.ably.chat.Message import com.ably.chat.Room -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonPrimitive +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString @Composable fun FetchParentMessageComponent(room: Room, replyData: JsonObject) { var parentMessage by remember { mutableStateOf(null) } LaunchedEffect(replyData) { - val serial = replyData["serial"]?.jsonPrimitive?.content + val serial = (replyData["serial"] as? JsonString)?.value if (serial != null) { parentMessage = room.messages.get(serial) } @@ -360,22 +365,25 @@ room.messages.subscribe { event in ``` ```kotlin +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString + // Subscribe to messages and handle replies val localMessages = mutableListOf() room.messages.subscribe { event -> val message = event.message - val replyData = message.metadata["reply"]?.jsonObject + val replyData = message.metadata["reply"] as? JsonObject if (replyData != null) { - val replySerial = replyData["serial"]?.jsonPrimitive?.content + val replySerial = (replyData["serial"] as? JsonString)?.value val parentMessage = localMessages.find { it.serial == replySerial } if (parentMessage != null) { println("Reply to ${parentMessage.clientId}: ${parentMessage.text}") } else { - val replyClientId = replyData["clientId"]?.jsonPrimitive?.content - val previewText = replyData["previewText"]?.jsonPrimitive?.content + val replyClientId = (replyData["clientId"] as? JsonString)?.value + val previewText = (replyData["previewText"] as? JsonString)?.value println("Reply to $replyClientId: $previewText") } } @@ -391,8 +399,8 @@ import androidx.compose.material.* import androidx.compose.runtime.* import com.ably.chat.Message import com.ably.chat.Room -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString @Composable fun MessageListComponent(room: Room) { @@ -412,9 +420,9 @@ fun MessageListComponent(room: Room) { messages.forEach { message -> Column { // Display reply information if present - val replyData = message.metadata["reply"]?.jsonObject + val replyData = message.metadata["reply"] as? JsonObject if (replyData != null) { - val previewText = replyData["previewText"]?.jsonPrimitive?.content + val previewText = (replyData["previewText"] as? JsonString)?.value Text(text = "Replying to: $previewText") }