diff --git a/src/pages/docs/chat/rooms/history.mdx b/src/pages/docs/chat/rooms/history.mdx index 61bf10377f..733811b69f 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..b521af0e91 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 +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 + + // 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 [ + "id": .string(mediaId), + "title": .string(title), + "width": .number(Double(width)), + "height": .number(Double(height)) + ] +} +``` + +```kotlin +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 + + // 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 jsonObject { + put("id", mediaId) + put("title", title) + put("width", width) + put("height", height) + } +} +``` + +```jetpack +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 + + // 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 jsonObject { + put("id", mediaId) + put("title", title) + put("width", width) + put("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. @@ -77,6 +155,61 @@ const ChatComponent = () => { ); }; ``` + +```swift +var mediaToAttach: [JSONObject] = [] + +func onMediaAttach() async { + let mediaData = await uploadMedia() + mediaToAttach.append(mediaData) +} +``` + +```kotlin +import com.ably.chat.json.JsonObject + +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 com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString +import com.ably.chat.json.JsonNumber +import kotlinx.coroutines.launch + +@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 { 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})") + } + } +} +``` ## Send a message @@ -142,6 +275,106 @@ const MessageSender = () => { ); }; ``` + +```swift +func send(text: String, mediaToAttach: [JSONObject]) async throws { + var metadata: MessageMetadata = [:] + if !mediaToAttach.isEmpty { + metadata["media"] = .array(mediaToAttach.map { .object($0) }) + } + + try await room.messages.send(withParams: .init( + text: text, + metadata: metadata + )) +} +``` + +```kotlin +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()) { + jsonObject { + put("media", JsonArray(mediaToAttach)) + } + } 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 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 + +@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()) { + jsonObject { + put("media", JsonArray(mediaToAttach)) + } + } else null + + room.messages.send( + text = messageText, + metadata = metadata + ) + + mediaToAttach = emptyList() + messageText = "" + } + }) { + Text("Send") + } + + 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})") + } + } +} +``` Be aware that message `metadata` is not validated by the server. Always treat it as untrusted user input. @@ -178,6 +411,82 @@ 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) -> [JSONObject] { + guard case let .array(mediaArray) = message.metadata["media"] else { + return [] + } + + return mediaArray.compactMap { mediaValue -> JSONObject? in + guard case let .object(mediaObj) = mediaValue, + case let .string(id) = mediaObj["id"] 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 mediaObj + } +} +``` + +```kotlin +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"] as? JsonArray ?: return emptyList() + + return mediaArray.mapNotNull { mediaValue -> + val mediaObj = mediaValue as? JsonObject ?: return@mapNotNull null + val id = (mediaObj["id"] as? JsonString)?.value ?: return@mapNotNull null + + if (mediaIdRegex.matches(id)) { + mediaObj + } else { + null + } + } +} +``` + +```jetpack +import com.ably.chat.Message +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"] as? JsonArray ?: return emptyList() + + return mediaArray.mapNotNull { mediaValue -> + val mediaObj = mediaValue as? JsonObject ?: return@mapNotNull null + val id = (mediaObj["id"] as? JsonString)?.value ?: return@mapNotNull null + + if (mediaIdRegex.matches(id)) { + mediaObj + } else { + null + } + } +} +``` Use a function or component to display the message and its media: @@ -242,6 +551,109 @@ const MessageDisplay = ({ message }) => { ); }; ``` + +```swift +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) + } + } + } + } + } + } +} +``` + +```kotlin +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 { + 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 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/$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 +import com.ably.chat.json.JsonString + +@Composable +fun MessageDisplayComponent(message: Message) { + val validMedia = getValidMedia(message) + + Column { + Text(text = message.text) + + 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/$id", + contentDescription = title, + ) + } + } + } + } +} +``` ### Add media to an existing message @@ -287,6 +699,101 @@ const AddMediaToMessage = ({ message }) => { ); }; ``` + +```swift +func addMediaToMessage(message: Message, mediaData: JSONObject) async throws { + var newMetadata = message.metadata + + var mediaArray: [JSONValue] + if case let .array(existingArray) = newMetadata["media"] { + mediaArray = existingArray + } else { + mediaArray = [] + } + + mediaArray.append(.object(mediaData)) + + newMetadata["media"] = .array(mediaArray) + + try await room.messages.update( + withSerial: message.serial, + params: .init( + text: message.text, + metadata: newMetadata + ), + details: nil + ) +} +``` + +```kotlin +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 newMediaArray = JsonArray(existingMedia + mediaData) + + val newMetadata = jsonObject { + 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 com.ably.chat.json.JsonArray +import com.ably.chat.json.jsonObject +import kotlinx.coroutines.launch + +@Composable +fun AddMediaToMessageComponent(room: Room, message: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + val mediaData = uploadMedia() + + 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) + } + } + 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 +842,124 @@ 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( + withSerial: message.serial, + params: .init( + text: message.text, + metadata: newMetadata + ), + details: nil + ) +} +``` + +```kotlin +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"] as? JsonArray + if (existingMedia == null || existingMedia.isEmpty()) { + // do nothing if there is no media + return + } + + val newMediaArray = JsonArray(existingMedia.filter { mediaValue -> + val mediaObj = mediaValue as? JsonObject + val id = (mediaObj?.get("id") as? JsonString)?.value + id != mediaIdToRemove + }) + + val newMetadata = jsonObject { + 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 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 + +@Composable +fun RemoveMediaFromMessageComponent(room: Room, message: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + val mediaIdToRemove = "abcd123abcd" + + val existingMedia = message.metadata["media"] as? JsonArray + if (existingMedia == null || existingMedia.isEmpty()) { + // do nothing if there is no media + return@launch + } + + val newMediaArray = JsonArray(existingMedia.filter { mediaValue -> + val mediaObj = mediaValue as? JsonObject + val id = (mediaObj?.get("id") as? JsonString)?.value + id != mediaIdToRemove + }) + + val newMetadata = jsonObject { + 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 1ab3277527..bf46139751 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -128,21 +128,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 )) @@ -323,6 +323,40 @@ const MyComponent = () => { }; ``` +```swift +// Remove a 👍 reaction using the default type +try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init(name: "👍")) + +// Remove a :love: reaction using the Unique type +try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init( + name: ":love:", + type: .unique +)) + +// Remove a ❤️ reaction using the Multiple type +try await room.messages.reactions.delete(fromMessageWithSerial: message.serial, params: .init( + name: "❤️", + type: .multiple +)) +``` + +```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 using the Multiple type +room.messages.reactions.delete(message, + name = "❤️", + type = MessageReactionType.Multiple, +) +``` + ```jetpack import androidx.compose.material.* import androidx.compose.runtime.* @@ -346,16 +380,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 ❤️") } } ``` @@ -615,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 ae5b54ab3f..577fe2dd98 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -62,6 +62,78 @@ 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(withParams: .init( + text: replyText, + metadata: metadata + )) +} +``` + +```kotlin +import com.ably.chat.json.jsonObject + +suspend fun sendReply(replyToMessage: Message, replyText: String) { + 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( + text = replyText, + metadata = metadata + ) +} +``` + +```jetpack +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 + +@Composable +fun SendReplyComponent(room: Room, messageToReplyTo: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + 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( + text = "My reply", + metadata = metadata + ) + } + }) { + Text("Send Reply") + } +} +``` ## Subscribe to message replies @@ -96,6 +168,40 @@ 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 +import com.ably.chat.json.jsonObject + +fun prepareReply(parentMessage: Message) = jsonObject { + put("serial", parentMessage.serial) + put("timestamp", parentMessage.timestamp) + put("clientId", parentMessage.clientId) + put("previewText", parentMessage.text.take(140)) +} +``` + +```jetpack +import com.ably.chat.Message +import com.ably.chat.json.jsonObject + +fun prepareReply(parentMessage: Message) = jsonObject { + 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 +232,52 @@ 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(withSerial: serial) + return message +} +``` + +```kotlin +import com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString + +suspend fun fetchParentMessage(replyData: JsonObject): Message { + val serial = (replyData["serial"] as? JsonString)?.value + ?: 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 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"] as? JsonString)?.value + if (serial != null) { + parentMessage = room.messages.get(serial) + } + } + + parentMessage?.let { message -> + Text(text = message.text) + } +} +``` ### Display replies @@ -187,6 +339,100 @@ 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 +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"] as? JsonObject + if (replyData != null) { + 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"] as? JsonString)?.value + val previewText = (replyData["previewText"] as? JsonString)?.value + 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 com.ably.chat.json.JsonObject +import com.ably.chat.json.JsonString + +@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"] as? JsonObject + if (replyData != null) { + val previewText = (replyData["previewText"] as? JsonString)?.value + Text(text = "Replying to: $previewText") + } + + // Display the message text + Text(text = message.text) + } + } + } +} +``` ## Considerations