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 6a1d06d..e65e617 100644 --- a/chat-android/src/main/java/com/ably/chat/ChatClient.kt +++ b/chat-android/src/main/java/com/ably/chat/ChatClient.kt @@ -1,7 +1,8 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat import io.ably.lib.realtime.AblyRealtime -import io.ably.lib.types.ClientOptions typealias RealtimeClient = AblyRealtime @@ -35,3 +36,26 @@ interface ChatClient { */ val clientOptions: ClientOptions } + +fun ChatClient(realtimeClient: RealtimeClient, clientOptions: ClientOptions): ChatClient = DefaultChatClient(realtimeClient, clientOptions) + +internal class DefaultChatClient( + override val realtime: RealtimeClient, + override val clientOptions: ClientOptions, +) : ChatClient { + + private val chatApi = ChatApi(realtime) + + override val rooms: Rooms = DefaultRooms( + realtimeClient = realtime, + chatApi = chatApi, + clientOptions = clientOptions, + clientId = clientId, + ) + + override val connection: Connection + get() = TODO("Not yet implemented") + + override val clientId: String + get() = realtime.auth.clientId +} 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 a43259c..8bc9241 100644 --- a/chat-android/src/main/java/com/ably/chat/Messages.kt +++ b/chat-android/src/main/java/com/ably/chat/Messages.kt @@ -1,3 +1,5 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat import io.ably.lib.realtime.Channel @@ -173,3 +175,53 @@ data class SendMessageParams( */ val headers: MessageHeaders? = null, ) + +class DefaultMessages( + private val roomId: String, + private val clientId: String, + private val realtimeClient: RealtimeClient, + private val chatApi: ChatApi, +) : Messages { + + /** + * 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 fun subscribe(listener: Messages.Listener) { + 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 send(params: SendMessageParams): Message { + return chatApi.sendMessage(roomId, params).let { + Message( + timeserial = it.timeserial, + clientId = clientId, + roomId = roomId, + text = params.text, + createdAt = it.createdAt, + metadata = params.metadata ?: mapOf(), + headers = params.headers ?: mapOf(), + ) + } + } + + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { + TODO("Not yet implemented") + } + + override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + 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 0ebe721..62a6bb0 100644 --- a/chat-android/src/main/java/com/ably/chat/Occupancy.kt +++ b/chat-android/src/main/java/com/ably/chat/Occupancy.kt @@ -1,3 +1,5 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat import io.ably.lib.realtime.Channel @@ -63,3 +65,30 @@ data class OccupancyEvent( */ val presenceMembers: Int, ) + +internal class DefaultOccupancy( + private val messages: Messages, +) : Occupancy { + override val channel: Channel + get() = messages.channel + + override fun subscribe(listener: Occupancy.Listener) { + TODO("Not yet implemented") + } + + override fun unsubscribe(listener: Occupancy.Listener) { + TODO("Not yet implemented") + } + + override suspend fun get(): OccupancyEvent { + TODO("Not yet implemented") + } + + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { + TODO("Not yet implemented") + } + + override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + 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 c2c55b1..ba49296 100644 --- a/chat-android/src/main/java/com/ably/chat/Presence.kt +++ b/chat-android/src/main/java/com/ably/chat/Presence.kt @@ -1,3 +1,5 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat import android.text.PrecomputedText.Params @@ -132,3 +134,47 @@ data class PresenceEvent( */ val data: PresenceData, ) + +internal class DefaultPresence( + private val messages: Messages, +) : Presence { + + override val channel: Channel + get() = messages.channel + + override suspend fun get(params: List): List { + TODO("Not yet implemented") + } + + override suspend fun isUserPresent(clientId: String): Boolean { + TODO("Not yet implemented") + } + + override suspend fun enter(data: PresenceData?) { + TODO("Not yet implemented") + } + + override suspend fun update(data: PresenceData?) { + TODO("Not yet implemented") + } + + override suspend fun leave(data: PresenceData?) { + 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) { + TODO("Not yet implemented") + } + + override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + TODO("Not yet implemented") + } +} 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 3f6616e..7ce66b8 100644 --- a/chat-android/src/main/java/com/ably/chat/Room.kt +++ b/chat-android/src/main/java/com/ably/chat/Room.kt @@ -1,3 +1,5 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat /** @@ -80,3 +82,53 @@ interface Room { */ suspend fun detach() } + +internal class DefaultRoom( + override val roomId: String, + override val options: RoomOptions, + realtimeClient: RealtimeClient, + chatApi: ChatApi, + clientId: String, +) : Room { + + override val messages: Messages = DefaultMessages( + roomId = roomId, + realtimeClient = realtimeClient, + chatApi = chatApi, + clientId = clientId, + ) + + override val presence: Presence = DefaultPresence( + messages = messages, + ) + + override val reactions: RoomReactions = DefaultRoomReactions( + roomId = roomId, + realtimeClient = realtimeClient, + ) + + override val typing: Typing = DefaultTyping( + roomId = roomId, + realtimeClient = realtimeClient, + ) + + override val occupancy: Occupancy = DefaultOccupancy( + messages = messages, + ) + + override val status: RoomStatus get() { + TODO("Not yet implemented") + } + + override suspend fun attach() { + messages.channel.attachCoroutine() + typing.channel.attachCoroutine() + reactions.channel.attachCoroutine() + } + + override suspend fun detach() { + messages.channel.detachCoroutine() + typing.channel.detachCoroutine() + reactions.channel.detachCoroutine() + } +} 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 3ced2e2..f3a2d58 100644 --- a/chat-android/src/main/java/com/ably/chat/RoomReactions.kt +++ b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt @@ -1,3 +1,5 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat import io.ably.lib.realtime.Channel @@ -100,3 +102,33 @@ data class SendReactionParams( */ val headers: ReactionHeaders? = null, ) + +internal class DefaultRoomReactions( + roomId: String, + private val realtimeClient: RealtimeClient, +) : RoomReactions { + private val roomReactionsChannelName = "$roomId::\$chat::\$reactions" + + override val channel: Channel + get() = realtimeClient.channels.get(roomReactionsChannelName, ChatChannelOptions()) + + override suspend fun send(params: SendReactionParams) { + 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) { + TODO("Not yet implemented") + } + + override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + TODO("Not yet implemented") + } +} 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 e4fd7ff..b860fbf 100644 --- a/chat-android/src/main/java/com/ably/chat/Rooms.kt +++ b/chat-android/src/main/java/com/ably/chat/Rooms.kt @@ -1,5 +1,8 @@ package com.ably.chat +import io.ably.lib.types.AblyException +import io.ably.lib.types.ErrorInfo + /** * Manages the lifecycle of chat rooms. */ @@ -35,3 +38,43 @@ interface Rooms { */ suspend fun release(roomId: String) } + +/** + * Manages the chat rooms. + */ +internal class DefaultRooms( + private val realtimeClient: RealtimeClient, + private val chatApi: ChatApi, + override val clientOptions: ClientOptions, + private val clientId: String, +) : Rooms { + private val roomIdToRoom: MutableMap = mutableMapOf() + + override fun get(roomId: String, options: RoomOptions): Room { + return synchronized(this) { + val room = roomIdToRoom.getOrPut(roomId) { + DefaultRoom( + roomId = roomId, + options = options, + realtimeClient = realtimeClient, + chatApi = chatApi, + clientId = clientId, + ) + } + + if (room.options != options) { + throw AblyException.fromErrorInfo( + ErrorInfo("Room already exists with different options", HttpStatusCodes.BadRequest, ErrorCodes.BadRequest), + ) + } + + room + } + } + + override suspend fun release(roomId: String) { + synchronized(this) { + roomIdToRoom.remove(roomId) + } + } +} 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 c6532af..fa75bc2 100644 --- a/chat-android/src/main/java/com/ably/chat/Typing.kt +++ b/chat-android/src/main/java/com/ably/chat/Typing.kt @@ -1,3 +1,5 @@ +@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration") + package com.ably.chat import io.ably.lib.realtime.Channel @@ -77,3 +79,41 @@ interface Typing : EmitsDiscontinuities { * Represents a typing event. */ data class TypingEvent(val currentlyTyping: Set) + +internal class DefaultTyping( + roomId: String, + private val realtimeClient: RealtimeClient, +) : Typing { + private val typingIndicatorsChannelName = "$roomId::\$chat::\$typingIndicators" + + 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) { + TODO("Not yet implemented") + } + + override suspend fun get(): Set { + TODO("Not yet implemented") + } + + override suspend fun start() { + TODO("Not yet implemented") + } + + override suspend fun stop() { + TODO("Not yet implemented") + } + + override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener) { + TODO("Not yet implemented") + } + + override fun offDiscontinuity(listener: EmitsDiscontinuities.Listener) { + TODO("Not yet implemented") + } +} diff --git a/chat-android/src/main/java/com/ably/chat/Utils.kt b/chat-android/src/main/java/com/ably/chat/Utils.kt new file mode 100644 index 0000000..5b84ba6 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Utils.kt @@ -0,0 +1,45 @@ +package com.ably.chat + +import io.ably.lib.realtime.Channel +import io.ably.lib.realtime.CompletionListener +import io.ably.lib.types.AblyException +import io.ably.lib.types.ChannelOptions +import io.ably.lib.types.ErrorInfo +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend fun Channel.attachCoroutine() = suspendCoroutine { continuation -> + attach(object : CompletionListener { + override fun onSuccess() { + continuation.resume(Unit) + } + + override fun onError(reason: ErrorInfo?) { + continuation.resumeWithException(AblyException.fromErrorInfo(reason)) + } + }) +} + +suspend fun Channel.detachCoroutine() = suspendCoroutine { continuation -> + detach(object : CompletionListener { + override fun onSuccess() { + continuation.resume(Unit) + } + + override fun onError(reason: ErrorInfo?) { + continuation.resumeWithException(AblyException.fromErrorInfo(reason)) + } + }) +} + +@Suppress("FunctionName") +fun ChatChannelOptions(init: (ChannelOptions.() -> Unit)? = null): ChannelOptions { + val options = ChannelOptions() + init?.let { options.it() } + options.params = options.params + mapOf( + // TODO replace with the actual version (e.g. using BuildConfig plugin https://github.com/gmazzo/gradle-buildconfig-plugin) + "agent" to "chat-js/0.0.0", + ) + return options +} diff --git a/detekt.yml b/detekt.yml index cc8eda1..9a47ad1 100644 --- a/detekt.yml +++ b/detekt.yml @@ -999,7 +999,7 @@ style: UseCheckOrError: active: true UseDataClass: - active: true + active: false allowVars: false UseEmptyCounterpart: active: false