From 291d6e5411af5425db9efd393b9473d92fd82b1e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 14 Jan 2026 13:12:17 +0530 Subject: [PATCH 01/11] Added documentation with code snippets for chat-kotlin jetpack annotations --- src/data/languages/languageData.ts | 3 +- src/data/languages/languageInfo.ts | 5 + src/data/languages/types.ts | 1 + src/pages/docs/chat/connect.mdx | 57 ++++ .../docs/chat/getting-started/android.mdx | 45 ++-- src/pages/docs/chat/rooms/history.mdx | 80 +++++- src/pages/docs/chat/rooms/index.mdx | 92 ++++++- .../docs/chat/rooms/message-reactions.mdx | 245 +++++++++++++++++- src/pages/docs/chat/rooms/messages.mdx | 190 ++++++++++++-- src/pages/docs/chat/rooms/occupancy.mdx | 71 ++++- src/pages/docs/chat/rooms/presence.mdx | 159 +++++++++++- src/pages/docs/chat/rooms/reactions.mdx | 76 +++++- src/pages/docs/chat/rooms/typing.mdx | 105 +++++++- src/pages/docs/chat/setup.mdx | 48 +++- 14 files changed, 1069 insertions(+), 108 deletions(-) diff --git a/src/data/languages/languageData.ts b/src/data/languages/languageData.ts index f5c1120992..baba22da4e 100644 --- a/src/data/languages/languageData.ts +++ b/src/data/languages/languageData.ts @@ -39,7 +39,8 @@ export default { javascript: '1.1', react: '1.1', swift: '1.0', - kotlin: '1.0', + kotlin: '1.1', + jetpack: '1.1', }, spaces: { javascript: '0.4', diff --git a/src/data/languages/languageInfo.ts b/src/data/languages/languageInfo.ts index f78ff81a2e..c1ff9c00c6 100644 --- a/src/data/languages/languageInfo.ts +++ b/src/data/languages/languageInfo.ts @@ -86,6 +86,11 @@ export default { label: 'Kotlin', syntaxHighlighterKey: 'kotlin', }, + jetpack: { + label: 'Jetpack Compose', + syntaxHighlighterKey: 'kotlin', + alias: 'kotlin', + }, realtime: { label: 'Realtime', syntaxHighlighterKey: 'javascript', diff --git a/src/data/languages/types.ts b/src/data/languages/types.ts index 879ac741c6..e975fe3265 100644 --- a/src/data/languages/types.ts +++ b/src/data/languages/types.ts @@ -26,6 +26,7 @@ export const languageKeys = [ 'css', 'laravel', 'typescript', + 'jetpack', ] as const; export type LanguageKey = (typeof languageKeys)[number]; diff --git a/src/pages/docs/chat/connect.mdx b/src/pages/docs/chat/connect.mdx index 14739608ce..49d0632d35 100644 --- a/src/pages/docs/chat/connect.mdx +++ b/src/pages/docs/chat/connect.mdx @@ -28,6 +28,10 @@ Use the [`status`](https://sdk.ably.com/builds/ably/ably-c Use the [`currentStatus`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-react.UseChatConnectionResponse.html#currentStatus) property returned in the response of the [`useChatConnection`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useChatConnection.html) hook to check which status a connection is currently in: + +Use the [`collectAsStatus()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-status.html) composable function to observe the connection status as a State: + + ```javascript const connectionStatus = chatClient.connection.status; @@ -56,6 +60,17 @@ let status = chatClient.connection.status ```kotlin val connectionStatus = chatClient.connection.status ``` + +```jetpack +import com.ably.chat.extensions.compose.collectAsStatus + +@Composable +fun MyComponent(chatClient: ChatClient) { + val connectionStatus by chatClient.connection.collectAsStatus() + + Text("Connection status: $connectionStatus") +} +``` @@ -84,6 +99,10 @@ Listeners can also be registered to monitor the changes in connection status. An Use the [`connection.onStatusChange()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Connection.html#onStatusChange)[`connection.onStatusChange()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/connection/onstatuschange%28%29-76t7)[`connection.status.onStatusChange()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-connection/on-status-change.html) method to register a listener for status change updates: + +In Jetpack Compose, you can use [`collectAsStatus()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-status.html) to observe status changes reactively: + + ```javascript const { off } = chatClient.connection.onStatusChange((change) => console.log(change)); @@ -114,6 +133,24 @@ val (off) = chatClient.connection.onStatusChange { statusChange: ConnectionStatu println(statusChange.toString()) } ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.ChatClient +import com.ably.chat.extensions.compose.collectAsStatus + +@Composable +fun MyComponent(chatClient: ChatClient) { + val connectionStatus by chatClient.connection.collectAsStatus() + + LaunchedEffect(connectionStatus) { + println("Connection status changed to: $connectionStatus") + } + + Text("Connection status: $connectionStatus") +} +``` @@ -148,6 +185,10 @@ The Chat SDK provides an `onDiscontinuity()` handler exposed via the Room object Any hooks that take an optional listener to monitor their events, such as typing indicator events in the `useTyping` hook, can also register a listener to be notified of, and handle, periods of discontinuity. + +Use the [`discontinuityAsFlow()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/discontinuity-as-flow.html) extension function to observe discontinuity events as a Flow in Jetpack Compose: + + For example, for messages: @@ -184,6 +225,22 @@ val (off) = room.onDiscontinuity { reason: ErrorInfo -> // Recover from the discontinuity } ``` + +```jetpack +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.discontinuityAsFlow + +@Composable +fun MyComponent(room: Room) { + LaunchedEffect(room) { + room.discontinuityAsFlow().collect { error -> + // Recover from the discontinuity + println("Discontinuity detected: $error") + } + } +} +``` diff --git a/src/pages/docs/chat/getting-started/android.mdx b/src/pages/docs/chat/getting-started/android.mdx index f29424da58..e49c4b51b8 100644 --- a/src/pages/docs/chat/getting-started/android.mdx +++ b/src/pages/docs/chat/getting-started/android.mdx @@ -252,8 +252,8 @@ fun ChatBox(room: Room?) { var sending by remember { mutableStateOf(false) } val messages = remember { mutableStateListOf() } - DisposableEffect(room) { - val subscription = room?.messages?.subscribe { event -> + LaunchedEffect(room) { + room?.messages?.asFlow()?.collect { event -> when (event.type) { MessageEventType.Created -> { // Check if the incoming message is correctly ordered @@ -266,10 +266,6 @@ fun ChatBox(room: Room?) { else -> Unit } } - - onDispose { - subscription?.unsubscribe() - } } Column( @@ -445,8 +441,8 @@ var edited: Message? by remember { mutableStateOf(null) } ```kotlin -DisposableEffect(room) { - val subscription = room?.messages?.subscribe { event -> +LaunchedEffect(room) { + room?.messages?.asFlow()?.collect { event -> when (event.type) { MessageEventType.Created -> messages.add(0, event.message) MessageEventType.Updated -> messages.replaceFirstWith(event.message) { @@ -455,10 +451,6 @@ DisposableEffect(room) { else -> Unit } } - - onDispose { - subscription?.unsubscribe() - } } ``` @@ -543,7 +535,7 @@ When you click on the edit button in the UI, you can modify the text and it will ## Step 6: Message history and continuity -Ably Chat enables you to retrieve previously sent messages in a room. This is useful for providing conversational context when a user first joins a room, or when they subsequently rejoin it later on. The message subscription object exposes the [`getPreviousMessages()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages-subscription/get-previous-messages.html) method to enable this functionality. This method returns a paginated response, which can be queried further to retrieve the next set of messages. +Ably Chat enables you to retrieve previously sent messages in a room. This is useful for providing conversational context when a user first joins a room, or when they subsequently rejoin it later on. The message subscription object exposes the [`historyBeforeSubscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages-subscription/history-before-subscribe.html) method to enable this functionality. This method returns a paginated response, which can be queried further to retrieve the next set of messages. Extend the `ChatBox` component to include a method to retrieve the last 10 messages when the component mounts. In your `MainActivity.kt` file, add the `DisposableEffect` in your `ChatBox` component: @@ -551,9 +543,10 @@ Extend the `ChatBox` component to include a method to retrieve the last 10 messa ```kotlin fun ChatBox(room: Room?) { /* variables declaration */ + var subscription by remember { mutableStateOf(null) } DisposableEffect(room) { - val subscription = room?.messages?.subscribe { event -> + subscription = room?.messages?.subscribe { event -> when (event.type) { MessageEventType.Created -> messages.add(0, event.message) MessageEventType.Updated -> messages.replaceFirstWith(event.message) { @@ -563,16 +556,19 @@ fun ChatBox(room: Room?) { } } - scope.launch { - val previousMessages = subscription?.historyBeforeSubscribe(10)?.items ?: emptyList() - messages.addAll(previousMessages) - } - onDispose { subscription?.unsubscribe() } - } - /* rest of your code */ + } + + LaunchedEffect(subscription) { + subscription?.let { sub -> + val previousMessages = sub.historyBeforeSubscribe(10)?.items ?: emptyList() + messages.addAll(previousMessages) + } + } + + /* rest of your code */ } ``` @@ -709,7 +705,7 @@ fun ChatBox(room: Room?) { Do the following to test this out: 1. Use the ably CLI to simulate sending some messages to the room from another client. -2. Refresh the page, this will cause the `ChatBox` component to mount again and call the `getPreviousMessages()` method. +2. Refresh the page, this will cause the `ChatBox` component to mount again and call the `historyBeforeSubscribe()` method. 3. You'll see the last 10 messages appear in the chat box. ## Step 7: Display who is present in the room @@ -722,14 +718,15 @@ In your `MainActivity.kt` file, create a new component called `PresenceStatusUi` ```kotlin @Composable fun PresenceStatusUi(room: Room?) { - val members = room?.collectAsPresenceMembers() + val membersState = room?.collectAsPresenceMembers() + val members by membersState ?: remember { mutableStateOf(emptyList()) } LaunchedEffect(room) { room?.presence?.enter() } Text( - text = "Online: ${members?.size ?: 0}", + text = "Online: ${members.size}", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 8.dp) ) diff --git a/src/pages/docs/chat/rooms/history.mdx b/src/pages/docs/chat/rooms/history.mdx index 74d1507ece..2673214b58 100644 --- a/src/pages/docs/chat/rooms/history.mdx +++ b/src/pages/docs/chat/rooms/history.mdx @@ -11,6 +11,10 @@ The history feature enables users to retrieve messages that have been previously Use the [`messages.history()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#history)[`messages.history()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/history(withparams:))[`messages.history()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/history.html) method to retrieve messages that have been previously sent to a room. This returns a paginated response, which can be queried further to retrieve the next set of messages. + +Use the [`collectAsPagingMessagesState()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-paging-messages-state.html) method to retrieve messages that have been previously sent to a room. This returns a paginated response, which can be queried further to retrieve the next set of messages. + + Use the [`history()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-react.UseMessagesResponse.html#history) method available from the response of the `useMessages` hook to retrieve messages that have been previously sent to a room. This returns a paginated response, which can be queried further to retrieve the next set of messages. @@ -71,6 +75,29 @@ while (historicalMessages.hasNext()) { println("End of messages") ``` + +```jetpack +import androidx.compose.foundation.lazy.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.OrderBy +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsPagingMessagesState + +@Composable +fun HistoryComponent(room: Room) { + val pagingMessagesState by room.messages.collectAsPagingMessagesState( + orderBy = OrderBy.NewestFirst + ) + + LazyColumn { + items(pagingMessagesState.messages.size) { index -> + val message = pagingMessagesState.messages[index] + Text("Message: ${message.text}") + } + } +} +``` The following optional parameters can be passed when retrieving previously sent messages: @@ -87,7 +114,11 @@ The following optional parameters can be passed when retrieving previously sent Users can also retrieve historical messages that were sent to a room before the point that they registered a listener by [subscribing](/docs/chat/rooms/messages#subscribe). The order of messages returned is from most recent, to oldest. This is useful for providing conversational context when a user first joins a room, or when they subsequently rejoin it later on. It also ensures that the message history they see is continuous, without any overlap of messages being returned between their subscription and their history call. -Use the [`historyBeforeSubscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.MessageSubscriptionResponse.html#historyBeforeSubscribe)[`historyBeforeSubscribe(withParams:)`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messagesubscriptionresponse/historybeforesubscribe%28withparams%3A%29))[`getPreviousMessages()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages-subscription/get-previous-messages.html) function returned as part of a [message subscription](/docs/chat/rooms/messages#subscribe) response to only retrieve messages that were received before the listener was subscribed to the room. This returns a paginated response, which can be queried further to retrieve the next set of messages. +Use the [`historyBeforeSubscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.MessageSubscriptionResponse.html#historyBeforeSubscribe)[`historyBeforeSubscribe(withParams:)`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messagesubscriptionresponse/historybeforesubscribe%28withparams%3A%29))[`historyBeforeSubscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages-subscription/history-before-subscribe.html) function returned as part of a [message subscription](/docs/chat/rooms/messages#subscribe) response to only retrieve messages that were received before the listener was subscribed to the room. This returns a paginated response, which can be queried further to retrieve the next set of messages. + + + +Use the [`historyBeforeSubscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages-subscription/history-before-subscribe.html) function returned as part of a [message subscription](/docs/chat/rooms/messages#subscribe) response to only retrieve messages that were received before the listener was subscribed to the room. This returns a paginated response, which can be queried further to retrieve the next set of messages. @@ -167,6 +198,53 @@ while (historicalMessages.hasNext()) { println("End of messages") ``` + +```jetpack +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.MessagesSubscription +import com.ably.chat.Room + +@Composable +fun HistoryBeforeSubscribeComponent(room: Room) { + var messages by remember { mutableStateOf>(emptyList()) } + var subscription by remember { mutableStateOf(null) } + + DisposableEffect(room) { + subscription = room.messages.subscribe { + println("New message received") + } + + onDispose { + subscription?.unsubscribe() + } + } + + LaunchedEffect(subscription) { + subscription?.let { sub -> + var historicalMessages = sub.historyBeforeSubscribe(limit = 50) + println(historicalMessages.items.toString()) + messages = historicalMessages.items.map { it.text } + + while (historicalMessages.hasNext()) { + historicalMessages = historicalMessages.next() + println(historicalMessages.items.toString()) + messages = messages + historicalMessages.items.map { it.text } + } + + println("End of messages") + } + } + + // Display messages in UI + Column { + messages.forEach { message -> + Text(message) + } + } +} +``` The following parameters can be passed when retrieving previously sent messages: diff --git a/src/pages/docs/chat/rooms/index.mdx b/src/pages/docs/chat/rooms/index.mdx index 4e8a3bdfc0..e4f9338874 100644 --- a/src/pages/docs/chat/rooms/index.mdx +++ b/src/pages/docs/chat/rooms/index.mdx @@ -21,8 +21,8 @@ The default 30-day retention period can be extended up to 365 days by [contactin Users send messages to a room and subscribe to the room in order to receive messages. - -To get an instance of a chat room, use the [`rooms.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Rooms.html#get)[`rooms.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/rooms/get%28named%3Aoptions%3A%29)[`rooms.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-rooms/get.html) method. It will create a new room instance if one doesn't already exist, or return the existing one if it does. + +To get an instance of a chat room, use the [`rooms.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Rooms.html#get)[`rooms.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/rooms/get%28named%3Aoptions%3A%29)[`rooms.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-rooms/get.html) method. It will create a new room instance if one doesn't already exist, or return the existing one if it does. @@ -70,6 +70,10 @@ let room = try await chatClient.rooms.get(named: "basketball-stream", options: . ```kotlin val room = chatClient.rooms.get(roomId = "basketball-stream") ``` + +```jetpack +val room = chatClient.rooms.get(roomId = "basketball-stream") +``` @@ -79,9 +83,9 @@ If the value changes between re-renders then the room will be discarded and recr - + -When you create or retrieve a room using `rooms.get()`, you can provide custom configuration for some features for that room by passing a [`RoomOptions`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomOptions.html)[`RoomOptions`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomoptions)[`RoomOptions`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-options/index.html) object as the second argument. If you do not provide a `RoomOptions` object, the default settings will be used. +When you create or retrieve a room using `rooms.get()`, you can provide custom configuration for some features for that room by passing a [`RoomOptions`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomOptions.html)[`RoomOptions`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomoptions)[`RoomOptions`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-options/index.html) object as the second argument. If you do not provide a `RoomOptions` object, the default settings will be used. ```javascript @@ -120,6 +124,20 @@ val room = chatClient.rooms.get(roomId = "basketball-stream") { } } ``` + +```jetpack +val room = chatClient.rooms.get(roomId = "basketball-stream") { + typing { + heartbeatThrottle = 5.seconds + } + presence { + enableEvents = true + } + occupancy { + enableEvents = true + } +} +``` The details of the options available to each feature are documented on their respective pages: @@ -138,8 +156,8 @@ Releasing a room allows the underlying resources to be garbage collected or rele Releasing a room may be optional for many applications. If you have multiple transient rooms, such as in the case of a 1:1 support chat, then it may be more beneficial. Also, proactively disconnecting rather than waiting for the standard two-minute timeout can help reduce costs and improve performance. - -Once [`rooms.release()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Rooms.html#release)[`rooms.release()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/rooms/release%28named%3A%29)[`rooms.release()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-rooms/release.html) has been called, the room will be unusable and a new instance will need to be created using [`rooms.get()`](#create) if you want to reuse it. + +Once [`rooms.release()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Rooms.html#release)[`rooms.release()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/rooms/release%28named%3A%29)[`rooms.release()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-rooms/release.html) has been called, the room will be unusable and a new instance will need to be created using [`rooms.get()`](#create) if you want to reuse it. ```javascript @@ -153,6 +171,10 @@ try await rooms.release(named: "basketball-stream") ```kotlin rooms.release("basketball-stream") ``` + +```jetpack +rooms.release("basketball-stream") +``` @@ -168,10 +190,10 @@ By default the `ChatRoomProvider` will automatically call [`release()`](https:// To start receiving messages and events from a room, you need to attach to it. Attaching to a room tells Ably to start streaming messages to the client, and ensures that events are not missed in case of temporary network interruptions. - + Once an instance of a room has been created using `rooms.get()`, clients attach to it to start receiving messages and events from the room. -Use the [`attach()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Room.html#attach)[`attach()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/room/attach%28%29)[`attach()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room/attach.html) method on a room to attach to it: +Use the [`attach()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Room.html#attach)[`attach()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/room/attach%28%29)[`attach()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room/attach.html) method on a room to attach to it: @@ -203,6 +225,10 @@ try await room.attach() ```kotlin room.attach() ``` + +```jetpack +room.attach() +``` As soon as a client is attached to a room, Ably will begin streaming messages and events to them. To receive the messages and events in your application code, you need to add listeners to the events that you are interested in by subscribing, for example using the [`messages.subscribe()`](/docs/chat/rooms/messages#subscribe) method. Add listeners before attaching to avoid missing any messages or events. @@ -215,8 +241,8 @@ As soon as a client is attached to a room, Ably will begin streaming messages an ### Detach from a room - -Use the [`detach()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Room.html#detach)[`detach()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/room/detach%28%29)[`detach()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room/detach.html) method on a room to detach from it and stop receiving messages and events: + +Use the [`detach()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Room.html#detach)[`detach()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/room/detach%28%29)[`detach()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room/detach.html) method on a room to detach from it and stop receiving messages and events: ```javascript @@ -230,6 +256,10 @@ try await room.detach() ```kotlin room.detach() ``` + +```jetpack +room.detach() +``` @@ -262,6 +292,10 @@ A room can have any of the following statuses: Use the [`status`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomStatus.html#status)[`status`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomstatus)[`status`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room/status.html) property to check which status a room is currently in: + +Use the [`collectAsStatus()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-status.html) composable function to observe the room status as a State: + + Use the `roomStatus` property to view the current [`Room`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Room.html) status changes. The `roomError` property is its associated error. Any hooks that take an optional listener have these properties available in their response, such as `useMessages` or `useTyping`. It is more common that you will monitor the room status in the specific feature hooks rather than needing to use `useRoom`. These events are related to the room instance of the nearest [`ChatRoomProvider`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.ChatRoomProvider.html). For example, with the `useMessages` hook: @@ -300,12 +334,31 @@ let status = room.status ```kotlin val status = room.status ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsStatus + +@Composable +fun MyComponent(room: Room) { + val roomStatus by room.collectAsStatus() + + Text("Room status: $roomStatus") +} +``` You can also subscribe to room status updates by registering a listener. An event will be emitted whenever the status of the room changes. Use the [`room.onStatusChange()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Room.html#onStatusChange)[`room.onStatusChange()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/room/onstatuschange%28%29-s9g)[`room.onStatusChange()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room/on-status-change.html) method in a room to register a listener for status change updates: + + + +Use the [`collectAsStatus()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-status.html) extension function to observe room status changes reactively in Jetpack Compose: + ```javascript @@ -325,8 +378,25 @@ val (off) = room.onStatusChange { statusChange: RoomStatusChange -> println(statusChange.toString()) } ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsStatus + +@Composable +fun MyComponent(room: Room) { + val roomStatus by room.collectAsStatus() + + LaunchedEffect(roomStatus) { + println("Room status changed to: $roomStatus") + } + + Text("Room status: $roomStatus") +} +``` - Use the `off()` function returned in the `onStatusChange()` response to remove a room status listener: diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index 961e129af3..6424bb50fa 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -23,7 +23,7 @@ Note that if sending two identical reactions of type `Distinct`, the second one ### Configure the default reaction type - + The default reaction type can be configured at room-level by passing `RoomOptions` when calling `rooms.get`. If nothing is set, the default is `Distinct`. @@ -81,7 +81,7 @@ const MyComponent = () => { ## Sending a message reaction - + To send a message reaction use `room.messages.reactions.send(message, params)`. This method takes the following parameters: * `message` - The message to send the reaction to. Can be either a Message object or a string containing the message serial. * `params` - Set the `name`, and optionally override the `type` or set a `count`. @@ -145,8 +145,8 @@ await room.messages.reactions.send(forMessageWithSerial: message.serial, params: room.messages.reactions.send(message, name = "👍") // The reaction can be anything, not just UTF-8 emojis: -room.messages.reactions.send(message, name = ":like:")) -room.messages.reactions.send(message, name = "+1")) +room.messages.reactions.send(message, name = ":like:") +room.messages.reactions.send(message, name = "+1") // Send a :love: reaction using the Unique type room.messages.reactions.send(message, @@ -162,6 +162,43 @@ room.messages.reactions.send(message, ) ``` +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.MessageReactionType +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun SendMessageReactionComponent(room: Room, message: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + // Send a 👍 reaction using the default type + room.messages.reactions.send(message, name = "👍") + } + }) { + Text("Send 👍") + } + + Button(onClick = { + coroutineScope.launch { + // Send a ❤️ reaction with count 100 using the Multiple type + room.messages.reactions.send( + message, + name = "❤️", + type = MessageReactionType.Multiple, + count = 100, + ) + } + }) { + Text("Send ❤️ x100") + } +} +``` + ```react import { MessageReactionType } from '@ably/chat'; import { useMessages } from '@ably/chat/react'; @@ -210,7 +247,7 @@ The `annotation-publish` capability is required for sending reactions. ## Removing a message reaction - + To remove a message reaction use `room.messages.reactions.delete(message, params)`. This method takes the following parameters: * `message` - The message to remove the reaction from. This can be a Message object, or just the string serial. * `params` - Set the `name`, and optionally override the `type` or set a `count`. @@ -277,6 +314,43 @@ const MyComponent = () => { ); }; ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.MessageReactionType +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun RemoveMessageReactionComponent(room: Room, message: Message) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + // Remove a 👍 reaction using the default type + room.messages.reactions.delete(message, name = "👍") + } + }) { + Text("Remove 👍") + } + + Button(onClick = { + coroutineScope.launch { + // Remove a ❤️ reaction with count 50 using the Multiple type + room.messages.reactions.delete( + message, + name = "❤️", + type = MessageReactionType.Multiple, + count = 50, + ) + } + }) { + Text("Remove ❤️ x50") + } +} +``` ## Messages and reactions @@ -343,6 +417,96 @@ interface Message { } } ``` + +```swift +struct Message { + // ... (other fields omitted) + var reactions: MessageReactionSummary +} + +struct MessageReactionSummary { + var unique: [String: ClientIDList] + var distinct: [String: ClientIDList] + var multiple: [String: ClientIDCounts] +} + +// example (in real use, it is unlikely that all reaction types are present): +// ... other message fields omitted +reactions: MessageReactionSummary( + unique: [ + "👍": ClientIDList(total: 2, clientIDs: ["clientA", "clientB"], clipped: false), + "❤️": ClientIDList(total: 1, clientIDs: ["clientC"], clipped: false), + ], + distinct: [ + "👍": ClientIDList(total: 2, clientIDs: ["clientA", "clientB"], clipped: false), + "❤️": ClientIDList(total: 1, clientIDs: ["clientA"], clipped: false), + ], + multiple: [ + "👍": ClientIDCounts(total: 10, clientIDs: ["clientA": 7, "clientB": 3], totalUnidentified: 0, clipped: false, totalClientIDs: 2), + "❤️": ClientIDCounts(total: 100, clientIDs: ["clientA": 100], totalUnidentified: 0, clipped: false, totalClientIDs: 1), + ] +) +``` + +```kotlin +interface Message { + // ... (other fields omitted) + val reactions: MessageReactionSummary +} + +interface MessageReactionSummary { + val unique: Map + val distinct: Map + val multiple: Map +} + +// example (in real use, it is unlikely that all reaction types are present): +// ... other message fields omitted +reactions = MessageReactionSummary( + unique = mapOf( + "👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false), + "❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientC"), clipped = false), + ), + distinct = mapOf( + "👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false), + "❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientA"), clipped = false), + ), + multiple = mapOf( + "👍" to SummaryClientIdCounts(total = 10, clientIds = mapOf("clientA" to 7, "clientB" to 3), totalUnidentified = 0, clipped = false, totalClientIds = 2), + "❤️" to SummaryClientIdCounts(total = 100, clientIds = mapOf("clientA" to 100), totalUnidentified = 0, clipped = false, totalClientIds = 1), + ) +) +``` + +```jetpack +interface Message { + // ... (other fields omitted) + val reactions: MessageReactionSummary +} + +interface MessageReactionSummary { + val unique: Map + val distinct: Map + val multiple: Map +} + +// example (in real use, it is unlikely that all reaction types are present): +// ... other message fields omitted +reactions = MessageReactionSummary( + unique = mapOf( + "👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false), + "❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientC"), clipped = false), + ), + distinct = mapOf( + "👍" to SummaryClientIdList(total = 2, clientIds = listOf("clientA", "clientB"), clipped = false), + "❤️" to SummaryClientIdList(total = 1, clientIds = listOf("clientA"), clipped = false), + ), + multiple = mapOf( + "👍" to SummaryClientIdCounts(total = 10, clientIds = mapOf("clientA" to 7, "clientB" to 3), totalUnidentified = 0, clipped = false, totalClientIds = 2), + "❤️" to SummaryClientIdCounts(total = 100, clientIds = mapOf("clientA" to 100), totalUnidentified = 0, clipped = false, totalClientIds = 1), + ) +) +``` All reaction types are always available via `Message.reactions`, regardless of the default reaction type configured via room options. @@ -354,7 +518,7 @@ Always call `Message.with(event)` when applying message events and reaction even ## Subscribing to message reactions - + Ably generates a summary (aggregate) of the reactions for each message and for each reaction type. For displaying accurate counts for message reactions, subscribe to changes in the message summary. @@ -381,6 +545,20 @@ room.messages.reactions.subscribe { event -> } ``` +```jetpack +import androidx.compose.runtime.* +import com.ably.chat.Room + +@Composable +fun SubscribeToReactionsComponent(room: Room) { + LaunchedEffect(room) { + room.messages.reactions.asFlow().collect { event -> + println("received reactions summary event: $event") + } + } +} +``` + ```react import { useMessages } from '@ably/chat/react'; @@ -454,6 +632,36 @@ room.messages.reactions.subscribe { event -> } ``` +```jetpack +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room + +@Composable +fun ReactionsWithMessagesComponent(room: Room) { + var messages by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(room) { + // init messages + messages = room.messages.history(limit = 50).items + } + + LaunchedEffect(room) { + // subscribe to message reactions summary events + room.messages.reactions.asFlow().collect { event -> + // find the relevant message (in practice: use binary search or a map for lookups) + val idx = messages.indexOfLast { msg -> msg.serial == event.messageSerial } + if (idx != -1) { + // update message + messages = messages.toMutableList().apply { + this[idx] = this[idx].with(event) + } + } + } + } +} +``` + ```react import { useState, useEffect } from 'react'; import { useMessages, Message } from '@ably/chat/react'; @@ -568,7 +776,7 @@ const MyComponent = () => { ``` - + Then you can receive raw reactions using the `room.messages.reactions.subscribeRaw()` method: @@ -607,6 +815,29 @@ room.messages.reactions.subscribeRaw { event -> } ``` +```jetpack +import androidx.compose.runtime.* +import com.ably.chat.MessageReactionEventType +import com.ably.chat.Room + +@Composable +fun SubscribeToRawReactionsComponent(room: Room) { + DisposableEffect(room) { + val (unsubscribe) = room.messages.reactions.subscribeRaw { event -> + if (event.type == MessageReactionEventType.Create) { + println("new reaction: ${event.reaction}") + } else if (event.type == MessageReactionEventType.Delete) { + println("reaction removed: ${event.reaction}") + } + } + + onDispose { + unsubscribe() + } + } +} +``` + ```react import { useMessages } from '@ably/chat/react'; import { MessageReactionEventType } from '@ably/chat'; diff --git a/src/pages/docs/chat/rooms/messages.mdx b/src/pages/docs/chat/rooms/messages.mdx index 1b85ca775c..6524ed9b09 100644 --- a/src/pages/docs/chat/rooms/messages.mdx +++ b/src/pages/docs/chat/rooms/messages.mdx @@ -17,6 +17,10 @@ A user can also update or delete a message, all users that are subscribed to the Subscribe to receive messages in a room by registering a listener. Use the [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#subscribe)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/subscribe%28%29-360z1)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) method in a room to receive all messages that are sent to it: + +Use the [`collectAsPagingMessagesState()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-paging-messages-state.html) composable function to observe messages with automatic pagination support. Alternatively, you can use [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) for a simple subscription: + + Subscribe to messages with the [`useMessages`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useMessages.html) hook. Supply a listener and the hook will automatically subscribe to message events sent to the room. As long as a defined value is provided, the subscription will persist across renders. If the listener value is undefined, the subscription will be removed until it becomes defined again. @@ -57,6 +61,22 @@ val subscription = room.messages.subscribe { messageEvent: ChatMessageEvent -> println(messageEvent.message.toString()) } ``` + +```jetpack +import com.ably.chat.extensions.compose.collectAsPagingMessagesState + +@Composable +fun MyComponent(room: Room) { + val pagingMessagesState by room.messages.collectAsPagingMessagesState() + + LazyColumn { + items(pagingMessagesState.messages.size) { index -> + val message = pagingMessagesState.messages[index] + Text("Message: ${message.text}") + } + } +} +``` ### Message structure @@ -107,6 +127,10 @@ See [below](#global-ordering) for more information on how to apply deterministic Use the `unsubscribe()` function returned in the `subscribe()` response to remove a chat message listener: + +`collectAsPagingMessagesState()` handles lifecycle and cleanup automatically. + + You don't need to handle removing listeners, as this is done automatically by the SDK. @@ -145,8 +169,8 @@ The [`detach()`](/docs/chat/rooms#detach) method detaches a user from the room. ## Send a message - -Use the [`messages.send()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#send)[`messages.send()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/send%28withparams%3A%29)[`messages.send()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/send.html) method to send a message in a chat room. All users that are "subscribed](subscribe to messages on that room will receive it: + +Use the [`messages.send()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#send)[`messages.send()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/send%28withparams%3A%29)[`messages.send()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/send.html) method to send a message in a chat room. All users that are [subscribed](#subscribe) to messages on that room will receive it: @@ -183,12 +207,32 @@ let message = try await room.messages.send(params: .init(text: "hello")) ```kotlin room.messages.send(text = "hello") ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun MyComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + room.messages.send(text = "hello") + } + }) { + Text("Send Message") + } +} +``` ## Get a single message - -Use the [`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#get)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/get%28withserial:%29)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/get.html) method to get a message in a chat room. + +Use the [`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#get)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/get%28withserial:%29)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/get.html) method to get a message in a chat room using message serial. @@ -200,14 +244,6 @@ Use the [`getMessage()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/type await room.messages.get('01726232498871-001@abcdefghij:001'); ``` -```kotlin -val message = room.messages.get("01726232498871-001@abcdefghij:001") -``` - -```swift -let message = try await room.messages.get(withSerial: "01726232498871-001@abcdefghij:001") -``` - ```react import { useMessages } from '@ably/chat/react'; @@ -225,14 +261,26 @@ const MyComponent = () => { ); }; ``` + +```swift +let message = try await room.messages.get(withSerial: "01726232498871-001@abcdefghij:001") +``` + +```kotlin +val message = room.messages.get("01726232498871-001@abcdefghij:001") +``` + +```jetpack +val message = room.messages.get("01726232498871-001@abcdefghij:001") +``` ## Update a message - -Use the [`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#update)[`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/update%28withserial:params:details:%29)[`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/update.html) method to update a message in a chat room. All users that are [subscribed](#subscribe) to messages on that room will receive the update: + +Use the [`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#update)[`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/update%28withserial:params:details:%29)[`messages.update()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/update.html) method to update a message in a chat room. All users that are [subscribed](#subscribe) to messages on that room will receive the update: @@ -288,12 +336,37 @@ val updatedMessage = room.messages.update( operationDescription = "Message update by user", ) ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun MyComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + val originalMessage: Message // assume this is available + + Button(onClick = { + coroutineScope.launch { + room.messages.update( + originalMessage.copy(text = "my updated text"), + operationDescription = "Message update by user", + ) + } + }) { + Text("Update Message") + } +} +``` ### Filter for updates - -Use the [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#subscribe)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/subscribe%28%29-8jolq)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) method to receive messages in a room. To filter for updated messages, provide a listener that checks the `type``action` property of the message event: + +Use the [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#subscribe)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/subscribe%28%29-8jolq)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) method to receive messages in a room. To filter for updated messages, provide a listener that checks the `type``action` property of the message event: @@ -383,6 +456,34 @@ val messagesSubscription = room.messages.subscribe { event -> } } ``` + +```jetpack +@Composable +fun MyComponent(room: Room) { + var myMessageList by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(room) { + room.messages.asFlow().collect { event -> + when (event.type) { + ChatMessageEventType.Created -> { + myMessageList = myMessageList + event.message + } + ChatMessageEventType.Updated -> { + myMessageList = myMessageList.map { message -> + if (message.serial == event.message.serial && + event.message.version.serial > message.version.serial) { + event.message + } else { + message + } + } + } + else -> {} + } + } + } +} +``` See [below](#global-ordering) for more information on how to deterministically apply ordering to update events in your application. @@ -426,8 +527,8 @@ The updated message response is identical to the structure of a message, with th ## Delete a message - -Use the [`messages.delete()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#delete)[`messages.delete()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/delete%28withserial:details:%29)[`messages.delete()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/delete.html) method to delete a message in a chat room. All users that are [subscribed](#subscribe) to messages on that room will receive the deletion: + +Use the [`messages.delete()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#delete)[`messages.delete()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/delete%28withserial:details:%29)[`messages.delete()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/delete.html) method to delete a message in a chat room. All users that are [subscribed](#subscribe) to messages on that room will receive the deletion: @@ -482,12 +583,37 @@ val deletedMessage = room().messages.delete( operationDescription = "Message deleted by user", ) ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Message +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun MyComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + val messageToDelete: Message // assume this is available + + Button(onClick = { + coroutineScope.launch { + room.messages.delete( + messageToDelete, + operationDescription = "Message deleted by user", + ) + } + }) { + Text("Delete Message") + } +} +``` ### Filter for deletes - -Use the [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#subscribe)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/subscribe%28%29-8jolq)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) method to receive messages in a room. To filter for deleted messages, provide a listener that checks the `type``action` property of the message event: + +Use the [`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#subscribe)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/subscribe%28%29-8jolq)[`messages.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/subscribe.html) method to receive messages in a room. To filter for deleted messages, provide a listener that checks the `type``action` property of the message event: @@ -577,6 +703,30 @@ val messagesSubscription = room.messages.subscribe { event -> } } ``` + +```jetpack +@Composable +fun MyComponent(room: Room) { + var myMessageList by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(room) { + room.messages.asFlow().collect { event -> + when (event.type) { + ChatMessageEventType.Created -> { + myMessageList = myMessageList + event.message + } + ChatMessageEventType.Deleted -> { + myMessageList = myMessageList.filterNot { message -> + message.serial == event.message.serial && + event.message.version.serial > message.version.serial + } + } + else -> {} + } + } + } +} +``` See [below](#global-ordering) for more information on how to deterministically apply ordering to delete events in your application. diff --git a/src/pages/docs/chat/rooms/occupancy.mdx b/src/pages/docs/chat/rooms/occupancy.mdx index fe7017b6ce..620b4893fc 100644 --- a/src/pages/docs/chat/rooms/occupancy.mdx +++ b/src/pages/docs/chat/rooms/occupancy.mdx @@ -11,8 +11,8 @@ Occupancy generates messages on any client entering/leaving a room, and so incre ## Subscribe to room occupancy - -Subscribe to a room's occupancy by registering a listener. Occupancy events are emitted whenever the number of online users within a room changes. Use the [`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Occupancy.html#subscribe)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/occupancy/subscribe%28%29-3loon)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-occupancy/subscribe.html) method in a room to receive updates: + +Subscribe to a room's occupancy by registering a listener. Occupancy events are emitted whenever the number of online users within a room changes. Use the [`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Occupancy.html#subscribe)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/occupancy/subscribe%28%29-3loon)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-occupancy/subscribe.html)[`collectAsOccupancy()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-occupancy.html) method in a room to receive updates: @@ -58,6 +58,21 @@ val subscription = room.occupancy.subscribe { event: OccupancyEvent -> println("Number of members present is: ${event.occupancy.presenceMembers}") } ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsOccupancy + +@Composable +fun OccupancyComponent(room: Room) { + val occupancy by room.occupancy.collectAsOccupancy() + + Text("Number of users connected: ${occupancy.connections}") + Text("Number of members present: ${occupancy.presenceMembers}") +} +``` - -Use the [`presence.enter()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#enter)[`presence.enter()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/enter%28withdata%3A%29)[`presence.enter()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/enter.html) method to indicate when a user joins a room. This will send a presence event to all users subscribed to presence indicating that a new member has joined the chat. You can also set an optional data field with information such as the status of a user: + +Use the [`presence.enter()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#enter)[`presence.enter()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/enter%28withdata%3A%29)[`presence.enter()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/enter.html) method to indicate when a user joins a room. This will send a presence event to all users subscribed to presence indicating that a new member has joined the chat. You can also set an optional data field with information such as the status of a user: @@ -176,10 +208,29 @@ room.presence.enter( }, ) ``` + +```jetpack +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.json.* + +@Composable +fun EnterPresenceComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(room) { + room.presence.enter( + jsonObject { + put("status", "Online") + } + ) + } +} +``` - -Use the [`presence.update()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#update)[`presence.update()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/update%28withdata%3A%29)[`presence.update()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/update.html) method when a user wants to update their data, such as an update to their status, or to indicate that they're raising their hand. Updates will send a presence event to all users subscribed to presence: + +Use the [`presence.update()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#update)[`presence.update()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/update%28withdata%3A%29)[`presence.update()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/update.html) method when a user wants to update their data, such as an update to their status, or to indicate that they're raising their hand. Updates will send a presence event to all users subscribed to presence: @@ -227,10 +278,35 @@ room.presence.update( }, ) ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.json.* +import kotlinx.coroutines.launch + +@Composable +fun UpdatePresenceComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + room.presence.update( + jsonObject { + put("status", "Busy") + } + ) + } + }) { + Text("Set Status to Busy") + } +} +``` - -Use the [`presence.leave()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#leave)[`presence.leave()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/leave%28withdata%3A%29)[`presence.leave()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/leave.html) method to explicitly remove a user from the presence set. This will send a presence event to all users subscribed to presence. You can also set an optional data field such as setting a status of 'Back later'. + +Use the [`presence.leave()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#leave)[`presence.leave()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/leave%28withdata%3A%29)[`presence.leave()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/leave.html) method to explicitly remove a user from the presence set. This will send a presence event to all users subscribed to presence. You can also set an optional data field such as setting a status of 'Back later'. @@ -278,6 +354,31 @@ room.presence.leave( }, ) ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.json.* +import kotlinx.coroutines.launch + +@Composable +fun LeavePresenceComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + room.presence.leave( + jsonObject { + put("status", "Be back later!") + } + ) + } + }) { + Text("Leave Presence") + } +} +``` When a user goes offline or closes their [connection](/docs/chat/connect), a leave event is also emitted and they are removed from the presence set. @@ -350,10 +451,10 @@ The following options can be set when [creating a room](/docs/chat/rooms#create) ## Retrieve the presence set - + The online presence of users can be retrieved in one-off calls. This can be used to check the status of an individual user, or return the entire presence set as an array. -Use the [`presence.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#get)[`presence.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/get%28%29)[`presence.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/get.html) method to retrieve an array of all users currently entered into the presence set, or individual users: +Use the [`presence.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#get)[`presence.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/get%28%29)[`presence.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/get.html)[`collectAsPresenceMembers()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-presence-members.html) method to retrieve an array of all users currently entered into the presence set, or individual users: ```javascript @@ -379,9 +480,26 @@ val presentMembers = room.presence.get() // Retrieve the status of specific users by their clientId: val presentMember = room.presence.get(clientId = "clemons123") ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsPresenceMembers + +@Composable +fun GetPresenceComponent(room: Room) { + val presentMembers by room.presence.collectAsPresenceMembers() + + Text("Total present: ${presentMembers.size}") + presentMembers.forEach { member -> + Text("User: ${member.clientId}") + } +} +``` -Alternatively, use the [`presence.isUserPresent()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#isUserPresent)[`presence.isUserPresent()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/isuserpresent%28withclientid%3A%29)[`presence.isUserPresent()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/is-user-present.html) method and pass in a user's `clientId` to check whether they are online or not. This will return a boolean: +Alternatively, use the [`presence.isUserPresent()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#isUserPresent)[`presence.isUserPresent()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/isuserpresent%28withclientid%3A%29)[`presence.isUserPresent()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/is-user-present.html) method and pass in a user's `clientId` to check whether they are online or not. This will return a boolean: ```javascript @@ -395,6 +513,23 @@ let isPresent = try await room.presence.isUserPresent(withClientID: "clemons123" ```kotlin val isPresent = room.presence.isUserPresent("client-id") ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room + +@Composable +fun IsUserPresentComponent(room: Room, clientId: String) { + var isPresent by remember { mutableStateOf(false) } + + LaunchedEffect(room, clientId) { + isPresent = room.presence.isUserPresent(clientId) + } + + Text("User $clientId is ${if (isPresent) "present" else "not present"}") +} +``` diff --git a/src/pages/docs/chat/rooms/reactions.mdx b/src/pages/docs/chat/rooms/reactions.mdx index e494ebc5e6..4988e267dc 100644 --- a/src/pages/docs/chat/rooms/reactions.mdx +++ b/src/pages/docs/chat/rooms/reactions.mdx @@ -9,8 +9,8 @@ Room reactions are ephemeral and not stored or aggregated by Ably. The intention ## Subscribe to room reactions - -Subscribe to room reactions by registering a listener. Use the [`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomReactions.html#subscribe)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/subscribe%28%29-64gdf)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-reactions/subscribe.html) method in a room to receive reactions: + +Subscribe to room reactions by registering a listener. Use the [`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomReactions.html#subscribe)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/subscribe%28%29-64gdf)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-reactions/subscribe.html) method in a room to receive reactions: @@ -47,7 +47,22 @@ for await event in reactionSubscription { ```kotlin val subscription = room.reactions.subscribe { event: RoomReactionEvent -> - println("Received a reaciton of name ${event.reaction.name} with metadata ${event.reaction.metadata}") + println("Received a reaction of name ${event.reaction.name} with metadata ${event.reaction.metadata}") +} +``` + +```jetpack +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.RoomReactionEvent + +@Composable +fun RoomReactionsComponent(room: Room) { + LaunchedEffect(room) { + room.reactions.asFlow().collect { event: RoomReactionEvent -> + println("Received a reaction of name ${event.reaction.name} with metadata ${event.reaction.metadata}") + } + } } ``` @@ -69,8 +84,8 @@ The following are the properties of a room reaction event: ### Unsubscribe from room reactions - -Use the `unsubscribe()` function returned in the `subscribe()` response to remove a room reaction listener: + +Use the `unsubscribe()` function returned in the `subscribe()` response to remove a room reaction listener. Jetpack Compose automatically handles lifecycle and cleanup when using `LaunchedEffect`: @@ -81,7 +96,7 @@ You don't need to handle removing listeners, as this is done automatically by th When you unmount the component that is using the `useRoomReactions` hook, it will automatically handle unsubscribing any associated listeners registered for room reactions. - + ```javascript // Initial subscription @@ -99,6 +114,17 @@ val (unsubscribe) = room.reactions.subscribe { event -> println("Received a reaction of type ${event.reaction.name}, and metadata ${event.reaction.metadata}") } +// To remove the listener +unsubscribe() +``` + +```jetpack +// Jetpack Compose handles cleanup automatically +// When using subscribe directly: +val (unsubscribe) = room.reactions.subscribe { event -> + println("Received a reaction of type ${event.reaction.name}, and metadata ${event.reaction.metadata}") +} + // To remove the listener unsubscribe() ``` @@ -107,8 +133,8 @@ unsubscribe() ## Send a room reaction - -Use the [`reactions.send()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomReactions.html#send)[`reactions.send()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/send%28withparams%3A%29)[`reactions.send()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-reactions/send.html) method to send a room-level reaction. The most common way of using this method is to trigger it whenever a user clicks an emoji button in a room: + +Use the [`reactions.send()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomReactions.html#send)[`reactions.send()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/send%28withparams%3A%29)[`reactions.send()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-reactions/send.html) method to send a room-level reaction. The most common way of using this method is to trigger it whenever a user clicks an emoji button in a room: @@ -154,4 +180,38 @@ room.reactions.send(name = "heart", metadata = jsonObject { put("effect", "fireworks") }) ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.json.* +import kotlinx.coroutines.launch + +@Composable +fun SendReactionComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + room.reactions.send(name = "like") + } + }) { + Text("Send Like") + } + + Button(onClick = { + coroutineScope.launch { + room.reactions.send( + name = "heart", + metadata = jsonObject { + put("effect", "fireworks") + } + ) + } + }) { + Text("Send Heart with Effect") + } +} +``` diff --git a/src/pages/docs/chat/rooms/typing.mdx b/src/pages/docs/chat/rooms/typing.mdx index ec6b07adfd..5d3a0cb67f 100644 --- a/src/pages/docs/chat/rooms/typing.mdx +++ b/src/pages/docs/chat/rooms/typing.mdx @@ -7,8 +7,8 @@ Typing indicators enable you to display which users are currently writing a mess ## Subscribe to typing events - -Subscribe to typing events by registering a listener. Typing events can be emitted when a user starts typing, and when they stop typing. Use the [`typing.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#subscribe)[`typing.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/subscribe%28%29-7uox7)[`typing.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/subscribe.html) method in a room to receive these updates: + +Subscribe to typing events by registering a listener. Typing events can be emitted when a user starts typing, and when they stop typing. Use the [`typing.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#subscribe)[`typing.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/subscribe%28%29-7uox7)[`typing.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/subscribe.html)[`collectAsCurrentlyTyping()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-currently-typing.html) method in a room to receive these updates: @@ -55,6 +55,20 @@ val subscription = room.typing.subscribe { event: TypingSetEvent -> println("currently typing: ${event.currentlyTyping}") } ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsCurrentlyTyping + +@Composable +fun TypingComponent(room: Room) { + val currentlyTyping by room.typing.collectAsCurrentlyTyping() + + Text("Currently typing: ${currentlyTyping.joinToString(", ")}") +} +``` ### Typing event structure @@ -91,8 +105,8 @@ You can use the size of the `currentlyTyping` set to decide whether to display i ### Unsubscribe from typing events - -Use the `unsubscribe()` function returned in the `subscribe()` response to remove a typing listener: + +Use the `unsubscribe()` function returned in the `subscribe()` response to remove a typing listener. Jetpack Compose automatically handles lifecycle and cleanup when using `collectAsCurrentlyTyping()`: @@ -103,7 +117,7 @@ You don't need to handle removing listeners, as this is done automatically by th When you unmount the component that is using the `useTyping` hook, it will automatically handle unsubscribing any associated listeners registered for typing events. - + ```javascript // Initial subscription @@ -122,6 +136,17 @@ val (unsubscribe) = room.typing.subscribe { event -> println("Typing event received: $event") } +// To remove the listener +unsubscribe() +``` + +```jetpack +// Jetpack Compose handles cleanup automatically +// When using subscribe directly: +val (unsubscribe) = room.typing.subscribe { event -> + println("Typing event received: $event") +} + // To remove the listener unsubscribe() ``` @@ -130,8 +155,8 @@ unsubscribe() ## Set typing status - -Use the [`typing.keystroke()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#keystroke)[`typing.keystroke()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/keystroke%28%29)[`typing.keystroke()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/keystroke.html) method to emit a typing event with `type` set to `typing.started`. + +Use the [`typing.keystroke()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#keystroke)[`typing.keystroke()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/keystroke%28%29)[`typing.keystroke()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/keystroke.html) method to emit a typing event with `type` set to `typing.started`. @@ -169,10 +194,34 @@ try await room.typing.keystroke() ```kotlin room.typing.keystroke() ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun TypingKeystrokeComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + var text by remember { mutableStateOf("") } + + TextField( + value = text, + onValueChange = { newText -> + text = newText + coroutineScope.launch { + room.typing.keystroke() + } + }, + label = { Text("Type a message") } + ) +} +``` - -Use the [`stop()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#stop)[`stop()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/stop%28%29)[`stop()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/stop.html) method to emit a typing event with `type` set to `typing.stopped`. + +Use the [`stop()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#stop)[`stop()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/stop%28%29)[`stop()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/stop.html) method to emit a typing event with `type` set to `typing.stopped`. @@ -209,6 +258,26 @@ try await room.typing.stop() ```kotlin room.typing.stop() ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import kotlinx.coroutines.launch + +@Composable +fun StopTypingComponent(room: Room) { + val coroutineScope = rememberCoroutineScope() + + Button(onClick = { + coroutineScope.launch { + room.typing.stop() + } + }) { + Text("Stop Typing") + } +} +``` ### Typing Event Frequency @@ -249,8 +318,8 @@ All clients in a room must have the same timeout value configured. If not, typin ## Retrieve a list of users that are currently typing - -Use the [`typing.current`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#current)[`typing.current`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/current)[`typing.current`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/current.html) property to retrieve a set of `clientId`s for all users that are currently typing in the room: + +Use the [`typing.current`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Typing.html#current)[`typing.current`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/typing/current)[`typing.current`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-typing/current.html)[`collectAsCurrentlyTyping()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-currently-typing.html) property to retrieve a set of `clientId`s for all users that are currently typing in the room: @@ -265,6 +334,20 @@ let currentlyTypingClientIds = room.typing.current ```kotlin val currentlyTypingClientIds = room.typing.current ``` + +```jetpack +import androidx.compose.material.* +import androidx.compose.runtime.* +import com.ably.chat.Room +import com.ably.chat.extensions.compose.collectAsCurrentlyTyping + +@Composable +fun CurrentlyTypingComponent(room: Room) { + val currentlyTyping by room.typing.collectAsCurrentlyTyping() + + Text("Currently typing: ${currentlyTyping.joinToString(", ")}") +} +``` diff --git a/src/pages/docs/chat/setup.mdx b/src/pages/docs/chat/setup.mdx index fdb0aafdaf..f5191b4706 100644 --- a/src/pages/docs/chat/setup.mdx +++ b/src/pages/docs/chat/setup.mdx @@ -115,14 +115,19 @@ import AblyChat - + ### Gradle The Ably Chat SDK is available on the Maven Central Repository. To include the dependency in your project, add the following to your `build.gradle.kts` file: ```kotlin - implementation("com.ably.chat:chat:1.0.0") + implementation("com.ably.chat:chat:1.1.0") +``` + +```jetpack +implementation("com.ably.chat:chat:1.1.0") +implementation("com.ably.chat:chat-extensions-compose:1.1.0") ``` @@ -130,7 +135,12 @@ For groovy: ```kotlin - implementation 'com.ably.chat:chat:1.0.0' + implementation 'com.ably.chat:chat:1.1.0' +``` + +```jetpack +implementation 'com.ably.chat:chat:1.1.0' +implementation 'com.ably.chat:chat-extensions-compose:1.1.0' ``` @@ -186,6 +196,21 @@ val realtimeClient = AblyRealtime( }, ) +val chatClient = ChatClient(realtimeClient) +``` + +```jetpack +import com.ably.chat.ChatClient +import io.ably.lib.realtime.AblyRealtime +import io.ably.lib.types.ClientOptions + +val realtimeClient = AblyRealtime( + ClientOptions().apply { + key = "{{API_KEY}}" + clientId = "" + }, +) + val chatClient = ChatClient(realtimeClient) ``` @@ -214,7 +239,7 @@ Additional options can also be passed to the Chat client to customize the follow - + | Property | Description | | -------- | ----------- | @@ -271,13 +296,26 @@ val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Debug } ``` + +```jetpack +val realtimeClient = AblyRealtime( + ClientOptions().apply { + key = "{{API_KEY}}" + clientId = "" + }, +) +val chatClient = ChatClient(realtimeClient) { + logHandler = CustomLogHandler() // Implements com.ably.chat.LogHandler interface + logLevel = LogLevel.Debug +} +``` The `logHandler` property is your own function that will be called for each line of log output generated by the Chat SDK. - + The `logHandler` property is your custom `LogHandler` implementation that will be called for each line of log output generated by the Chat SDK. From 58c09685767119669b5efe0792d0a8bd4da140d9 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 14 Jan 2026 14:35:36 +0530 Subject: [PATCH 02/11] 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 03/11] - 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 520c5c65cfd37ac257b06c52a87dc4a7a584dae8 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 14 Jan 2026 19:18:29 +0530 Subject: [PATCH 04/11] - Fixed snippets as per review comments - Fixed method signatures of jetpack compose extensions - Removed jetpack alias reference to kotlin, instead will use native jetpack icon - Updated ably/ui to jetpack compatible latest version --- package.json | 2 +- src/data/languages/languageInfo.ts | 1 - .../docs/chat/getting-started/android.mdx | 2 +- src/pages/docs/chat/rooms/history.mdx | 8 +++--- .../docs/chat/rooms/message-reactions.mdx | 16 ++++++++++++ src/pages/docs/chat/rooms/messages.mdx | 8 +++--- src/pages/docs/chat/rooms/occupancy.mdx | 4 +-- src/pages/docs/chat/rooms/presence.mdx | 19 +++----------- src/pages/docs/chat/rooms/reactions.mdx | 21 ++++++---------- src/pages/docs/chat/rooms/typing.mdx | 25 +++++++------------ yarn.lock | 18 ++++++------- 11 files changed, 55 insertions(+), 69 deletions(-) diff --git a/package.json b/package.json index 64d36c9b57..730c3cb248 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "validate-llms-txt": "node bin/validate-llms.txt.ts" }, "dependencies": { - "@ably/ui": "17.9.16", + "@ably/ui": "17.11.4", "@codesandbox/sandpack-react": "^2.20.0", "@codesandbox/sandpack-themes": "^2.0.21", "@gfx/zopfli": "^1.0.15", diff --git a/src/data/languages/languageInfo.ts b/src/data/languages/languageInfo.ts index c1ff9c00c6..69f589b8ea 100644 --- a/src/data/languages/languageInfo.ts +++ b/src/data/languages/languageInfo.ts @@ -89,7 +89,6 @@ export default { jetpack: { label: 'Jetpack Compose', syntaxHighlighterKey: 'kotlin', - alias: 'kotlin', }, realtime: { label: 'Realtime', diff --git a/src/pages/docs/chat/getting-started/android.mdx b/src/pages/docs/chat/getting-started/android.mdx index e49c4b51b8..f830241c47 100644 --- a/src/pages/docs/chat/getting-started/android.mdx +++ b/src/pages/docs/chat/getting-started/android.mdx @@ -719,7 +719,7 @@ In your `MainActivity.kt` file, create a new component called `PresenceStatusUi` @Composable fun PresenceStatusUi(room: Room?) { val membersState = room?.collectAsPresenceMembers() - val members by membersState ?: remember { mutableStateOf(emptyList()) } + val members = membersState?.value ?: emptyList() LaunchedEffect(room) { room?.presence?.enter() diff --git a/src/pages/docs/chat/rooms/history.mdx b/src/pages/docs/chat/rooms/history.mdx index 2673214b58..61bf10377f 100644 --- a/src/pages/docs/chat/rooms/history.mdx +++ b/src/pages/docs/chat/rooms/history.mdx @@ -86,13 +86,11 @@ import com.ably.chat.extensions.compose.collectAsPagingMessagesState @Composable fun HistoryComponent(room: Room) { - val pagingMessagesState by room.messages.collectAsPagingMessagesState( - orderBy = OrderBy.NewestFirst - ) + val pagingMessagesState = room.collectAsPagingMessagesState() LazyColumn { - items(pagingMessagesState.messages.size) { index -> - val message = pagingMessagesState.messages[index] + items(pagingMessagesState.loaded.size) { index -> + val message = pagingMessagesState.loaded[index] Text("Message: ${message.text}") } } diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index 6424bb50fa..1ab3277527 100644 --- a/src/pages/docs/chat/rooms/message-reactions.mdx +++ b/src/pages/docs/chat/rooms/message-reactions.mdx @@ -59,6 +59,14 @@ val room = ablyChatClient.rooms.get("room1") { } ``` +```jetpack +val room = ablyChatClient.rooms.get("room1") { + messages { + defaultMessageReactionType = MessageReactionType.Unique + } +} +``` + ```react import { MessageReactionType } from '@ably/chat'; import { ChatRoomProvider } from '@ably/chat/react'; @@ -757,6 +765,14 @@ val room = ablyChatClient.rooms.get("room1") { } ``` +```jetpack +val room = ablyChatClient.rooms.get("room1") { + messages { + rawMessageReactions = true + } +} +``` + ```react import { ChatRoomProvider } from '@ably/chat/react'; diff --git a/src/pages/docs/chat/rooms/messages.mdx b/src/pages/docs/chat/rooms/messages.mdx index 6524ed9b09..31b01fd132 100644 --- a/src/pages/docs/chat/rooms/messages.mdx +++ b/src/pages/docs/chat/rooms/messages.mdx @@ -67,11 +67,11 @@ import com.ably.chat.extensions.compose.collectAsPagingMessagesState @Composable fun MyComponent(room: Room) { - val pagingMessagesState by room.messages.collectAsPagingMessagesState() + val pagingMessagesState = room.collectAsPagingMessagesState() LazyColumn { - items(pagingMessagesState.messages.size) { index -> - val message = pagingMessagesState.messages[index] + items(pagingMessagesState.loaded.size) { index -> + val message = pagingMessagesState.loaded[index] Text("Message: ${message.text}") } } @@ -578,7 +578,7 @@ let deletedMessage = try await room.messages.delete( ```kotlin val messageToDelete: Message -val deletedMessage = room().messages.delete( +val deletedMessage = room.messages.delete( messageToDelete, operationDescription = "Message deleted by user", ) diff --git a/src/pages/docs/chat/rooms/occupancy.mdx b/src/pages/docs/chat/rooms/occupancy.mdx index 620b4893fc..3dc7f0d8b2 100644 --- a/src/pages/docs/chat/rooms/occupancy.mdx +++ b/src/pages/docs/chat/rooms/occupancy.mdx @@ -67,7 +67,7 @@ import com.ably.chat.extensions.compose.collectAsOccupancy @Composable fun OccupancyComponent(room: Room) { - val occupancy by room.occupancy.collectAsOccupancy() + val occupancy by room.collectAsOccupancy() Text("Number of users connected: ${occupancy.connections}") Text("Number of members present: ${occupancy.presenceMembers}") @@ -207,7 +207,7 @@ import com.ably.chat.extensions.compose.collectAsOccupancy @Composable fun CurrentOccupancyComponent(room: Room) { - val occupancy by room.occupancy.collectAsOccupancy() + val occupancy by room.collectAsOccupancy() Text("Connections: ${occupancy.connections}") Text("Presence members: ${occupancy.presenceMembers}") diff --git a/src/pages/docs/chat/rooms/presence.mdx b/src/pages/docs/chat/rooms/presence.mdx index 57958868cb..f43da66bbb 100644 --- a/src/pages/docs/chat/rooms/presence.mdx +++ b/src/pages/docs/chat/rooms/presence.mdx @@ -73,7 +73,7 @@ import com.ably.chat.extensions.compose.collectAsPresenceMembers @Composable fun PresenceComponent(room: Room) { - val presenceMembers by room.presence.collectAsPresenceMembers() + val presenceMembers by room.collectAsPresenceMembers() Text("Present members: ${presenceMembers.size}") presenceMembers.forEach { member -> @@ -115,7 +115,7 @@ You don't need to handle removing listeners, as this is done automatically by th When you unmount the component that is using the `usePresenceListener` hook, it will automatically handle unsubscribing any associated listeners registered for presence status updates. - + ```javascript // Initial subscription @@ -133,17 +133,6 @@ val (unsubscribe) = room.presence.subscribe { event -> println("Presence event ${event.type} from ${event.member.clientId} with data ${event.member.data}") } -// To remove the listener -unsubscribe() -``` - -```jetpack -// Jetpack Compose handles cleanup automatically -// When using subscribe directly: -val (unsubscribe) = room.presence.subscribe { event -> - println("Presence event ${event.type} from ${event.member.clientId} with data ${event.member.data}") -} - // To remove the listener unsubscribe() ``` @@ -216,8 +205,6 @@ import com.ably.chat.json.* @Composable fun EnterPresenceComponent(room: Room) { - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(room) { room.presence.enter( jsonObject { @@ -489,7 +476,7 @@ import com.ably.chat.extensions.compose.collectAsPresenceMembers @Composable fun GetPresenceComponent(room: Room) { - val presentMembers by room.presence.collectAsPresenceMembers() + val presentMembers by room.collectAsPresenceMembers() Text("Total present: ${presentMembers.size}") presentMembers.forEach { member -> diff --git a/src/pages/docs/chat/rooms/reactions.mdx b/src/pages/docs/chat/rooms/reactions.mdx index 4988e267dc..bb17c412b4 100644 --- a/src/pages/docs/chat/rooms/reactions.mdx +++ b/src/pages/docs/chat/rooms/reactions.mdx @@ -84,8 +84,12 @@ The following are the properties of a room reaction event: ### Unsubscribe from room reactions - -Use the `unsubscribe()` function returned in the `subscribe()` response to remove a room reaction listener. Jetpack Compose automatically handles lifecycle and cleanup when using `LaunchedEffect`: + +Use the `unsubscribe()` function returned in the `subscribe()` response to remove a room reaction listener: + + + +Jetpack Compose automatically handles lifecycle and cleanup when using `LaunchedEffect` with `asFlow`. @@ -96,7 +100,7 @@ You don't need to handle removing listeners, as this is done automatically by th When you unmount the component that is using the `useRoomReactions` hook, it will automatically handle unsubscribing any associated listeners registered for room reactions. - + ```javascript // Initial subscription @@ -114,17 +118,6 @@ val (unsubscribe) = room.reactions.subscribe { event -> println("Received a reaction of type ${event.reaction.name}, and metadata ${event.reaction.metadata}") } -// To remove the listener -unsubscribe() -``` - -```jetpack -// Jetpack Compose handles cleanup automatically -// When using subscribe directly: -val (unsubscribe) = room.reactions.subscribe { event -> - println("Received a reaction of type ${event.reaction.name}, and metadata ${event.reaction.metadata}") -} - // To remove the listener unsubscribe() ``` diff --git a/src/pages/docs/chat/rooms/typing.mdx b/src/pages/docs/chat/rooms/typing.mdx index 5d3a0cb67f..c5b0464c0d 100644 --- a/src/pages/docs/chat/rooms/typing.mdx +++ b/src/pages/docs/chat/rooms/typing.mdx @@ -64,7 +64,7 @@ import com.ably.chat.extensions.compose.collectAsCurrentlyTyping @Composable fun TypingComponent(room: Room) { - val currentlyTyping by room.typing.collectAsCurrentlyTyping() + val currentlyTyping by room.collectAsCurrentlyTyping() Text("Currently typing: ${currentlyTyping.joinToString(", ")}") } @@ -105,8 +105,12 @@ You can use the size of the `currentlyTyping` set to decide whether to display i ### Unsubscribe from typing events - -Use the `unsubscribe()` function returned in the `subscribe()` response to remove a typing listener. Jetpack Compose automatically handles lifecycle and cleanup when using `collectAsCurrentlyTyping()`: + +Use the `unsubscribe()` function returned in the `subscribe()` response to remove a typing listener: + + + +Jetpack Compose automatically handles lifecycle and cleanup when using `collectAsCurrentlyTyping()`. @@ -117,7 +121,7 @@ You don't need to handle removing listeners, as this is done automatically by th When you unmount the component that is using the `useTyping` hook, it will automatically handle unsubscribing any associated listeners registered for typing events. - + ```javascript // Initial subscription @@ -136,17 +140,6 @@ val (unsubscribe) = room.typing.subscribe { event -> println("Typing event received: $event") } -// To remove the listener -unsubscribe() -``` - -```jetpack -// Jetpack Compose handles cleanup automatically -// When using subscribe directly: -val (unsubscribe) = room.typing.subscribe { event -> - println("Typing event received: $event") -} - // To remove the listener unsubscribe() ``` @@ -343,7 +336,7 @@ import com.ably.chat.extensions.compose.collectAsCurrentlyTyping @Composable fun CurrentlyTypingComponent(room: Room) { - val currentlyTyping by room.typing.collectAsCurrentlyTyping() + val currentlyTyping by room.collectAsCurrentlyTyping() Text("Currently typing: ${currentlyTyping.joinToString(", ")}") } diff --git a/yarn.lock b/yarn.lock index 5bcab424cf..77f3982a0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@ably/ui@17.9.16": - version "17.9.16" - resolved "https://registry.yarnpkg.com/@ably/ui/-/ui-17.9.16.tgz#609550308d65081e43ffe19787db4a2a10079dc9" - integrity sha512-Me3ywixw2VvYbw/PexPIRHKgL89AzPgDE4KzfFseMYK9DuFV/4plmsnWYWRb08pXKwYrna7JtqmaYYZA8zYVXQ== +"@ably/ui@17.11.4": + version "17.11.4" + resolved "https://registry.yarnpkg.com/@ably/ui/-/ui-17.11.4.tgz#a2b763b201d0bbfae51dab90ee1d8797d3772536" + integrity sha512-0d+sdGW+wiRdNDWBKqk7eea1efstku5Mwu03pN0Ej7jNJEMAh4e1rDBirJvq1MENDzGloTxF62GNbrSAXD9NVw== dependencies: "@heroicons/react" "^2.2.0" "@radix-ui/react-accordion" "^1.2.1" @@ -26,7 +26,7 @@ embla-carousel "^8.6.0" embla-carousel-autoplay "^8.6.0" embla-carousel-react "^8.6.0" - es-toolkit "^1.39.10" + es-toolkit "^1.43.0" highlight.js "^11.11.1" highlightjs-curl "^1.3.0" js-cookie "^3.0.5" @@ -7783,10 +7783,10 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -es-toolkit@^1.39.10: - version "1.39.10" - resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.39.10.tgz#513407af73e79f9940e7ec7650f2e6dceeaf1d81" - integrity sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w== +es-toolkit@^1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.43.0.tgz#2c278d55ffeb30421e6e73a009738ed37b10ef61" + integrity sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA== es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: version "0.10.64" From 719150f9366d9370b5e26e777be4878ac15616b2 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 15 Jan 2026 11:38:24 +0530 Subject: [PATCH 05/11] Fixed chat-swift snippets with correct method/API signature --- src/pages/docs/chat/getting-started/swift.mdx | 8 ++++---- src/pages/docs/chat/rooms/index.mdx | 2 +- src/pages/docs/chat/rooms/messages.mdx | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/docs/chat/getting-started/swift.mdx b/src/pages/docs/chat/getting-started/swift.mdx index 1725519f8e..1d69549e11 100644 --- a/src/pages/docs/chat/getting-started/swift.mdx +++ b/src/pages/docs/chat/getting-started/swift.mdx @@ -345,7 +345,7 @@ struct ContentView: View { guard !newMessage.isEmpty, let room = room else { return } do { - _ = try await room.messages.send(params: .init(text: newMessage)) + _ = try await room.messages.send(withParams: .init(text: newMessage)) newMessage = "" } catch { print("Failed to send message: \(error)") @@ -480,7 +480,7 @@ struct ContentView: View { guard !newMessage.isEmpty, let room = room else { return } do { - _ = try await room.messages.send(params: .init(text: newMessage)) + _ = try await room.messages.send(withParams: .init(text: newMessage)) newMessage = "" } catch { print("Failed to send message: \(error)") @@ -492,7 +492,7 @@ struct ContentView: View { do { let updateMessageParams = UpdateMessageParams(text: newMessage) - _ = try await room.messages.update(withSerial: editingMessage.serial params: updateMessageParams, details: nil) + _ = try await room.messages.update(withSerial: editingMessage.serial, params: updateMessageParams, details: nil) self.editingMessage = nil newMessage = "" } catch { @@ -840,7 +840,7 @@ struct ContentView: View { guard let room = room else { return } do { - try await room.reactions.send(params: .init(name: name)) + try await room.reactions.send(withParams: .init(name: name)) } catch { print("Failed to send reaction: \(error)") } diff --git a/src/pages/docs/chat/rooms/index.mdx b/src/pages/docs/chat/rooms/index.mdx index e4f9338874..eb591ba103 100644 --- a/src/pages/docs/chat/rooms/index.mdx +++ b/src/pages/docs/chat/rooms/index.mdx @@ -104,7 +104,7 @@ const room = await chatClient.rooms.get('basketball-stream', options); ``` ```swift -let presence = PresenceOptions(enter: false) +let presence = PresenceOptions(enableEvents: false) let typing = TypingOptions(heartbeatThrottle: 5.0) // seconds // using defaults for reactions and occupancy let options = RoomOptions(presence: presence, typing: typing, occupancy: .init()) diff --git a/src/pages/docs/chat/rooms/messages.mdx b/src/pages/docs/chat/rooms/messages.mdx index 6524ed9b09..683002d5e4 100644 --- a/src/pages/docs/chat/rooms/messages.mdx +++ b/src/pages/docs/chat/rooms/messages.mdx @@ -201,7 +201,7 @@ const MyComponent = () => { ``` ```swift -let message = try await room.messages.send(params: .init(text: "hello")) +let message = try await room.messages.send(withParams: .init(text: "hello")) ``` ```kotlin @@ -323,7 +323,7 @@ const MyComponent = () => { ```swift let originalMessage: Message let updatedMessage = try await room.messages.update( - forSerial: originalMessage.serial, + withSerial: originalMessage.serial, params: .init(text: "my updated text"), details: .init(description: "Message update by user") ) @@ -571,8 +571,8 @@ const MyComponent = () => { ```swift let messageToDelete: Message let deletedMessage = try await room.messages.delete( - forSerial: messageToDelete.serial, - params: .init(description: "Message deleted by user") + withSerial: messageToDelete.serial, + details: .init(description: "Message deleted by user") ) ``` From 373a789e49a2764eec8b8392437f9f0c3d41a425 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 15 Jan 2026 12:07:18 +0530 Subject: [PATCH 06/11] Fixed chat-js snippets for react and react-native --- src/pages/docs/chat/getting-started/react-native.mdx | 2 +- src/pages/docs/chat/getting-started/react.mdx | 2 +- src/pages/docs/chat/rooms/reactions.mdx | 2 ++ src/pages/docs/chat/setup.mdx | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/docs/chat/getting-started/react-native.mdx b/src/pages/docs/chat/getting-started/react-native.mdx index f51daa7034..e06c4c6a38 100644 --- a/src/pages/docs/chat/getting-started/react-native.mdx +++ b/src/pages/docs/chat/getting-started/react-native.mdx @@ -824,7 +824,7 @@ You can also use the Ably CLI to enter the room from another client by running t ## Step 9: Send a reaction Clients can send a reaction to a room to show their sentiment for what is happening, such as a point being scored in a sports game. -Ably Chat provides a [`useReactions()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useReactions.html) hook to send and receive reactions in a room. These are short-lived (ephemeral) and are not stored in the room history. +Ably Chat provides a [`useRoomReactions()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useRoomReactions.html) hook to send and receive reactions in a room. These are short-lived (ephemeral) and are not stored in the room history. In your `ChatApp.tsx` file, add a new component called `ReactionComponent`, like so: diff --git a/src/pages/docs/chat/getting-started/react.mdx b/src/pages/docs/chat/getting-started/react.mdx index 1be877d3b6..041442bd72 100644 --- a/src/pages/docs/chat/getting-started/react.mdx +++ b/src/pages/docs/chat/getting-started/react.mdx @@ -783,7 +783,7 @@ You can also use the Ably CLI to enter the room from another client by running t ## Step 9: Send a room reaction Clients can send a reaction to a room to show their sentiment for what is happening, such as a point being scored in a sports game. -Ably Chat provides a [`useReactions()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useReactions.html) hook to send and receive reactions in a room. These are short-lived (ephemeral) and are not stored in the room history. +Ably Chat provides a [`useRoomReactions()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.useRoomReactions.html) hook to send and receive reactions in a room. These are short-lived (ephemeral) and are not stored in the room history. In your `src/App.tsx` file, add a new component called `ReactionComponent`, like so: diff --git a/src/pages/docs/chat/rooms/reactions.mdx b/src/pages/docs/chat/rooms/reactions.mdx index 4988e267dc..1fbef0fae5 100644 --- a/src/pages/docs/chat/rooms/reactions.mdx +++ b/src/pages/docs/chat/rooms/reactions.mdx @@ -35,6 +35,8 @@ const MyComponent = () => { console.log('Received reaction: ', reactionEvent.reaction); }, }); + + return
Room Reactions Component
; }; ``` diff --git a/src/pages/docs/chat/setup.mdx b/src/pages/docs/chat/setup.mdx index f5191b4706..fd584e129f 100644 --- a/src/pages/docs/chat/setup.mdx +++ b/src/pages/docs/chat/setup.mdx @@ -266,7 +266,7 @@ import { ChatClientProvider } from '@ably/chat/react'; const ably = new Ably.Realtime({ key: '{{API_KEY}}', clientId: ''}); const chatClient = new ChatClient(ably, {logHandler: logWriteFunc, logLevel: 'debug' }); -const App = => { +const App = () => { return ( From aa974ef2cc0ae32374c94c2608b7efdd937b656c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 15 Jan 2026 20:02:51 +0530 Subject: [PATCH 07/11] Fixed react specific chat-js code snippets --- src/pages/docs/chat/react-ui-kit/components.mdx | 12 ++++++------ src/pages/docs/chat/rooms/media.mdx | 12 ++++++------ src/pages/docs/chat/rooms/presence.mdx | 16 ++++++++-------- src/pages/docs/chat/rooms/replies.mdx | 8 ++++---- src/pages/docs/chat/rooms/typing.mdx | 14 +++++++------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/pages/docs/chat/react-ui-kit/components.mdx b/src/pages/docs/chat/react-ui-kit/components.mdx index b4fdce3818..6542cec8fb 100644 --- a/src/pages/docs/chat/react-ui-kit/components.mdx +++ b/src/pages/docs/chat/react-ui-kit/components.mdx @@ -303,11 +303,11 @@ import { MessageInput } from '@ably/chat-react-ui-kit'; import { useMessages } from '@ably/chat/react'; // Basic usage -const { send } = useMessages(); +const { sendMessage } = useMessages(); const handleSendMessage = (text: string) => { console.log(`Sending message: ${text}`); - send({ text }); + sendMessage({ text }); }; { console.log(`Editing message with serial: ${message.serial}, setting text to: ${newText}`); - update(message.serial, { text: newText }); + updateMessage(message.serial, { text: newText }); }} onDelete={(message: Message) => { console.log(`Deleting message with serial: ${message.serial}`); @@ -546,11 +546,11 @@ const { update, deleteMessage, addReaction, removeReaction } = useMessages(); }} onReactionAdd={(message: Message, emoji: string) => { console.log(`Adding reaction ${emoji} to message with serial: ${message.serial}`); - addReaction(message.serial, emoji); + sendReaction(message.serial, { name: emoji }); }} onReactionRemove={(message: Message, emoji: string) => { console.log(`Removing reaction ${emoji} from message with serial: ${message.serial}`); - removeReaction(message.serial, emoji); + deleteReaction(message.serial, { name: emoji }); }} /> ``` diff --git a/src/pages/docs/chat/rooms/media.mdx b/src/pages/docs/chat/rooms/media.mdx index ca117e232e..364b35dbd3 100644 --- a/src/pages/docs/chat/rooms/media.mdx +++ b/src/pages/docs/chat/rooms/media.mdx @@ -237,14 +237,14 @@ import { useMessages } from '@ably/chat/react'; const MessageSender = () => { const [mediaToAttach, setMediaToAttach] = useState([]); const [messageText, setMessageText] = useState(''); - const { send } = useMessages(); + const { sendMessage } = useMessages(); const handleSend = async () => { let metadata = {}; if (mediaToAttach.length > 0) { metadata["media"] = mediaToAttach; } - await send({ + await sendMessage({ text: messageText, metadata: metadata }); @@ -708,7 +708,7 @@ room.messages.update(message.serial, message.copy({metadata: newMetadata})) import { useMessages } from '@ably/chat/react'; const AddMediaToMessage = ({ message }) => { - const { update } = useMessages(); + const { updateMessage } = useMessages(); const addMediaToMessage = async () => { const mediaId = 'abcd123abcd'; // assume this is the media we want to add @@ -719,7 +719,7 @@ const AddMediaToMessage = ({ message }) => { } newMetadata.media.push(mediaId); - await update(message.serial, message.copy({metadata: newMetadata})); + await updateMessage(message.serial, message.copy({metadata: newMetadata})); }; return ( @@ -867,7 +867,7 @@ room.messages.update(message.serial, message.copy({metadata: newMetadata})) import { useMessages } from '@ably/chat/react'; const RemoveMediaFromMessage = ({ message }) => { - const { update } = useMessages(); + const { updateMessage } = useMessages(); const removeMediaFromMessage = async () => { const mediaId = 'abcd123abcd'; // assume this is the media we want to remove @@ -880,7 +880,7 @@ const RemoveMediaFromMessage = ({ message }) => { const newMetadata = structuredClone(message.metadata); newMetadata.media = newMetadata.media.filter(id => mediaId !== id); - await update(message.serial, message.copy({metadata: newMetadata})); + await updateMessage(message.serial, message.copy({metadata: newMetadata})); }; return ( diff --git a/src/pages/docs/chat/rooms/presence.mdx b/src/pages/docs/chat/rooms/presence.mdx index 57958868cb..bcae26a631 100644 --- a/src/pages/docs/chat/rooms/presence.mdx +++ b/src/pages/docs/chat/rooms/presence.mdx @@ -186,11 +186,11 @@ const MyComponent = () => { // By default, presence will be entered when the hook mounts and left // when it subsequently unmounts. - const { isPresent } = usePresence(presenceParams); + const { myPresenceState } = usePresence(presenceParams); return (
-
Presence status: {isPresent ? 'Online' : 'Offline'}
+
Presence status: {myPresenceState.present ? 'Online' : 'Offline'}
); }; @@ -251,7 +251,7 @@ const MyComponent = () => { initialData: { status: 'Online' }, }; - const { update, isPresent } = usePresence(presenceParams); + const { update, myPresenceState } = usePresence(presenceParams); const updatePresence = () => { update({ status: 'Away' }); @@ -259,7 +259,7 @@ const MyComponent = () => { return (
-
Presence status: {isPresent ? 'Online' : 'Offline'}
+
Presence status: {myPresenceState.present ? 'Online' : 'Offline'}
); @@ -332,11 +332,11 @@ const MyComponent = () => { }; // Call leave explicitly to disable auto-entry and leave presence - const { isPresent, leave } = usePresence(presenceParams); + const { myPresenceState, leave } = usePresence(presenceParams); return (
-
Presence status: {isPresent ? 'Online' : 'Offline'}
+
Presence status: {myPresenceState.present ? 'Online' : 'Offline'}
); }; @@ -405,7 +405,7 @@ For various reasons, you may wish to take manual control of presence (as you do autoEnterLeave: false, }; - const { isPresent, leave, enter, update } = usePresence(presenceParams); + const { myPresenceState, leave, enter, update } = usePresence(presenceParams); // Manual mount behavior - enter presence when component mounts useEffect(() => { @@ -425,7 +425,7 @@ For various reasons, you may wish to take manual control of presence (as you do return (
-
Presence status: {isPresent ? 'Online' : 'Offline'}
+
Presence status: {myPresenceState.present ? 'Online' : 'Offline'}
); diff --git a/src/pages/docs/chat/rooms/replies.mdx b/src/pages/docs/chat/rooms/replies.mdx index 2224e8d72a..0565f13588 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -20,7 +20,7 @@ async function sendReply(replyToMessage, replyText) { const metadata = { reply: { serial: replyToMessage.serial, - timestamp: replyToMessage.createdAt.getTime(), + timestamp: replyToMessage.timestamp.getTime(), clientId: replyToMessage.clientId, previewText: replyToMessage.text.substring(0, 140) } @@ -43,7 +43,7 @@ const ReplyComponent = ({ messageToReplyTo }) => { const metadata = { reply: { serial: messageToReplyTo.serial, - createdAt: messageToReplyTo.createdAt.getTime(), + timestamp: messageToReplyTo.timestamp.getTime(), clientId: messageToReplyTo.clientId, previewText: messageToReplyTo.text.substring(0, 140) } @@ -150,7 +150,7 @@ When a user replies to a message, extract and store the parent message details: function prepareReply(parentMessage) { return { serial: parentMessage.serial, - createdAt: parentMessage.createdAt.getTime(), + timestamp: parentMessage.timestamp.getTime(), clientId: parentMessage.clientId, previewText: parentMessage.text.substring(0, 140) }; @@ -161,7 +161,7 @@ function prepareReply(parentMessage) { const prepareReply = (parentMessage) => { return { serial: parentMessage.serial, - createdAt: parentMessage.createdAt.getTime(), + timestamp: parentMessage.timestamp.getTime(), clientId: parentMessage.clientId, previewText: parentMessage.text.substring(0, 140) }; diff --git a/src/pages/docs/chat/rooms/typing.mdx b/src/pages/docs/chat/rooms/typing.mdx index 5d3a0cb67f..a2479fc9aa 100644 --- a/src/pages/docs/chat/rooms/typing.mdx +++ b/src/pages/docs/chat/rooms/typing.mdx @@ -27,7 +27,7 @@ import { useTyping } from '@ably/chat/react'; import { TypingSetEvent } from '@ably/chat'; const MyComponent = () => { - const {currentlyTyping, error } = useTyping({ + const { currentlyTyping, roomError } = useTyping({ listener: (typingEvent: TypingSetEvent) => { console.log('Typing event received: ', typingEvent); }, @@ -35,8 +35,8 @@ const MyComponent = () => { return (
- {error &&

Typing Error: {error.message}

} -

Currently typing: {Array.from(currentlyTyping).join(', ')}

+ {roomError &&

Typing Error: {roomError.message}

} +

Currently typing: {Array.from(currentlyTyping).join(', ')}

); }; @@ -172,14 +172,14 @@ await room.typing.keystroke(); import { useTyping } from '@ably/chat/react'; const MyComponent = () => { - const { keystroke, currentlyTyping, error } = useTyping(); + const { keystroke, currentlyTyping, roomError } = useTyping(); const handleKeystrokeClick = () => { keystroke(); }; return (
- {error &&

Typing Error: {error.message}

} + {roomError &&

Typing Error: {roomError.message}

}

Currently typing: {Array.from(currentlyTyping).join(', ')}

@@ -237,14 +237,14 @@ await room.typing.stop(); import { useTyping } from '@ably/chat/react'; const MyComponent = () => { - const { stop, error } = useTyping(); + const { stop, roomError } = useTyping(); const handleStopClick = () => { stop(); }; return (
- {error &&

Typing Error: {error.message}

} + {roomError &&

Typing Error: {roomError.message}

}
); From a6181a122a3a5fe72651a237c21a19abedb000f3 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 15 Jan 2026 19:05:12 +0530 Subject: [PATCH 08/11] - 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") } From 3352a1625b408ff2c883fb3c76eaae6af9348571 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 20 Jan 2026 19:58:17 +0530 Subject: [PATCH 09/11] Optimized snippet imports and addressed feedback on usage of jetpack compose stateful annotations --- src/pages/docs/chat/rooms/media.mdx | 71 +++++++++------------------ src/pages/docs/chat/rooms/replies.mdx | 22 +++------ 2 files changed, 31 insertions(+), 62 deletions(-) diff --git a/src/pages/docs/chat/rooms/media.mdx b/src/pages/docs/chat/rooms/media.mdx index b521af0e91..6718164065 100644 --- a/src/pages/docs/chat/rooms/media.mdx +++ b/src/pages/docs/chat/rooms/media.mdx @@ -27,7 +27,7 @@ The following is an example of an upload function: ```javascript async function uploadMedia() { - // ask the user to choose their media + // ask the user to choose their media asynchronously // upload the media to your storage service // return a unique identifier for the media @@ -46,7 +46,7 @@ async function uploadMedia() { ```swift func uploadMedia() async -> JSONObject { - // ask the user to choose their media + // ask the user to choose their media asynchronously // upload the media to your storage service // return a unique identifier for the media @@ -69,11 +69,10 @@ func uploadMedia() async -> JSONObject { ``` ```kotlin -import com.ably.chat.json.JsonObject -import com.ably.chat.json.jsonObject +import com.ably.chat.json.* suspend fun uploadMedia(): JsonObject { - // ask the user to choose their media + // ask the user to choose their media asynchronously // upload the media to your storage service // return a unique identifier for the media @@ -96,11 +95,10 @@ suspend fun uploadMedia(): JsonObject { ``` ```jetpack -import com.ably.chat.json.JsonObject -import com.ably.chat.json.jsonObject +import com.ably.chat.json.* suspend fun uploadMedia(): JsonObject { - // ask the user to choose their media + // ask the user to choose their media asynchronously // upload the media to your storage service // return a unique identifier for the media @@ -180,21 +178,19 @@ 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 com.ably.chat.json.* import kotlinx.coroutines.launch @Composable fun MediaAttachmentComponent() { - var mediaToAttach by remember { mutableStateOf>(emptyList()) } + val mediaToAttach = remember { mutableStateListOf() } val coroutineScope = rememberCoroutineScope() Column { Button(onClick = { coroutineScope.launch { val mediaData = uploadMedia() - mediaToAttach = mediaToAttach + mediaData + mediaToAttach += mediaData } }) { Text("Attach Media") @@ -291,9 +287,7 @@ func send(text: String, mediaToAttach: [JSONObject]) async throws { ``` ```kotlin -import com.ably.chat.json.JsonObject -import com.ably.chat.json.JsonArray -import com.ably.chat.json.jsonObject +import com.ably.chat.json.* suspend fun send(text: String, mediaToAttach: List) { val metadata = if (mediaToAttach.isNotEmpty()) { @@ -316,16 +310,12 @@ 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 com.ably.chat.json.* import kotlinx.coroutines.launch @Composable fun MessageSenderComponent(room: Room) { - var mediaToAttach by remember { mutableStateOf>(emptyList()) } + val mediaToAttach = remember { mutableStateListOf() } var messageText by remember { mutableStateOf("") } val coroutineScope = rememberCoroutineScope() @@ -339,7 +329,7 @@ fun MessageSenderComponent(room: Room) { Button(onClick = { coroutineScope.launch { val mediaData = uploadMedia() - mediaToAttach = mediaToAttach + mediaData + mediaToAttach += mediaData } }) { Text("Attach Media") @@ -358,7 +348,7 @@ fun MessageSenderComponent(room: Room) { metadata = metadata ) - mediaToAttach = emptyList() + mediaToAttach.clear() messageText = "" } }) { @@ -440,9 +430,7 @@ func getValidMedia(message: Message) -> [JSONObject] { ``` ```kotlin -import com.ably.chat.json.JsonObject -import com.ably.chat.json.JsonArray -import com.ably.chat.json.JsonString +import com.ably.chat.json.* // assume IDs are 10-15 characters long and alphanumeric val mediaIdRegex = Regex("^[a-z0-9]{10,15}$") @@ -465,9 +453,7 @@ fun getValidMedia(message: Message): List { ```jetpack import com.ably.chat.Message -import com.ably.chat.json.JsonObject -import com.ably.chat.json.JsonArray -import com.ably.chat.json.JsonString +import com.ably.chat.json.* // assume IDs are 10-15 characters long and alphanumeric val mediaIdRegex = Regex("^[a-z0-9]{10,15}$") @@ -589,7 +575,7 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView -import com.ably.chat.json.JsonString +import com.ably.chat.json.* fun createMessageView(message: Message, context: android.content.Context): View { val container = LinearLayout(context).apply { @@ -727,9 +713,7 @@ func addMediaToMessage(message: Message, mediaData: JSONObject) async throws { ``` ```kotlin -import com.ably.chat.json.JsonObject -import com.ably.chat.json.JsonArray -import com.ably.chat.json.jsonObject +import com.ably.chat.json.* suspend fun addMediaToMessage(message: Message, mediaData: JsonObject) { val existingMedia = message.metadata["media"] as? JsonArray ?: JsonArray(emptyList()) @@ -756,10 +740,8 @@ suspend fun addMediaToMessage(message: Message, mediaData: JsonObject) { ```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 com.ably.chat.* +import com.ably.chat.json.* import kotlinx.coroutines.launch @Composable @@ -874,10 +856,7 @@ func removeMediaFromMessage(message: Message, mediaIdToRemove: String) async thr ``` ```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 +import com.ably.chat.json.* suspend fun removeMediaFromMessage(message: Message, mediaIdToRemove: String) { val existingMedia = message.metadata["media"] as? JsonArray @@ -912,12 +891,8 @@ suspend fun removeMediaFromMessage(message: Message, mediaIdToRemove: String) { ```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 com.ably.chat.* +import com.ably.chat.json.* import kotlinx.coroutines.launch @Composable diff --git a/src/pages/docs/chat/rooms/replies.mdx b/src/pages/docs/chat/rooms/replies.mdx index 577fe2dd98..2a5db394d1 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -244,8 +244,7 @@ func fetchParentMessage(replyData: JSONObject) async throws -> Message { ``` ```kotlin -import com.ably.chat.json.JsonObject -import com.ably.chat.json.JsonString +import com.ably.chat.json.* suspend fun fetchParentMessage(replyData: JsonObject): Message { val serial = (replyData["serial"] as? JsonString)?.value @@ -257,10 +256,8 @@ suspend fun fetchParentMessage(replyData: JsonObject): Message { ```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 +import com.ably.chat.* +import com.ably.chat.json.* @Composable fun FetchParentMessageComponent(room: Room, replyData: JsonObject) { @@ -365,8 +362,7 @@ room.messages.subscribe { event in ``` ```kotlin -import com.ably.chat.json.JsonObject -import com.ably.chat.json.JsonString +import com.ably.chat.json.* // Subscribe to messages and handle replies val localMessages = mutableListOf() @@ -397,18 +393,16 @@ room.messages.subscribe { event -> 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 +import com.ably.chat.* +import com.ably.chat.json.* @Composable fun MessageListComponent(room: Room) { - var messages by remember { mutableStateOf>(emptyList()) } + val messages = remember { mutableStateListOf() } DisposableEffect(room) { val (unsubscribe) = room.messages.subscribe { event -> - messages = messages + event.message + messages += event.message } onDispose { From efefab57be98d5dbc64b2279a95d89ff32669c2e Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Tue, 20 Jan 2026 20:23:01 +0530 Subject: [PATCH 10/11] Simplified swift code for chat media and replies based on review comments --- src/pages/docs/chat/rooms/media.mdx | 49 +++++++------------ .../docs/chat/rooms/message-reactions.mdx | 2 +- src/pages/docs/chat/rooms/replies.mdx | 37 +++++++++----- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/pages/docs/chat/rooms/media.mdx b/src/pages/docs/chat/rooms/media.mdx index 6718164065..b81084c93a 100644 --- a/src/pages/docs/chat/rooms/media.mdx +++ b/src/pages/docs/chat/rooms/media.mdx @@ -276,10 +276,11 @@ const MessageSender = () => { func send(text: String, mediaToAttach: [JSONObject]) async throws { var metadata: MessageMetadata = [:] if !mediaToAttach.isEmpty { + // Convert each JSONObject to JSONValue.object, then wrap in JSONValue.array metadata["media"] = .array(mediaToAttach.map { .object($0) }) } - try await room.messages.send(withParams: .init( + _ = try await room.messages.send(withParams: .init( text: text, metadata: metadata )) @@ -290,6 +291,7 @@ func send(text: String, mediaToAttach: [JSONObject]) async throws { import com.ably.chat.json.* suspend fun send(text: String, mediaToAttach: List) { + // Wrap the media list in a JsonArray for the metadata val metadata = if (mediaToAttach.isNotEmpty()) { jsonObject { put("media", JsonArray(mediaToAttach)) @@ -406,24 +408,19 @@ const getValidMedia = (message) => { import Foundation // assume IDs are 10-15 characters long and alphanumeric -let mediaIdRegex = try! NSRegularExpression(pattern: "^[a-z0-9]{10,15}$") +let mediaIdRegex = /^[a-z0-9]{10,15}$/ func getValidMedia(message: Message) -> [JSONObject] { - guard case let .array(mediaArray) = message.metadata["media"] else { + guard let mediaArray = message.metadata["media"]?.arrayValue else { return [] } - return mediaArray.compactMap { mediaValue -> JSONObject? in - guard case let .object(mediaObj) = mediaValue, - case let .string(id) = mediaObj["id"] else { + return mediaArray.compactMap { mediaValue in + guard let mediaObj = mediaValue.objectValue, + let id = mediaObj["id"]?.stringValue, + id.wholeMatch(of: mediaIdRegex) != nil 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 } } @@ -551,10 +548,9 @@ struct MessageView: View { 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"] { + ForEach(validMedia, id: \.self) { media in + if let id = media["id"]?.stringValue, + let title = media["title"]?.stringValue { AsyncImage(url: URL(string: "https://example.com/images/\(id)")) { image in image.resizable().aspectRatio(contentMode: .fit) } placeholder: { @@ -690,18 +686,12 @@ const AddMediaToMessage = ({ message }) => { 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 = [] - } - + // Get existing media array or start with empty array + var mediaArray = newMetadata["media"]?.arrayValue ?? [] mediaArray.append(.object(mediaData)) - newMetadata["media"] = .array(mediaArray) - try await room.messages.update( + _ = try await room.messages.update( withSerial: message.serial, params: .init( text: message.text, @@ -827,18 +817,15 @@ const RemoveMediaFromMessage = ({ message }) => { ```swift func removeMediaFromMessage(message: Message, mediaIdToRemove: String) async throws { - guard case let .array(mediaArray) = message.metadata["media"], + guard let mediaArray = message.metadata["media"]?.arrayValue, !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 + // Keep items that don't match the ID to remove + mediaValue.objectValue?["id"]?.stringValue != mediaIdToRemove } var newMetadata = message.metadata diff --git a/src/pages/docs/chat/rooms/message-reactions.mdx b/src/pages/docs/chat/rooms/message-reactions.mdx index bf46139751..5537c8e814 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(withParams: .init(limit: 50))).items +var messages = (try 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 2a5db394d1..673b96b8b0 100644 --- a/src/pages/docs/chat/rooms/replies.mdx +++ b/src/pages/docs/chat/rooms/replies.mdx @@ -235,11 +235,10 @@ const FetchParentMessage = ({ replyData }) => { ```swift func fetchParentMessage(replyData: JSONObject) async throws -> Message { - guard case let .string(serial) = replyData["serial"] else { + guard let serial = replyData["serial"]?.stringValue else { throw NSError(domain: "ReplyError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid serial"]) } - let message = try await room.messages.get(withSerial: serial) - return message + return try await room.messages.get(withSerial: serial) } ``` @@ -338,21 +337,33 @@ const MessageList = () => { ``` ```swift +// Extension to extract reply data from a message +extension Message { + var replySerial: String? { + metadata["reply"]?.objectValue?["serial"]?.stringValue + } + + var replyPreview: (clientId: String, text: String)? { + guard let replyData = metadata["reply"]?.objectValue, + let clientId = replyData["clientId"]?.stringValue, + let previewText = replyData["previewText"]?.stringValue else { + return nil + } + return (clientId, previewText) + } +} + // Subscribe to messages and handle replies var localMessages: [Message] = [] -room.messages.subscribe { event in +for await event in room.messages.subscribe() { 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)") - } + if let replySerial = message.replySerial { + if let parentMessage = localMessages.first(where: { $0.serial == replySerial }) { + print("Reply to \(parentMessage.clientID): \(parentMessage.text)") + } else if let preview = message.replyPreview { + print("Reply to \(preview.clientId): \(preview.text)") } } From bbafc53f02c17aa1cb824536505165bad9732313 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 23 Jan 2026 18:13:58 +0530 Subject: [PATCH 11/11] Fixed unnecessary changes introduced by merge commit --- src/pages/docs/chat/rooms/messages.mdx | 2 +- src/pages/docs/chat/rooms/occupancy.mdx | 4 ++-- src/pages/docs/chat/rooms/presence.mdx | 21 ++------------------- src/pages/docs/chat/rooms/reactions.mdx | 6 +++--- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/pages/docs/chat/rooms/messages.mdx b/src/pages/docs/chat/rooms/messages.mdx index b1064fcb77..afd3dd37e7 100644 --- a/src/pages/docs/chat/rooms/messages.mdx +++ b/src/pages/docs/chat/rooms/messages.mdx @@ -231,7 +231,7 @@ fun MyComponent(room: Room) { ## Get a single message -Use the [`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#get)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/get%28withserial:%29)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/get.html) method to get a message in a chat room using message serial. +Use the [`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Messages.html#get)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/messages/get%28withserial:%29)[`messages.get()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages/get.html) method to get a message in a chat room using message serial. diff --git a/src/pages/docs/chat/rooms/occupancy.mdx b/src/pages/docs/chat/rooms/occupancy.mdx index a54783f084..b789286436 100644 --- a/src/pages/docs/chat/rooms/occupancy.mdx +++ b/src/pages/docs/chat/rooms/occupancy.mdx @@ -11,8 +11,8 @@ Occupancy generates messages on any client entering/leaving a room, and so incre ## Subscribe to room occupancy - -Subscribe to a room's occupancy by registering a listener. Occupancy events are emitted whenever the number of online users within a room changes. Use the [`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Occupancy.html#subscribe)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/occupancy/subscribe%28%29-3loon)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-occupancy/subscribe.html)[`collectAsOccupancy()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-occupancy.html) method in a room to receive updates: + +Subscribe to a room's occupancy by registering a listener. Occupancy events are emitted whenever the number of online users within a room changes. Use the [`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Occupancy.html#subscribe)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/occupancy/subscribe%28%29-3loon)[`occupancy.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-occupancy/subscribe.html) method in a room to receive updates: diff --git a/src/pages/docs/chat/rooms/presence.mdx b/src/pages/docs/chat/rooms/presence.mdx index 604d49b214..51ecc56a2d 100644 --- a/src/pages/docs/chat/rooms/presence.mdx +++ b/src/pages/docs/chat/rooms/presence.mdx @@ -7,8 +7,8 @@ The presence feature of a chat room enables online users to advertise to others ## Subscribe to presence - -Subscribe to users' presence status by registering a listener. Presence events are emitted whenever a member enters or leaves the presence set, or updates their user data. Use the [`presence.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#subscribe)[`presence.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/subscribe%28_%3A%29)[`presence.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/subscribe.html)[`collectAsPresenceMembers()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/jetpack/chat-extensions-compose/com.ably.chat.extensions.compose/collect-as-presence-members.html) method in a room to receive updates: + +Subscribe to users' presence status by registering a listener. Presence events are emitted whenever a member enters or leaves the presence set, or updates their user data. Use the [`presence.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.Presence.html#subscribe)[`presence.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/presence/subscribe%28_%3A%29)[`presence.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-presence/subscribe.html) method in a room to receive updates: @@ -83,23 +83,6 @@ import androidx.compose.runtime.* import com.ably.chat.Room import com.ably.chat.extensions.compose.collectAsPresenceMembers -@Composable -fun PresenceComponent(room: Room) { - val presenceMembers by room.collectAsPresenceMembers() - - Text("Present members: ${presenceMembers.size}") - presenceMembers.forEach { member -> - Text("${member.clientId} is present with data: ${member.data}") - } -} -``` - -```jetpack -import androidx.compose.material.* -import androidx.compose.runtime.* -import com.ably.chat.Room -import com.ably.chat.extensions.compose.collectAsPresenceMembers - @Composable fun PresenceComponent(room: Room) { val presenceMembers by room.collectAsPresenceMembers() diff --git a/src/pages/docs/chat/rooms/reactions.mdx b/src/pages/docs/chat/rooms/reactions.mdx index 680decebb0..0d2c8fd2e0 100644 --- a/src/pages/docs/chat/rooms/reactions.mdx +++ b/src/pages/docs/chat/rooms/reactions.mdx @@ -9,8 +9,8 @@ Room reactions are ephemeral and not stored or aggregated by Ably. The intention ## Subscribe to room reactions - -Subscribe to room reactions by registering a listener. Use the [`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomReactions.html#subscribe)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/subscribe%28%29-64gdf)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-reactions/subscribe.html) method in a room to receive reactions: + +Subscribe to room reactions by registering a listener. Use the [`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/interfaces/chat-js.RoomReactions.html#subscribe)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-swift/main/AblyChat/documentation/ablychat/roomreactions/subscribe%28%29-64gdf)[`reactions.subscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-room-reactions/subscribe.html) method in a room to receive reactions: @@ -39,7 +39,7 @@ const MyComponent = () => { console.log('Received reaction: ', reactionEvent.reaction); }, }); - + return
Room Reactions Component
; }; ```