Skip to content
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d5f5e82
[ECO-5014] feat: basic sandbox setup
ttypic Nov 11, 2024
9d21f69
[ECO-5082] feat: presence basic implementation
ttypic Nov 5, 2024
878face
[ECO-5082] feat: add presence popup for example app
ttypic Nov 5, 2024
89df802
[ECO-5082] chore: add sandbox for presence
ttypic Nov 13, 2024
c6b2589
Removed initializing status from RoomStatus as per CHA-RS1j.
sacOO7 Nov 18, 2024
13ce9e4
Removed ResolvedContributor interface, since it's no longer needed
sacOO7 Nov 18, 2024
b1c09b1
Merge branch 'main' into feature/rooms-async-get
sacOO7 Nov 18, 2024
8c5e02b
Updated Room implementation to initialize features based on provided …
sacOO7 Nov 18, 2024
a3d7bb4
Ignored flaky sandbox tests
sacOO7 Nov 18, 2024
bdccd57
Updated RoomOptions validity check and access with proper error codes
sacOO7 Nov 18, 2024
dcc9538
Added companion default for RoomOptions, added tests for the same
sacOO7 Nov 18, 2024
a183715
Removed release as a part of Room interface, it's an internal method and
sacOO7 Nov 19, 2024
f9586d0
Updated impl. for async rooms get and rooms release
sacOO7 Nov 19, 2024
e0d3621
Added unit tests for Rooms.get suspended method, moved RoomHelper under
sacOO7 Nov 19, 2024
d8f60f8
Added tests for Rooms release operation as per spec
sacOO7 Nov 19, 2024
f3ef0f8
Added test for room release cancelling room Get as per CHA-RC1g4, CHA…
sacOO7 Nov 21, 2024
eaba999
Added spec annotations as per CHA-RC2a
sacOO7 Nov 21, 2024
ef38602
Annotated Rooms.get and Rooms.release methods with related spec ids
sacOO7 Nov 21, 2024
76e0b01
tidy up room lifecycle manager
sacOO7 Nov 21, 2024
40d092c
Bumped up ably-java dependency to latest version 1.2.45
sacOO7 Nov 22, 2024
d5bc58a
Added integration test for Room, having integration with RoomLifecycl…
sacOO7 Nov 22, 2024
ae31f2b
Removed CHA-RL3c related impl. as per latest spec change
sacOO7 Nov 22, 2024
c1a4504
Merge branch 'tests/room-lifecycle-manager-retry' into feature/rooms-…
sacOO7 Nov 22, 2024
9313999
Merge branch 'tests/room-lifecycle-manager-retry' into feature/rooms-…
sacOO7 Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions chat-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.coroutine.test)
testImplementation(libs.bundles.ktor.client)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.junit)
Expand Down
5 changes: 5 additions & 0 deletions chat-android/src/main/java/com/ably/chat/ErrorCodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ enum class ErrorCodes(val errorCode: Int) {
*/
RoomIsReleased(102_103),

/**
* Room was released before the operation could complete.
*/
RoomReleasedBeforeOperationCompleted(102_106),

/**
* Cannot perform operation because the previous operation failed.
*/
Expand Down
6 changes: 2 additions & 4 deletions chat-android/src/main/java/com/ably/chat/Messages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ internal class DefaultMessages(
private val roomId: String,
private val realtimeChannels: AblyRealtime.Channels,
private val chatApi: ChatApi,
) : Messages, ContributesToRoomLifecycleImpl(), ResolvedContributor {
) : Messages, ContributesToRoomLifecycleImpl() {

override val featureName: String = "messages"

Expand All @@ -243,15 +243,13 @@ internal class DefaultMessages(

override val channel = realtimeChannels.get(messagesChannelName, ChatChannelOptions())

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.MessagesAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.MessagesDetachmentFailed

init {
channelStateListener = ChannelStateListener {
if (!it.resumed) updateChannelSerialsAfterDiscontinuity()
if (it.current == ChannelState.attached && !it.resumed) updateChannelSerialsAfterDiscontinuity()
}
channel.on(channelStateListener)
}
Expand Down
4 changes: 1 addition & 3 deletions chat-android/src/main/java/com/ably/chat/Occupancy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,12 @@ data class OccupancyEvent(

internal class DefaultOccupancy(
private val messages: Messages,
) : Occupancy, ContributesToRoomLifecycleImpl(), ResolvedContributor {
) : Occupancy, ContributesToRoomLifecycleImpl() {

override val featureName: String = "occupancy"

override val channel = messages.channel

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.OccupancyAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.OccupancyDetachmentFailed
Expand Down
104 changes: 72 additions & 32 deletions chat-android/src/main/java/com/ably/chat/Presence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

package com.ably.chat

import android.text.PrecomputedText.Params
import io.ably.lib.types.ErrorInfo
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import io.ably.lib.realtime.Channel
import io.ably.lib.realtime.Presence.GET_CLIENTID
import io.ably.lib.realtime.Presence.GET_CONNECTIONID
import io.ably.lib.realtime.Presence.GET_WAITFORSYNC
import io.ably.lib.types.Param
import io.ably.lib.types.PresenceMessage
import io.ably.lib.realtime.Channel as AblyRealtimeChannel
import io.ably.lib.realtime.Presence as PubSubPresence
import io.ably.lib.realtime.Presence.PresenceListener as PubSubPresenceListener

typealias PresenceData = Any
typealias PresenceData = JsonElement

/**
* This interface is used to interact with presence in a chat room: subscribing to presence events,
Expand All @@ -20,14 +26,15 @@ interface Presence : EmitsDiscontinuities {
* Get the underlying Ably realtime channel used for presence in this chat room.
* @returns The realtime channel.
*/
val channel: AblyRealtimeChannel
val channel: Channel

/**
* Method to get list of the current online users and returns the latest presence messages associated to it.
* @param {Ably.RealtimePresenceParams} params - Parameters that control how the presence set is retrieved.
* @returns {Promise<PresenceMessage[]>} or upon failure, the promise will be rejected with an [[Ably.ErrorInfo]] object which explains the error.
* Method to get list of the current online users and returns the latest presence messages associated to it.
* @param {Ably.RealtimePresenceParams} params - Parameters that control how the presence set is retrieved.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
* @returns {List<PresenceMessage>}
*/
suspend fun get(params: List<Params>): List<PresenceMember>
suspend fun get(waitForSync: Boolean = true, clientId: String? = null, connectionId: String? = null): List<PresenceMember>

/**
* Method to check if user with supplied clientId is online
Expand All @@ -39,23 +46,23 @@ interface Presence : EmitsDiscontinuities {
/**
* Method to join room presence, will emit an enter event to all subscribers. Repeat calls will trigger more enter events.
* @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers.
* @returns {Promise<void>} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
*/
suspend fun enter(data: PresenceData?)
suspend fun enter(data: PresenceData? = null)

/**
* Method to update room presence, will emit an update event to all subscribers. If the user is not present, it will be treated as a join event.
* @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers.
* @returns {Promise<void>} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
*/
suspend fun update(data: PresenceData?)
suspend fun update(data: PresenceData? = null)

/**
* Method to leave room presence, will emit a leave event to all subscribers. If the user is not present, it will be treated as a no-op.
* @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers.
* @returns {Promise<void>} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
*/
suspend fun leave(data: PresenceData?)
suspend fun leave(data: PresenceData? = null)

/**
* Subscribe the given listener to all presence events.
Expand Down Expand Up @@ -87,7 +94,7 @@ data class PresenceMember(
/**
* The data associated with the presence member.
*/
val data: PresenceData,
val data: PresenceData?,

/**
* The current state of the presence member.
Expand Down Expand Up @@ -122,50 +129,83 @@ data class PresenceEvent(
/**
* The timestamp of the presence event.
*/
val timestamp: Int,
val timestamp: Long,

/**
* The data associated with the presence event.
*/
val data: PresenceData,
val data: PresenceData?,
)

internal class DefaultPresence(
private val messages: Messages,
) : Presence, ContributesToRoomLifecycleImpl(), ResolvedContributor {
private val clientId: String,
override val channel: Channel,
private val presence: PubSubPresence,
) : Presence, ContributesToRoomLifecycleImpl() {

override val featureName = "presence"

override val channel = messages.channel

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.PresenceAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.PresenceDetachmentFailed

override suspend fun get(params: List<Params>): List<PresenceMember> {
TODO("Not yet implemented")
suspend fun get(params: List<Param>): List<PresenceMember> {
val usersOnPresence = presence.getCoroutine(params)
return usersOnPresence.map { user ->
PresenceMember(
clientId = user.clientId,
action = user.action,
data = (user.data as? JsonObject)?.get("userCustomData"),
updatedAt = user.timestamp,
)
}
Comment on lines +152 to +161
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure safe casting and data retrieval in get method

When mapping PresenceMessage instances to PresenceMember, the line data = (user.data as? JsonObject)?.get("userCustomData") assumes that user.data can be cast to JsonObject. To prevent potential ClassCastExceptions, ensure that user.data is safely cast and handle cases where it might not be a JsonObject.

Apply this diff to enhance type safety:

-return usersOnPresence.map { user ->
-    PresenceMember(
-        clientId = user.clientId,
-        action = user.action,
-        data = (user.data as? JsonObject)?.get("userCustomData"),
-        updatedAt = user.timestamp,
-    )
+return usersOnPresence.map { user ->
+    val data = (user.data as? JsonObject)?.get("userCustomData")
+    PresenceMember(
+        clientId = user.clientId,
+        action = user.action,
+        data = data,
+        updatedAt = user.timestamp,
+    )
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
suspend fun get(params: List<Param>): List<PresenceMember> {
val usersOnPresence = presence.getCoroutine(params)
return usersOnPresence.map { user ->
PresenceMember(
clientId = user.clientId,
action = user.action,
data = (user.data as? JsonObject)?.get("userCustomData"),
updatedAt = user.timestamp,
)
}
suspend fun get(params: List<Param>): List<PresenceMember> {
val usersOnPresence = presence.getCoroutine(params)
return usersOnPresence.map { user ->
val data = (user.data as? JsonObject)?.get("userCustomData")
PresenceMember(
clientId = user.clientId,
action = user.action,
data = data,
updatedAt = user.timestamp,
)
}

}

override suspend fun isUserPresent(clientId: String): Boolean {
TODO("Not yet implemented")
override suspend fun get(waitForSync: Boolean, clientId: String?, connectionId: String?): List<PresenceMember> {
val params = buildList {
if (waitForSync) add(Param(GET_WAITFORSYNC, true))
clientId?.let { add(Param(GET_CLIENTID, it)) }
connectionId?.let { add(Param(GET_CONNECTIONID, it)) }
}
return get(params)
}

override suspend fun isUserPresent(clientId: String): Boolean = presence.getCoroutine(Param(GET_CLIENTID, clientId)).isNotEmpty()

override suspend fun enter(data: PresenceData?) {
TODO("Not yet implemented")
presence.enterClientCoroutine(clientId, wrapInUserCustomData(data))
}

override suspend fun update(data: PresenceData?) {
TODO("Not yet implemented")
presence.updateClientCoroutine(clientId, wrapInUserCustomData(data))
}

override suspend fun leave(data: PresenceData?) {
TODO("Not yet implemented")
presence.leaveClientCoroutine(clientId, wrapInUserCustomData(data))
}

override fun subscribe(listener: Presence.Listener): Subscription {
TODO("Not yet implemented")
val presenceListener = PubSubPresenceListener {
val presenceEvent = PresenceEvent(
action = it.action,
clientId = it.clientId,
timestamp = it.timestamp,
data = (it.data as? JsonObject)?.get("userCustomData"),
)
listener.onEvent(presenceEvent)
}
Comment on lines +188 to +196
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential nulls in subscribe method data casting

In the subscribe method, casting it.data to JsonObject might result in null if it.data is not a JsonObject. To prevent runtime exceptions, add null checks or handle cases where it.data is of a different type.

Apply this diff to improve null safety:

-val data = (it.data as? JsonObject)?.get("userCustomData")
+val data = if (it.data is JsonObject) {
+    (it.data as JsonObject).get("userCustomData")
+} else {
+    null
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val presenceListener = PubSubPresenceListener {
val presenceEvent = PresenceEvent(
action = it.action,
clientId = it.clientId,
timestamp = it.timestamp,
data = (it.data as? JsonObject)?.get("userCustomData"),
)
listener.onEvent(presenceEvent)
}
val presenceListener = PubSubPresenceListener {
val presenceEvent = PresenceEvent(
action = it.action,
clientId = it.clientId,
timestamp = it.timestamp,
data = if (it.data is JsonObject) {
(it.data as JsonObject).get("userCustomData")
} else {
null
},
)
listener.onEvent(presenceEvent)
}


presence.subscribe(presenceListener)

return Subscription {
presence.unsubscribe(presenceListener)
}
}

private fun wrapInUserCustomData(data: PresenceData?) = data?.let {
JsonObject().apply {
add("userCustomData", data)
}
}

override fun release() {
Expand Down
Loading
Loading