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 bf33332c..c155a388 100644 --- a/chat-android/src/main/java/com/ably/chat/Messages.kt +++ b/chat-android/src/main/java/com/ably/chat/Messages.kt @@ -4,7 +4,7 @@ package com.ably.chat import com.ably.chat.QueryOptions.MessageOrder.NewestFirst import com.google.gson.JsonObject -import io.ably.lib.realtime.AblyRealtime +import io.ably.lib.realtime.Channel import io.ably.lib.realtime.ChannelState import io.ably.lib.realtime.ChannelStateListener import io.ably.lib.types.AblyException @@ -211,11 +211,8 @@ internal class DefaultMessagesSubscription( } internal class DefaultMessages( - private val roomId: String, - private val realtimeChannels: AblyRealtime.Channels, - private val chatApi: ChatApi, - private val logger: Logger, -) : Messages, ContributesToRoomLifecycleImpl(logger) { + val room: DefaultRoom, +) : Messages, ContributesToRoomLifecycleImpl(room.roomLogger) { override val featureName: String = "messages" @@ -223,15 +220,23 @@ internal class DefaultMessages( private var channelStateListener: ChannelStateListener + private val logger = room.roomLogger.withContext(tag = "Messages") + + private val roomId = room.roomId + + private val chatApi = room.chatApi + + private val realtimeChannels = room.realtimeClient.channels + private var lock = Any() /** * (CHA-M1) * the channel name for the chat messages channel. */ - private val messagesChannelName = "$roomId::\$chat::\$chatMessages" + private val messagesChannelName = "${room.roomId}::\$chat::\$chatMessages" - override val channel = realtimeChannels.get(messagesChannelName, ChatChannelOptions()) + override val channel: Channel = realtimeChannels.get(messagesChannelName, room.options.messagesChannelOptions()) override val attachmentErrorCode: ErrorCode = ErrorCode.MessagesAttachmentFailed 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 c2afa229..b6d4ed84 100644 --- a/chat-android/src/main/java/com/ably/chat/Occupancy.kt +++ b/chat-android/src/main/java/com/ably/chat/Occupancy.kt @@ -81,21 +81,9 @@ internal class DefaultOccupancy( override val detachmentErrorCode: ErrorCode = ErrorCode.OccupancyDetachmentFailed - private val realtimeChannels = room.realtimeClient.channels - private val logger = room.roomLogger.withContext(tag = "Occupancy") - // (CHA-O1) - private val messagesChannelName = "${room.roomId}::\$chat::\$chatMessages" - - override val channel: Channel = realtimeChannels.get( - messagesChannelName, - ChatChannelOptions { - params = mapOf( - "occupancy" to "metrics", - ) - }, - ) + override val channel: Channel = room.messages.channel private val listeners: MutableList = CopyOnWriteArrayList() diff --git a/chat-android/src/main/java/com/ably/chat/Room.kt b/chat-android/src/main/java/com/ably/chat/Room.kt index 92405710..a99bc4fd 100644 --- a/chat-android/src/main/java/com/ably/chat/Room.kt +++ b/chat-android/src/main/java/com/ably/chat/Room.kt @@ -127,12 +127,7 @@ internal class DefaultRoom( private val roomScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1) + CoroutineName(roomId) + SupervisorJob()) - override val messages = DefaultMessages( - roomId = roomId, - realtimeChannels = realtimeClient.channels, - chatApi = chatApi, - logger = roomLogger.withContext(tag = "Messages"), - ) + override val messages = DefaultMessages(room = this) private var _presence: Presence? = null override val presence: Presence diff --git a/chat-android/src/main/java/com/ably/chat/RoomOptions.kt b/chat-android/src/main/java/com/ably/chat/RoomOptions.kt index f30940da..f253f4cd 100644 --- a/chat-android/src/main/java/com/ably/chat/RoomOptions.kt +++ b/chat-android/src/main/java/com/ably/chat/RoomOptions.kt @@ -1,5 +1,8 @@ package com.ably.chat +import io.ably.lib.types.ChannelMode +import io.ably.lib.types.ChannelOptions + /** * Represents the options for a given chat room. */ @@ -89,10 +92,36 @@ object OccupancyOptions * Throws AblyException for invalid room configuration. * Spec: CHA-RC2a */ -fun RoomOptions.validateRoomOptions() { +internal fun RoomOptions.validateRoomOptions() { typing?.let { if (typing.timeoutMs <= 0) { throw ablyException("Typing timeout must be greater than 0", ErrorCode.InvalidRequestBody) } } } + +/** + * Merges channel options/modes from presence and occupancy to be used for shared channel. + * This channel is shared by Room messages, presence and occupancy feature. + * @return channelOptions for shared channel with options/modes from presence and occupancy. + * Spec: CHA-RC3 + */ +internal fun RoomOptions.messagesChannelOptions(): ChannelOptions { + return ChatChannelOptions { + presence?.let { + val presenceModes = mutableListOf() + if (presence.enter) { + presenceModes.add(ChannelMode.presence) + } + if (presence.subscribe) { + presenceModes.add(ChannelMode.presence_subscribe) + } + modes = presenceModes.toTypedArray() + } + occupancy?.let { + params = mapOf( + "occupancy" to "metrics", + ) + } + } +} diff --git a/chat-android/src/test/java/com/ably/chat/MessagesTest.kt b/chat-android/src/test/java/com/ably/chat/MessagesTest.kt index 07b0dea6..c427c5de 100644 --- a/chat-android/src/test/java/com/ably/chat/MessagesTest.kt +++ b/chat-android/src/test/java/com/ably/chat/MessagesTest.kt @@ -1,21 +1,21 @@ package com.ably.chat -import com.ably.chat.room.createMockLogger +import com.ably.chat.room.createMockChannel +import com.ably.chat.room.createMockChatApi +import com.ably.chat.room.createMockRealtimeClient +import com.ably.chat.room.createMockRoom import com.google.gson.JsonObject -import io.ably.lib.realtime.AblyRealtime.Channels import io.ably.lib.realtime.Channel import io.ably.lib.realtime.ChannelBase import io.ably.lib.realtime.ChannelState import io.ably.lib.realtime.ChannelStateListener import io.ably.lib.realtime.buildChannelStateChange -import io.ably.lib.realtime.buildRealtimeChannel import io.ably.lib.types.AblyException import io.ably.lib.types.MessageAction import io.ably.lib.types.MessageExtras import io.mockk.every import io.mockk.mockk import io.mockk.slot -import io.mockk.spyk import io.mockk.verify import java.lang.reflect.Field import kotlinx.coroutines.runBlocking @@ -27,29 +27,23 @@ import org.junit.Test class MessagesTest { - private val realtimeClient = mockk(relaxed = true) - private val realtimeChannels = mockk(relaxed = true) - private val realtimeChannel = spyk(buildRealtimeChannel()) - private val chatApi = spyk(ChatApi(realtimeClient, "clientId", EmptyLogger(LogContext(tag = "TEST")))) - private lateinit var messages: DefaultMessages - private val logger = createMockLogger() + private val realtimeClient = createMockRealtimeClient() + private val realtimeChannel = realtimeClient.createMockChannel() + private lateinit var messages: DefaultMessages private val channelStateListenerSlot = slot() @Before fun setUp() { - every { realtimeChannels.get(any(), any()) } returns realtimeChannel - + every { realtimeClient.channels.get(any(), any()) } returns realtimeChannel every { realtimeChannel.on(capture(channelStateListenerSlot)) } answers { println("Channel state listener registered") } - messages = DefaultMessages( - roomId = "room1", - realtimeChannels = realtimeChannels, - chatApi = chatApi, - logger, - ) + val chatApi = createMockChatApi(realtimeClient) + val room = createMockRoom("room1", realtimeClient = realtimeClient, chatApi = chatApi) + + messages = DefaultMessages(room) } /** diff --git a/chat-android/src/test/java/com/ably/chat/room/RoomFeatureSharedChannelTest.kt b/chat-android/src/test/java/com/ably/chat/room/RoomFeatureSharedChannelTest.kt new file mode 100644 index 00000000..f3ee8061 --- /dev/null +++ b/chat-android/src/test/java/com/ably/chat/room/RoomFeatureSharedChannelTest.kt @@ -0,0 +1,67 @@ +package com.ably.chat.room + +import io.ably.lib.realtime.buildRealtimeChannel +import io.ably.lib.types.ChannelMode +import io.ably.lib.types.ChannelOptions +import io.mockk.every +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +/** + * Test to check shared channel for room features. + * Spec: CHA-RC3 + */ +class RoomFeatureSharedChannelTest { + + @Test + fun `(CHA-RC3a) Features with shared channel should call channels#get only once with combined modes+options`() = runTest { + val mockRealtimeClient = createMockRealtimeClient() + val capturedChannelOptions = mutableListOf() + + every { + mockRealtimeClient.channels.get("1234::\$chat::\$chatMessages", any()) + } answers { + capturedChannelOptions.add(secondArg()) + buildRealtimeChannel() + } + + // Create room with all feature enabled, + val room = createMockRoom(realtimeClient = mockRealtimeClient) + + // Messages, occupancy and presence features uses the same channel + Assert.assertEquals(room.messages.channel, room.presence.channel) + Assert.assertEquals(room.messages.channel, room.occupancy.channel) + + // Reactions and typing uses independent channel + Assert.assertNotEquals(room.messages.channel, room.typing.channel) + Assert.assertNotEquals(room.messages.channel, room.reactions.channel) + Assert.assertNotEquals(room.reactions.channel, room.typing.channel) + + Assert.assertEquals(1, capturedChannelOptions.size) + // Check for set presence modes + Assert.assertEquals(2, capturedChannelOptions[0].modes.size) + Assert.assertEquals(ChannelMode.presence, capturedChannelOptions[0].modes[0]) + Assert.assertEquals(ChannelMode.presence_subscribe, capturedChannelOptions[0].modes[1]) + // Check if occupancy matrix is set + Assert.assertEquals("metrics", capturedChannelOptions[0].params["occupancy"]) + + // channels.get is called only once for Messages, occupancy and presence since they share the same channel + verify(exactly = 1) { + mockRealtimeClient.channels.get("1234::\$chat::\$chatMessages", any()) + } + // channels.get called separately for typing since it uses it's own channel + verify(exactly = 1) { + mockRealtimeClient.channels.get("1234::\$chat::\$typingIndicators", any()) + } + // channels.get called separately for reactions since it uses it's own channel + verify(exactly = 1) { + mockRealtimeClient.channels.get("1234::\$chat::\$reactions", any()) + } + // channels.get is called thrice for all features + verify(exactly = 3) { + mockRealtimeClient.channels.get(any(), any()) + } + } +} diff --git a/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt b/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt index cebff0b7..2658daa8 100644 --- a/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt +++ b/chat-android/src/test/java/com/ably/chat/room/RoomTestHelpers.kt @@ -98,7 +98,7 @@ fun createRoomFeatureMocks(roomId: String = DEFAULT_ROOM_ID, clientId: String = val logger = createMockLogger() val room = createMockRoom(roomId, clientId, realtimeClient, chatApi, logger) - val messagesContributor = spyk(DefaultMessages(roomId, realtimeClient.channels, chatApi, logger), recordPrivateCalls = true) + val messagesContributor = spyk(DefaultMessages(room), recordPrivateCalls = true) val presenceContributor = spyk(DefaultPresence(room), recordPrivateCalls = true) val occupancyContributor = spyk(DefaultOccupancy(room), recordPrivateCalls = true) val typingContributor = spyk(DefaultTyping(room), recordPrivateCalls = true)