From 0ee8e8076277c80ce2c27a68fa2ef9aeac2c4f7d Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 9 Sep 2024 10:28:29 +0100 Subject: [PATCH 1/3] feat: use Cancellation object instead of unsubscribe methods It's proven to be easier to manage to implement when subscription return object that you can use to unsubscribe --- .gitignore | 1 + .../main/java/com/ably/chat/Cancellation.kt | 12 ++++++++ .../java/com/ably/chat/ConnectionStatus.kt | 8 +---- .../com/ably/chat/EmitsDiscontinuities.kt | 8 +---- .../src/main/java/com/ably/chat/Messages.kt | 29 +++++-------------- .../src/main/java/com/ably/chat/Occupancy.kt | 21 ++------------ .../src/main/java/com/ably/chat/Presence.kt | 20 ++----------- .../main/java/com/ably/chat/RoomReactions.kt | 19 ++---------- .../src/main/java/com/ably/chat/RoomStatus.kt | 7 +---- .../src/main/java/com/ably/chat/Typing.kt | 19 ++---------- 10 files changed, 36 insertions(+), 108 deletions(-) create mode 100644 chat-android/src/main/java/com/ably/chat/Cancellation.kt diff --git a/.gitignore b/.gitignore index 4544f872..92c9f86a 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,7 @@ publish.properties /.idea/compiler.xml /.idea/jarRepositories.xml /.idea/misc.xml +/.idea/shelf # general **/.DS_Store diff --git a/chat-android/src/main/java/com/ably/chat/Cancellation.kt b/chat-android/src/main/java/com/ably/chat/Cancellation.kt new file mode 100644 index 00000000..2357a1a2 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Cancellation.kt @@ -0,0 +1,12 @@ +package com.ably.chat + +/** + * A cancellation handle, returned by various functions (mostly subscriptions) + * where cancellation is required. + */ +fun interface Cancellation { + /** + * Handle cancellation (unsubscribe listeners, clean up) + */ + fun cancel() +} diff --git a/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt b/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt index 6858f507..c9cc8061 100644 --- a/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt +++ b/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt @@ -25,13 +25,7 @@ interface ConnectionStatus { * Registers a listener that will be called whenever the connection status changes. * @param listener The function to call when the status changes. */ - fun on(listener: Listener) - - /** - * Unregisters a listener - * @param listener The function to call when the status changes. - */ - fun off(listener: Listener) + fun on(listener: Listener): Cancellation /** * An interface for listening to changes for the connection status diff --git a/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt b/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt index 10ffac63..6606bc29 100644 --- a/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt +++ b/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt @@ -10,13 +10,7 @@ interface EmitsDiscontinuities { * Register a listener to be called when a discontinuity is detected. * @param listener The listener to be called when a discontinuity is detected. */ - fun onDiscontinuity(listener: Listener) - - /** - * Unregister a listener to be called when a discontinuity is detected. - * @param listener The listener - */ - fun offDiscontinuity(listener: Listener) + fun onDiscontinuity(listener: Listener): Cancellation /** * An interface for listening when discontinuity happens diff --git a/chat-android/src/main/java/com/ably/chat/Messages.kt b/chat-android/src/main/java/com/ably/chat/Messages.kt index 854b147b..edad9b55 100644 --- a/chat-android/src/main/java/com/ably/chat/Messages.kt +++ b/chat-android/src/main/java/com/ably/chat/Messages.kt @@ -3,7 +3,6 @@ package com.ably.chat import io.ably.lib.realtime.Channel -import io.ably.lib.types.PaginatedResult /** * This interface is used to interact with messages in a chat room: subscribing @@ -24,13 +23,7 @@ interface Messages : EmitsDiscontinuities { * @param listener callback that will be called * @returns A response object that allows you to control the subscription. */ - fun subscribe(listener: Listener) - - /** - * Unsubscribe listener - * @param listener callback that will be unsubscribed - */ - fun unsubscribe(listener: Listener) + fun subscribe(listener: Listener): MessagesSubscription /** * Get messages that have been previously sent to the chat room, based on the provided options. @@ -176,6 +169,10 @@ data class SendMessageParams( val headers: MessageHeaders? = null, ) +interface MessagesSubscription: Cancellation { + suspend fun getPreviousMessages(queryOptions: QueryOptions): PaginatedResult +} + class DefaultMessages( private val roomId: String, private val realtimeClient: RealtimeClient, @@ -190,25 +187,15 @@ class DefaultMessages( override val channel: Channel get() = realtimeClient.channels.get(messagesChannelName, ChatChannelOptions()) - override fun subscribe(listener: Messages.Listener) { + override fun subscribe(listener: Messages.Listener): MessagesSubscription { TODO("Not yet implemented") } - override fun unsubscribe(listener: Messages.Listener) { - TODO("Not yet implemented") - } - - override suspend fun get(options: QueryOptions): PaginatedResult { - TODO("Not yet implemented") - } + override suspend fun get(options: QueryOptions): PaginatedResult = chatApi.getMessages(roomId, options) override suspend fun send(params: SendMessageParams): Message = chatApi.sendMessage(roomId, params) - override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { - TODO("Not yet implemented") - } - - override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Cancellation { TODO("Not yet implemented") } } diff --git a/chat-android/src/main/java/com/ably/chat/Occupancy.kt b/chat-android/src/main/java/com/ably/chat/Occupancy.kt index 62a6bb09..95be9c40 100644 --- a/chat-android/src/main/java/com/ably/chat/Occupancy.kt +++ b/chat-android/src/main/java/com/ably/chat/Occupancy.kt @@ -23,14 +23,7 @@ interface Occupancy : EmitsDiscontinuities { * * @param listener A listener to be called when the occupancy of the room changes. */ - fun subscribe(listener: Listener) - - /** - * Unsubscribe a given listener to occupancy updates of the chat room. - * - * @param listener A listener to be unsubscribed. - */ - fun unsubscribe(listener: Listener) + fun subscribe(listener: Listener): Cancellation /** * Get the current occupancy of the chat room. @@ -72,11 +65,7 @@ internal class DefaultOccupancy( override val channel: Channel get() = messages.channel - override fun subscribe(listener: Occupancy.Listener) { - TODO("Not yet implemented") - } - - override fun unsubscribe(listener: Occupancy.Listener) { + override fun subscribe(listener: Occupancy.Listener): Cancellation { TODO("Not yet implemented") } @@ -84,11 +73,7 @@ internal class DefaultOccupancy( TODO("Not yet implemented") } - override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { - TODO("Not yet implemented") - } - - override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Cancellation { TODO("Not yet implemented") } } diff --git a/chat-android/src/main/java/com/ably/chat/Presence.kt b/chat-android/src/main/java/com/ably/chat/Presence.kt index ba492962..59871022 100644 --- a/chat-android/src/main/java/com/ably/chat/Presence.kt +++ b/chat-android/src/main/java/com/ably/chat/Presence.kt @@ -60,13 +60,7 @@ interface Presence : EmitsDiscontinuities { * Subscribe the given listener to all presence events. * @param listener listener to subscribe */ - fun subscribe(listener: Listener) - - /** - * Unsubscribe the given listener to all presence events. - * @param listener listener to unsubscribe - */ - fun unsubscribe(listener: Listener) + fun subscribe(listener: Listener): Cancellation /** * An interface for listening to new presence event @@ -162,19 +156,11 @@ internal class DefaultPresence( TODO("Not yet implemented") } - override fun subscribe(listener: Presence.Listener) { - TODO("Not yet implemented") - } - - override fun unsubscribe(listener: Presence.Listener) { - TODO("Not yet implemented") - } - - override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun subscribe(listener: Presence.Listener): Cancellation { TODO("Not yet implemented") } - override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Cancellation { TODO("Not yet implemented") } } diff --git a/chat-android/src/main/java/com/ably/chat/RoomReactions.kt b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt index f3a2d588..2c793391 100644 --- a/chat-android/src/main/java/com/ably/chat/RoomReactions.kt +++ b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt @@ -38,12 +38,7 @@ interface RoomReactions : EmitsDiscontinuities { * @param listener The listener function to be called when a reaction is received. * @returns A response object that allows you to control the subscription. */ - fun subscribe(listener: Listener) - - /** - * Unsubscribe all listeners from receiving room-level reaction events. - */ - fun unsubscribe(listener: Listener) + fun subscribe(listener: Listener): Cancellation /** * An interface for listening to new reaction events @@ -116,19 +111,11 @@ internal class DefaultRoomReactions( TODO("Not yet implemented") } - override fun subscribe(listener: RoomReactions.Listener) { - TODO("Not yet implemented") - } - - override fun unsubscribe(listener: RoomReactions.Listener) { - TODO("Not yet implemented") - } - - override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun subscribe(listener: RoomReactions.Listener): Cancellation { TODO("Not yet implemented") } - override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Cancellation { TODO("Not yet implemented") } } diff --git a/chat-android/src/main/java/com/ably/chat/RoomStatus.kt b/chat-android/src/main/java/com/ably/chat/RoomStatus.kt index 09f5b7a5..82620b76 100644 --- a/chat-android/src/main/java/com/ably/chat/RoomStatus.kt +++ b/chat-android/src/main/java/com/ably/chat/RoomStatus.kt @@ -21,12 +21,7 @@ interface RoomStatus { * @param listener The function to call when the status changes. * @returns An object that can be used to unregister the listener. */ - fun on(listener: Listener) - - /** - * Removes all listeners that were added by the `onChange` method. - */ - fun off(listener: Listener) + fun on(listener: Listener): Cancellation /** * An interface for listening to changes for the room status diff --git a/chat-android/src/main/java/com/ably/chat/Typing.kt b/chat-android/src/main/java/com/ably/chat/Typing.kt index fa75bc24..5de49f6e 100644 --- a/chat-android/src/main/java/com/ably/chat/Typing.kt +++ b/chat-android/src/main/java/com/ably/chat/Typing.kt @@ -37,12 +37,7 @@ interface Typing : EmitsDiscontinuities { * * @param listener A listener to be called when the typing state of a user in the room changes. */ - fun subscribe(listener: Listener) - - /** - * Unsubscribe listeners from receiving typing events. - */ - fun unsubscribe(listener: Listener) + fun subscribe(listener: Listener): Cancellation /** * Get the current typers, a set of clientIds. @@ -89,11 +84,7 @@ internal class DefaultTyping( override val channel: Channel get() = realtimeClient.channels.get(typingIndicatorsChannelName, ChatChannelOptions()) - override fun subscribe(listener: Typing.Listener) { - TODO("Not yet implemented") - } - - override fun unsubscribe(listener: Typing.Listener) { + override fun subscribe(listener: Typing.Listener): Cancellation { TODO("Not yet implemented") } @@ -109,11 +100,7 @@ internal class DefaultTyping( TODO("Not yet implemented") } - override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { - TODO("Not yet implemented") - } - - override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Cancellation { TODO("Not yet implemented") } } From c5ddf977805cee9e00da4df1986b4200173b0b91 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 9 Sep 2024 12:30:51 +0100 Subject: [PATCH 2/3] WIP: basic chat implementation --- .../src/main/java/com/ably/chat/ChatApi.kt | 9 ++- .../src/main/java/com/ably/chat/ChatClient.kt | 3 +- .../src/main/java/com/ably/chat/Messages.kt | 79 +++++++++++++++++-- .../src/main/java/com/ably/chat/Rooms.kt | 15 ++-- .../src/main/java/com/ably/chat/Utils.kt | 2 +- .../com/ably/chat/example/MainActivity.kt | 47 +++++++---- 6 files changed, 117 insertions(+), 38 deletions(-) diff --git a/chat-android/src/main/java/com/ably/chat/ChatApi.kt b/chat-android/src/main/java/com/ably/chat/ChatApi.kt index cbd9733e..50880bf6 100644 --- a/chat-android/src/main/java/com/ably/chat/ChatApi.kt +++ b/chat-android/src/main/java/com/ably/chat/ChatApi.kt @@ -17,19 +17,20 @@ private const val API_PROTOCOL_VERSION = 3 private const val PROTOCOL_VERSION_PARAM_NAME = "v" private val apiProtocolParam = Param(PROTOCOL_VERSION_PARAM_NAME, API_PROTOCOL_VERSION.toString()) -// TODO make this class internal -class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) { +internal class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) { /** * Get messages from the Chat Backend * * @return paginated result with messages */ - suspend fun getMessages(roomId: String, params: QueryOptions): PaginatedResult { + suspend fun getMessages(roomId: String, options: QueryOptions, fromSerial: String? = null): PaginatedResult { + val baseParams = options.toParams() + val params = fromSerial?.let { baseParams + Param("fromSerial", it) } ?: baseParams return makeAuthorizedPaginatedRequest( url = "/chat/v1/rooms/$roomId/messages", method = "GET", - params = params.toParams(), + params = params, ) { Message( timeserial = it.requireString("timeserial"), diff --git a/chat-android/src/main/java/com/ably/chat/ChatClient.kt b/chat-android/src/main/java/com/ably/chat/ChatClient.kt index b621db9b..e933666f 100644 --- a/chat-android/src/main/java/com/ably/chat/ChatClient.kt +++ b/chat-android/src/main/java/com/ably/chat/ChatClient.kt @@ -37,7 +37,8 @@ interface ChatClient { val clientOptions: ClientOptions } -fun ChatClient(realtimeClient: RealtimeClient, clientOptions: ClientOptions): ChatClient = DefaultChatClient(realtimeClient, clientOptions) +fun ChatClient(realtimeClient: RealtimeClient, clientOptions: ClientOptions = ClientOptions()): ChatClient = + DefaultChatClient(realtimeClient, clientOptions) internal class DefaultChatClient( override val realtime: RealtimeClient, diff --git a/chat-android/src/main/java/com/ably/chat/Messages.kt b/chat-android/src/main/java/com/ably/chat/Messages.kt index edad9b55..bad6317b 100644 --- a/chat-android/src/main/java/com/ably/chat/Messages.kt +++ b/chat-android/src/main/java/com/ably/chat/Messages.kt @@ -2,7 +2,13 @@ package com.ably.chat +import com.ably.chat.QueryOptions.MessageOrder.NewestFirst +import com.google.gson.JsonObject import io.ably.lib.realtime.Channel +import io.ably.lib.realtime.Channel.MessageListener +import io.ably.lib.realtime.ChannelState +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * This interface is used to interact with messages in a chat room: subscribing @@ -91,7 +97,7 @@ data class QueryOptions( /** * The order of messages in the query result. */ - val orderBy: MessageOrder = MessageOrder.NewestFirst, + val orderBy: MessageOrder = NewestFirst, ) { /** * Represents direction to query messages in. @@ -169,26 +175,73 @@ data class SendMessageParams( val headers: MessageHeaders? = null, ) -interface MessagesSubscription: Cancellation { +interface MessagesSubscription : Cancellation { suspend fun getPreviousMessages(queryOptions: QueryOptions): PaginatedResult } -class DefaultMessages( +internal class DefaultMessagesSubscription( + private val chatApi: ChatApi, + private val roomId: String, + private val cancellation: Cancellation, + private val timeserialProvider: suspend () -> String, +) : MessagesSubscription { + override fun cancel() { + cancellation.cancel() + } + + override suspend fun getPreviousMessages(queryOptions: QueryOptions): PaginatedResult { + val fromSerial = timeserialProvider() + return chatApi.getMessages( + roomId = roomId, + options = queryOptions.copy(orderBy = NewestFirst), + fromSerial = fromSerial, + ) + } +} + +internal class DefaultMessages( private val roomId: String, - private val realtimeClient: RealtimeClient, + realtimeClient: RealtimeClient, private val chatApi: ChatApi, ) : Messages { + private var observers: Set = emptySet() + + private var channelSerial: String? = null + /** * the channel name for the chat messages channel. */ private val messagesChannelName = "$roomId::\$chat::\$chatMessages" - override val channel: Channel - get() = realtimeClient.channels.get(messagesChannelName, ChatChannelOptions()) + override val channel: Channel = realtimeClient.channels.get(messagesChannelName, ChatChannelOptions()) override fun subscribe(listener: Messages.Listener): MessagesSubscription { - TODO("Not yet implemented") + observers += listener + val messageListener = MessageListener { + val pubSubMessage = it!! + val chatMessage = Message( + roomId = roomId, + createdAt = pubSubMessage.timestamp, + clientId = pubSubMessage.clientId, + timeserial = pubSubMessage.extras.asJsonObject().get("timeserial").asString, + text = (pubSubMessage.data as JsonObject).get("text").asString, + metadata = mapOf(), // rawPubSubMessage.data.metadata + headers = mapOf(), // rawPubSubMessage.extras.headers + ) + observers.forEach { listener -> listener.onEvent(MessageEvent(type = MessageEventType.Created, message = chatMessage)) } + } + channel.subscribe(messageListener) + + return DefaultMessagesSubscription( + chatApi = chatApi, + roomId = roomId, + cancellation = { + observers -= listener + channel.unsubscribe(messageListener) + }, + timeserialProvider = { getChannelSerial() }, + ) } override suspend fun get(options: QueryOptions): PaginatedResult = chatApi.getMessages(roomId, options) @@ -198,4 +251,16 @@ class DefaultMessages( override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Cancellation { TODO("Not yet implemented") } + + private suspend fun readAttachmentProperties() = suspendCoroutine { continuation -> + channel.once(ChannelState.attached) { + continuation.resume(channel.properties) + } + } + + private suspend fun getChannelSerial(): String { + if (channelSerial != null) return channelSerial!! + channelSerial = readAttachmentProperties().channelSerial + return channelSerial!! + } } diff --git a/chat-android/src/main/java/com/ably/chat/Rooms.kt b/chat-android/src/main/java/com/ably/chat/Rooms.kt index 44d917d0..cd5708b4 100644 --- a/chat-android/src/main/java/com/ably/chat/Rooms.kt +++ b/chat-android/src/main/java/com/ably/chat/Rooms.kt @@ -1,8 +1,5 @@ package com.ably.chat -import io.ably.lib.types.AblyException -import io.ably.lib.types.ErrorInfo - /** * Manages the lifecycle of chat rooms. */ @@ -24,7 +21,7 @@ interface Rooms { * @throws {@link ErrorInfo} if a room with the same ID but different options already exists. * @returns Room A new or existing Room object. */ - fun get(roomId: String, options: RoomOptions): Room + fun get(roomId: String, options: RoomOptions = RoomOptions()): Room /** * Release the Room object if it exists. This method only releases the reference @@ -60,11 +57,11 @@ internal class DefaultRooms( ) } - if (room.options != options) { - throw AblyException.fromErrorInfo( - ErrorInfo("Room already exists with different options", HttpStatusCodes.BadRequest, ErrorCodes.BadRequest), - ) - } +// if (room.options != options) { +// throw AblyException.fromErrorInfo( +// ErrorInfo("Room already exists with different options", HttpStatusCodes.BadRequest, ErrorCodes.BadRequest), +// ) +// } room } diff --git a/chat-android/src/main/java/com/ably/chat/Utils.kt b/chat-android/src/main/java/com/ably/chat/Utils.kt index 531a4eb3..4e6bcf85 100644 --- a/chat-android/src/main/java/com/ably/chat/Utils.kt +++ b/chat-android/src/main/java/com/ably/chat/Utils.kt @@ -39,7 +39,7 @@ suspend fun Channel.detachCoroutine() = suspendCoroutine { continuation -> fun ChatChannelOptions(init: (ChannelOptions.() -> Unit)? = null): ChannelOptions { val options = ChannelOptions() init?.let { options.it() } - options.params = options.params + mapOf( + options.params = (options.params ?: mapOf()) + mapOf( AGENT_PARAMETER_NAME to "chat-kotlin/${BuildConfig.APP_VERSION}", ) return options diff --git a/example/src/main/java/com/ably/chat/example/MainActivity.kt b/example/src/main/java/com/ably/chat/example/MainActivity.kt index 8fc363b0..d0bfaffc 100644 --- a/example/src/main/java/com/ably/chat/example/MainActivity.kt +++ b/example/src/main/java/com/ably/chat/example/MainActivity.kt @@ -14,12 +14,14 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -30,10 +32,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import com.ably.chat.ChatApi +import com.ably.chat.ChatClient import com.ably.chat.Message import com.ably.chat.QueryOptions -import com.ably.chat.QueryOptions.MessageOrder.OldestFirst import com.ably.chat.RealtimeClient import com.ably.chat.SendMessageParams import com.ably.chat.example.ui.theme.AblyChatExampleTheme @@ -46,6 +47,7 @@ val randomClientId = UUID.randomUUID().toString() class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val realtimeClient = RealtimeClient( ClientOptions().apply { key = BuildConfig.ABLY_KEY @@ -53,13 +55,15 @@ class MainActivity : ComponentActivity() { logLevel = 2 }, ) - val chatApi = ChatApi(realtimeClient, randomClientId) + + val chatClient = ChatClient(realtimeClient) + enableEdgeToEdge() setContent { AblyChatExampleTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Chat( - chatApi, + chatClient, modifier = Modifier.padding(innerPadding), ) } @@ -69,29 +73,42 @@ class MainActivity : ComponentActivity() { } @Composable -fun Chat(chatApi: ChatApi, modifier: Modifier = Modifier) { +fun Chat(chatClient: ChatClient, modifier: Modifier = Modifier) { var messageText by remember { mutableStateOf(TextFieldValue("")) } var sending by remember { mutableStateOf(false) } var messages by remember { mutableStateOf(listOf()) } + val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() val roomId = "my-room" + val room = chatClient.rooms.get(roomId) - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - Button(modifier = modifier.align(Alignment.CenterHorizontally), onClick = { + DisposableEffect(Unit) { + val subscription = room.messages.subscribe { + messages += it.message coroutineScope.launch { - messages = chatApi.getMessages(roomId, QueryOptions(orderBy = OldestFirst)).items + listState.animateScrollToItem(messages.size - 1) } - }) { - Text("Load") } + coroutineScope.launch { + messages = subscription.getPreviousMessages(QueryOptions()).items.reversed() + listState.animateScrollToItem(messages.size - 1) + } + + onDispose { + subscription.cancel() + } + } + + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + ) { LazyColumn( modifier = Modifier.weight(1f).padding(16.dp), userScrollEnabled = true, + state = listState, ) { items(messages.size) { index -> MessageBubble(messages[index]) @@ -105,8 +122,7 @@ fun Chat(chatApi: ChatApi, modifier: Modifier = Modifier) { ) { sending = true coroutineScope.launch { - chatApi.sendMessage( - roomId, + room.messages.send( SendMessageParams( text = messageText.text, ), @@ -160,7 +176,6 @@ fun ChatInputField( TextField( value = messageInput, onValueChange = onMessageChange, - readOnly = sending, modifier = Modifier .weight(1f) .background(Color.White), From dc96d23b797f133f756f38536c8bd4cf6e177247 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 10 Sep 2024 13:53:58 +0100 Subject: [PATCH 3/3] fix: array out of bound exception --- example/src/main/java/com/ably/chat/example/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/main/java/com/ably/chat/example/MainActivity.kt b/example/src/main/java/com/ably/chat/example/MainActivity.kt index d0bfaffc..9a3b0a17 100644 --- a/example/src/main/java/com/ably/chat/example/MainActivity.kt +++ b/example/src/main/java/com/ably/chat/example/MainActivity.kt @@ -80,7 +80,7 @@ fun Chat(chatClient: ChatClient, modifier: Modifier = Modifier) { val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() - val roomId = "my-room" + val roomId = "my-room2" val room = chatClient.rooms.get(roomId) DisposableEffect(Unit) { @@ -93,7 +93,7 @@ fun Chat(chatClient: ChatClient, modifier: Modifier = Modifier) { coroutineScope.launch { messages = subscription.getPreviousMessages(QueryOptions()).items.reversed() - listState.animateScrollToItem(messages.size - 1) + if (messages.isNotEmpty()) listState.animateScrollToItem(messages.size - 1) } onDispose {