Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHA-801] Flow and Jetpack Compose support #116

Merged
merged 3 commits into from
Mar 13, 2025
Merged

Conversation

ttypic
Copy link
Collaborator

@ttypic ttypic commented Mar 6, 2025

  • Added kotlinx.coroutines.flow.Flow support for all callback-based subscription
  • Added com.ably.chat:chat-extensions-compose package with extension functions for better integration with Jetpack Compose.

Summary by CodeRabbit

  • New Features

    • Enhanced reactive capabilities by introducing Kotlin Flow-based APIs for real-time updates on connection status, messages, occupancy, presence events, and typing notifications.
    • Simplified API usage with new default property values that reduce boilerplate code.
    • Introduced experimental composable extensions for Jetpack Compose, enabling seamless integration of live chat data into modern UI components.
    • Improved dependency management and build configurations to support the latest Android and Compose features.
    • Added new annotation for experimental APIs to inform users about potential changes.
  • Bug Fixes

    • Corrected lifecycle management of subscriptions to ensure proper unsubscription after flow completion.

Copy link

coderabbitai bot commented Mar 6, 2025

Walkthrough

The changes implement reactive, flow-based APIs across multiple chat components. Default property values have been introduced alongside new extension functions (e.g., asFlow(), statusAsFlow(), and discontinuityAsFlow()) that convert callback-based subscriptions to Kotlin Flows. A helper method in FlowUtils abstracts this transformation. New composable functions in the chat-extensions-compose module enable state collection in Jetpack Compose, supported by comprehensive tests ensuring proper unsubscription and resource clean-up. Additionally, an experimental annotation is added and Gradle settings updated to include new dependencies and modules.

Changes

File(s) Change Summary
chat-android/src/main/java/com/ably/chat/Connection.kt Added default values for error and retryIn in ConnectionStatusChange and introduced statusAsFlow().
chat-android/src/main/java/com/ably/chat/FlowUtils.kt Added transformCallbackAsFlow() to convert callback APIs into Flows using an unlimited buffer channel.
chat-android/src/main/java/com/ably/chat/{Messages.kt, Occupancy.kt, Presence.kt, RoomReactions.kt, Typing.kt} Added asFlow() extension functions to enable reactive handling of message, occupancy, presence, room reactions, and typing events.
chat-android/src/main/java/com/ably/chat/Room.kt Added statusAsFlow() to emit RoomStatusChange events as a Flow.
chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt Introduced the experimental annotation ExperimentalChatApi for marking experimental APIs.
chat-android/src/main/java/com/ably/chat/Discontinuities.kt Added discontinuityAsFlow() to expose discontinuity events as a Flow.
chat-android/src/test/java/com/ably/chat/{ConnectionTest.kt, MessagesTest.kt, OccupancyTest.kt, PresenceTest.kt, RoomTest.kt, TypingTest.kt} Introduced tests that validate automatic unsubscription and proper event emission from the new Flow-based APIs.
chat-extensions-compose/** Introduced a new module with Gradle configuration, AndroidManifest, ProGuard rules, and a suite of composable functions (collectAsStatus(), collectAsPagingMessagesState(), collectAsOccupancy(), etc.) for integrating reactive flows in Jetpack Compose, along with corresponding tests.
example/{build.gradle.kts, src/main/java/com/ably/chat/example/{MainActivity.kt, ui/PresencePopup.kt}} Updated sample app to integrate the new chat-extensions-compose module and refactored UI components to use composable state collectors.
gradle/libs.versions.toml & settings.gradle.kts Added new dependency versions, modules, and libraries for Jetpack Compose, molecule, and MockK support.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI Component
    participant Conn as Connection
    participant FlowUtil as transformCallbackAsFlow
    participant Channel as Channel
    UI->>Conn: Call statusAsFlow()
    Conn->>FlowUtil: Provide callback lambda
    FlowUtil->>Channel: Create Channel with unlimited buffer
    Conn->>FlowUtil: Emit status update via callback
    FlowUtil->>Channel: Send event to Channel
    Channel->>UI: Emit event to subscriber as Flow
    UI->>Channel: Cancel subscription
    Channel->>FlowUtil: Trigger unsubscribe() on cancellation
Loading
sequenceDiagram
    participant ComposeUI as Composable UI
    participant Collect as collectAsStatus()
    participant Conn as Connection
    participant Flow as statusAsFlow Flow
    ComposeUI->>Collect: Invoke collectAsStatus()
    Collect->>Conn: Call statusAsFlow()
    Conn->>Flow: Emit connection status updates
    Flow->>Collect: Update mutable state via LaunchedEffect
    Collect->>ComposeUI: Render updated connection status
Loading

Suggested reviewers

  • AndyTWF
  • sacOO7

Poem

I'm a chat bunny, hopping through code so bright,
Flows and callbacks transforming with delight.
Reactive streams dance in my ear,
Unsubscribing cleanly, no bugs to fear.
Compose lets my statuses shine through,
With tests that ensure all flows stay true.
Hop on, dear coder—let's celebrate anew!

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot temporarily deployed to staging/pull/116/dokka March 6, 2025 13:04 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (21)
chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt (3)

1-1: Consider more specific suppression reason and file naming.

The @file:Suppress("Filename") suggests there's a naming convention issue being suppressed. Consider either:

  1. Renaming the file to match the contained class (e.g., ExperimentalChatApi.kt)
  2. Using a more specific suppression message that explains why this naming convention is being violated

10-26: Good implementation of experimental API annotation.

The annotation is well-defined with appropriate metadata:

  • @MustBeDocumented ensures API docs include this annotation
  • @RequiresOptIn with warning level is appropriate for experimental features
  • Clear explanation message about potential future changes
  • Comprehensive target list covering all relevant Kotlin constructs

Consider adding a usage example in the documentation to show how developers should opt-in to use APIs marked with this annotation.


17-25: Missing annotation targets for potential use cases.

The current targets don't include PROPERTY_GETTER, VALUE_PARAMETER, or TYPE, which might be needed in the future for more comprehensive coverage.

@Target(
    AnnotationTarget.CLASS,
    AnnotationTarget.TYPEALIAS,
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY,
    AnnotationTarget.FIELD,
    AnnotationTarget.CONSTRUCTOR,
    AnnotationTarget.PROPERTY_SETTER,
+   AnnotationTarget.PROPERTY_GETTER,
+   AnnotationTarget.VALUE_PARAMETER,
+   AnnotationTarget.TYPE
)
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (1)

22-35: Consider optimizing presence update flow.

The implementation correctly handles state management, but there's a potential performance issue in how updates are processed.

Line 30 makes a full get() call after each flow emission, which might be redundant if the flow event already contains all the information needed. This could cause unnecessary network calls or processing.

Consider using the data from the flow event directly if possible, or document why the additional get() call is necessary:

        asFlow().collect {
            if (initialPresenceGet.isActive) initialPresenceGet.cancelAndJoin()
-            presenceMembers = get()
+            // If the event already contains the updated list, use it directly
+            // Otherwise, fetch the latest complete list
+            presenceMembers = get() // Comment explaining why a fresh get() is needed
        }
example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (1)

24-24: Reconsider the LongMethod suppression

Since the implementation has been significantly simplified by using the new Flow-based APIs, it may no longer be necessary to suppress the LongMethod warning.

-@Suppress("LongMethod")
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/RoomReactions.kt (1)

19-22: Consider adding a complementary "removeReaction" function.

Currently, addReaction only increments the counter. If the underlying domain requires decrementing or toggling reactions, you may need an equivalent removal operation.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (4)

19-19: Minor inconsistency in variable declaration.

For consistency with other test classes in this module, consider using private val instead of just val for the connection instance variable.

-    val connection = EmittingConnection()
+    private val connection = EmittingConnection()

19-19: Add the private modifier for consistency.

The connection property should be marked as private for better encapsulation and consistency with other test files.

-    val connection = EmittingConnection()
+    private val connection = EmittingConnection()

16-34: Add test for automatic unsubscription.

Add a test to verify that statusAsFlow() automatically unsubscribes when the flow completes, similar to what's needed in other test classes.

class ConnectionTest {

    private val connection = EmittingConnection()

    @Test
    fun `should return live value for the current connection`() = runTest {
        // existing test code...
    }
+    
+    @Test
+    fun `asFlow() should automatically unsubscribe when it's done`() = runTest {
+        var isUnsubscribed = false
+        val subscription = Subscription { isUnsubscribed = true }
+        
+        val connection = mockk<Connection>()
+        every { connection.onStatusChange(any()) } returns subscription
+        
+        val flow = connection.statusAsFlow()
+        
+        // Cancel the flow and check if unsubscription happened
+        flow.collect { }
+        
+        assertTrue(isUnsubscribed)
+    }
}

38-53: Add class documentation.

Add documentation to explain the purpose of the EmittingConnection class for improved code readability.

+/**
+ * A test implementation of Connection that allows manually emitting connection status changes.
+ * Used for testing reactive components that depend on connection status updates.
+ */
class EmittingConnection(mock: Connection) : Connection by mock {
    // existing implementation...
}
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (2)

58-59: Consider initializing Mutex with a parameter instead of property initializer

The mutex initialization can be simplified by using the constructor parameter.

-    private val mutex = Mutex(locked = false)
+    private val mutex = Mutex()

69-75: Use withLock pattern for pause/resume functions

Consider using the more idiomatic withLock pattern instead of directly calling lock()/unlock() for better error handling and consistency with the get() method.

-    suspend fun pause() {
-        mutex.lock()
-    }
-
-    fun resume() {
-        mutex.unlock()
-    }
+    suspend fun pause() {
+        mutex.lock()
+    }
+
+    suspend fun resume() {
+        if (mutex.isLocked) {
+            mutex.unlock()
+        }
+    }
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (3)

19-19: Add the private modifier for consistency.

The room property should be marked as private for better encapsulation and consistency with other test files.

-    private val room = EmittingRoom()
+    private val room = EmittingRoom()

16-34: Add a test for automatic unsubscription.

Similar to other test classes in this PR, add a test to verify that statusAsFlow() automatically unsubscribes when the flow completes.

class RoomTest {

    private val room = EmittingRoom()

    @Test
    fun `should return live value for room status change`() = runTest {
        moleculeFlow(RecompositionMode.Immediate) {
            room.asComposable()
        }.test {
            assertEquals(RoomStatus.Initialized, awaitItem())
            room.emit(RoomStatusChange(current = RoomStatus.Attached, previous = RoomStatus.Initialized))
            assertEquals(RoomStatus.Attached, awaitItem())
            room.emit(RoomStatusChange(current = RoomStatus.Failed, previous = RoomStatus.Attached))
            assertEquals(RoomStatus.Failed, awaitItem())
            cancel()
        }
    }
+    
+    @Test
+    fun `asFlow() should automatically unsubscribe when it's done`() = runTest {
+        var isUnsubscribed = false
+        val subscription = Subscription { isUnsubscribed = true }
+        
+        val room = mockk<Room>()
+        every { room.onStatusChange(any()) } returns subscription
+        
+        val flow = room.statusAsFlow()
+        
+        // Cancel the flow and check if unsubscription happened
+        flow.collect { }
+        
+        assertTrue(isUnsubscribed)
+    }
}

38-53: Add class documentation.

Add a doc comment explaining the purpose of the EmittingRoom class to improve readability and understanding for other developers.

+/**
+ * A test implementation of Room that allows manually emitting status change events.
+ * Used for testing reactive components that depend on Room status changes.
+ */
class EmittingRoom(mock: Room) : Room by mock {
    private val listeners = mutableListOf<Room.Listener>()

    override val status: RoomStatus = RoomStatus.Initialized

    override fun onStatusChange(listener: Room.Listener): Subscription {
        listeners.add(listener)
        return Subscription { listeners.remove(listener) }
    }

    fun emit(event: RoomStatusChange) {
        listeners.forEach {
            it.roomStatusChanged(event)
        }
    }
}
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (3)

115-115: Mark parameter as private.

The mock parameter doesn't need to be exposed publicly. Mark it as private to improve encapsulation.

-class EmittingPresence(val mock: Presence) : Presence by mock {
+class EmittingPresence(private val mock: Presence) : Presence by mock {

20-111: Add test for automatic unsubscription.

Add a test to verify that asFlow() automatically unsubscribes when the flow completes, similar to what's needed in other test classes.

class PresenceTest {
    private val presence = EmittingPresence()

    @Test
    fun `should return active presence set`() = runTest {
        // existing test code...
    }

    @Test
    fun `should cancel getting the initial presence set if event comes faster`() = runTest {
        // existing test code...
    }
+    
+    @Test
+    fun `asFlow() should automatically unsubscribe when it's done`() = runTest {
+        var isUnsubscribed = false
+        val subscription = Subscription { isUnsubscribed = true }
+        
+        val presence = mockk<Presence>()
+        every { presence.subscribe(any()) } returns subscription
+        
+        val flow = presence.asFlow()
+        
+        // Cancel the flow and check if unsubscription happened
+        flow.collect { }
+        
+        assertTrue(isUnsubscribed)
+    }
}

115-149: Add class documentation.

Add documentation to explain the purpose of the EmittingPresence class to improve code readability.

+/**
+ * A test implementation of Presence that allows manually emitting presence events.
+ * Uses a mutex to control the flow of presence updates for testing different scenarios.
+ */
class EmittingPresence(private val mock: Presence) : Presence by mock {
    // existing implementation...
}
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomReactionsTest.kt (3)

19-19: Add the private modifier for consistency.

The roomReactions property should be marked as private for better encapsulation and consistency.

-    private val roomReactions = EmittingRoomReactions()
+    private val roomReactions = EmittingRoomReactions()

16-49: Add test for automatic unsubscription.

Add a test to verify that asFlow() automatically unsubscribes when the flow is completed, similar to what's needed in other test classes.

class RoomReactionsTest {

    private val roomReactions = EmittingRoomReactions()

    @Test
    fun `should correctly count reactions`() = runTest {
        // existing test code...
    }
+    
+    @Test
+    fun `asFlow() should automatically unsubscribe when it's done`() = runTest {
+        var isUnsubscribed = false
+        val subscription = Subscription { isUnsubscribed = true }
+        
+        val reactions = mockk<RoomReactions>()
+        every { reactions.subscribe(any()) } returns subscription
+        
+        val flow = reactions.asFlow()
+        
+        // Cancel the flow and check if unsubscription happened
+        flow.collect { }
+        
+        assertTrue(isUnsubscribed)
+    }
}

53-66: Add class documentation.

Add documentation to explain the purpose of the EmittingRoomReactions class for better code readability.

+/**
+ * A test implementation of RoomReactions that allows manually emitting reaction events.
+ * Used for testing reactive components that depend on reaction updates.
+ */
class EmittingRoomReactions(mock: RoomReactions) : RoomReactions by mock {
    // existing implementation...
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 061eae0 and 328be26.

📒 Files selected for processing (38)
  • chat-android/src/main/java/com/ably/chat/Connection.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Messages.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Occupancy.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Presence.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Room.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/RoomReactions.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Typing.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/ConnectionTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/MessagesTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/OccupancyTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/PresenceTest.kt (3 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomTest.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/TypingTest.kt (3 hunks)
  • chat-extensions-compose/build.gradle.kts (1 hunks)
  • chat-extensions-compose/gradle.properties (1 hunks)
  • chat-extensions-compose/proguard-rules.pro (1 hunks)
  • chat-extensions-compose/src/main/AndroidManifest.xml (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/RoomReactions.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomReactionsTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/src/main/java/com/ably/chat/example/MainActivity.kt (7 hunks)
  • example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
  • settings.gradle.kts (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • chat-extensions-compose/src/main/AndroidManifest.xml
  • chat-extensions-compose/gradle.properties
  • chat-extensions-compose/proguard-rules.pro
🧰 Additional context used
🧠 Learnings (6)
chat-android/src/main/java/com/ably/chat/Typing.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/main/java/com/ably/chat/Typing.kt:178-183
Timestamp: 2024-11-28T11:10:20.947Z
Learning: In `chat-android/src/main/java/com/ably/chat/Typing.kt`, within the `DefaultTyping` class, the `stop()` method must execute within `typingScope` (a `CoroutineScope` with parallelism set to 1) to avoid race conditions when setting and cancelling `typingJob`.
chat-android/src/test/java/com/ably/chat/RoomTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomReactionsTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/OccupancyTest.kt:20-31
Timestamp: 2024-11-28T11:12:06.843Z
Learning: In `OccupancyTest.kt`, additional room state initialization using `setState` is unnecessary for testing occupancy functionality, as the specification doesn't require it.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: coverage
  • GitHub Check: check
🔇 Additional comments (70)
chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt (1)

5-9: Clarify the intended audience for this annotation.

The KDoc states this API is "internal" and "not intended to be used outside Ably," but experimental APIs are typically intended for external users to try with caution. Consider clarifying whether this is:

  1. Truly internal (then it may not need @RequiresOptIn)
  2. Experimental but available for users (then update the documentation)
settings.gradle.kts (1)

5-5: New module added to support Compose integration.

The addition of the chat-extensions-compose module aligns with the PR objective to add Jetpack Compose support to the Ably Chat Kotlin library.

chat-android/src/main/java/com/ably/chat/RoomReactions.kt (2)

11-11: Added Flow support for reactive programming.

The import of Kotlin Flow aligns with the PR objective to support reactive programming.


64-69: Flow extension function implementation looks good.

The implementation follows a clean and consistent pattern for transforming callback-based APIs into Flow-based APIs, which matches the PR objective.

chat-android/src/main/java/com/ably/chat/Typing.kt (3)

22-22: Added Flow support for reactive programming.

The import of Kotlin Flow aligns with the PR objective to support reactive programming.


92-97: Flow extension function implementation looks good.

The implementation follows a clean and consistent pattern for transforming callback-based APIs into Flow-based APIs, which matches the PR objective.


192-192: Good fix for potential memory leak.

Setting typingJob to null after cancellation ensures proper cleanup of resources and prevents potential memory leaks. This also helps address potential race conditions when setting and cancelling typingJob as noted in the retrieved learning.

chat-android/src/main/java/com/ably/chat/Messages.kt (2)

12-12: Added Flow support for reactive programming.

The import of Kotlin Flow aligns with the PR objective to support reactive programming.


125-130: Flow extension function implementation looks good.

The implementation follows a clean and consistent pattern for transforming callback-based APIs into Flow-based APIs, which matches the PR objective.

chat-android/src/main/java/com/ably/chat/Room.kt (2)

10-10: Import addition for Flow API

The import for kotlinx.coroutines.flow.Flow supports the new Flow-based API being introduced.


118-123: New function to expose Room status events as a Flow

This extension function provides a reactive way to observe room status changes, aligning with Kotlin's coroutines and Flow API standards. The implementation correctly leverages the existing callback-based API and properly handles subscription lifecycle.

chat-android/src/main/java/com/ably/chat/Presence.kt (2)

11-11: Import addition for Flow API

The import for kotlinx.coroutines.flow.Flow supports the new Flow-based API being introduced.


85-90: New function to expose presence events as a Flow

This extension function provides a reactive way to observe presence events using Kotlin's Flow API. The implementation correctly transforms the callback-based subscription into a Flow and ensures proper resource cleanup through the underlying transformCallbackAsFlow utility.

chat-android/src/main/java/com/ably/chat/FlowUtils.kt (1)

1-23: Well-implemented utility for converting callbacks to flows

This utility function elegantly transforms callback-based APIs into Kotlin Flows. The implementation includes several important aspects:

  1. Proper documentation explaining its purpose
  2. Use of Channel.UNLIMITED to handle backpressure without dropping events
  3. Correct resource cleanup in the finally block to prevent subscription leaks
  4. Generic type parameter allowing reuse across different event types

The implementation aligns with reactive programming best practices and enables a consistent pattern for Flow conversion throughout the codebase.

chat-android/src/main/java/com/ably/chat/Occupancy.kt (2)

15-15: Import addition for Flow API

The import for kotlinx.coroutines.flow.Flow supports the new Flow-based API being introduced.


59-64: New function to expose occupancy events as a Flow

This extension function provides a reactive way to observe occupancy events using Kotlin's Flow API. The implementation correctly transforms the callback-based subscription into a Flow, consistent with other reactive interfaces in the library.

chat-android/src/main/java/com/ably/chat/Connection.kt (3)

13-13: New Flow import added for reactive programming support

The addition of the Flow import enables the new reactive programming features being added to the library.


75-75: Good addition of default values to simplify API usage

Adding default null values to these optional parameters is a good improvement that simplifies the API usage. This change allows consumers to omit these parameters when they're not needed, making the code more concise.

Also applies to: 80-80


116-121: Great addition of Flow-based API

The new statusAsFlow() extension function provides a clean, reactive way to consume connection status changes. This aligns with modern Kotlin development practices and will significantly improve the developer experience when working with state changes.

The function properly leverages the existing callback mechanism while providing a more composable, reactive alternative.

example/build.gradle.kts (1)

62-62: Correctly integrated new Compose extensions module

Adding the new :chat-extensions-compose module as a dependency to the example project is necessary to demonstrate and test the new Compose integration features.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (1)

1-28: Well-implemented Compose integration for Room status

This implementation follows Jetpack Compose best practices:

  1. Properly marked as experimental with @ExperimentalChatApi
  2. Uses remember with the room instance as key to avoid recreating state unnecessarily
  3. Correctly uses LaunchedEffect with the room instance as key to manage the lifecycle of the Flow collection
  4. Efficiently updates and returns room status

The implementation will automatically recompose UI elements when room status changes, making it very easy to build reactive UIs with the Ably Chat library.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt (1)

1-28: Well-implemented Compose integration for typing indicators

This implementation follows the same solid pattern as the Room composable:

  1. Properly marked as experimental with @ExperimentalChatApi
  2. Uses remember with the typing instance as key for state stability
  3. Correctly uses LaunchedEffect to manage Flow collection lifecycle
  4. Returns a reactive set of currently typing users

The code is clean, focused, and will efficiently update the UI when typing status changes. This is a valuable addition for chat UIs that need to show typing indicators.

chat-android/src/test/java/com/ably/chat/OccupancyTest.kt (1)

156-177: Well-structured test for Flow API integration!

This test effectively verifies that the asFlow() extension function properly manages the subscription lifecycle by unsubscribing when the Flow is canceled. The approach using Turbine for testing flows is clean and follows best practices.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (1)

17-27: Good implementation of Compose integration

The implementation follows Jetpack Compose best practices:

  • Properly annotated with @ExperimentalChatApi to indicate experimental status
  • Using remember with the connection instance as a key to prevent unnecessary state recreation
  • Using LaunchedEffect to manage flow subscription within the composition lifecycle
  • Clear return type and documentation
chat-android/src/test/java/com/ably/chat/TypingTest.kt (1)

139-160: Consistent test approach for Flow API

This test maintains consistency with the testing approach used in OccupancyTest, properly verifying that the asFlow() extension function automatically unsubscribes when the flow is canceled. This consistent pattern across different components demonstrates good testing practices.

chat-android/src/test/java/com/ably/chat/RoomTest.kt (1)

11-35: Good test coverage for Room status flow

The test effectively verifies the automatic unsubscription behavior of statusAsFlow(). This is important to prevent memory leaks and ensure proper resource cleanup. The test structure follows the same pattern as in other components, which is good for maintainability.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (4)

1-14: Well-structured imports and package organization.

The imports are well-organized and include all necessary components for Compose integration with proper separation between core Android, Compose, and Ably Chat imports.


15-18: Good use of data class with default values.

The CurrentOccupancy data class is appropriately defined with default values, making it easy to instantiate and use in Compose UIs.


23-25: Appropriate annotation usage.

The @ExperimentalChatApi annotation clearly indicates this is an experimental feature, and the @Composable annotation properly marks this function for use within Compose UI.


26-46: Well-implemented Compose state management.

The implementation properly manages state through:

  • Using remember with the correct key to reset state when the Occupancy instance changes
  • Proper coroutine cancellation with cancelAndJoin when new data arrives
  • Using LaunchedEffect scoped to the Occupancy instance for proper lifecycle management

This ensures that the occupancy data stays up-to-date and prevents memory leaks.

chat-extensions-compose/build.gradle.kts (5)

1-7: Appropriate plugins for Compose module.

The selection of plugins is appropriate for a Compose-enabled Android library module, including the necessary Compose compiler plugin.


11-32: Properly configured Android library setup.

The Android configuration is well-structured with:

  • Appropriate namespace for Compose extensions
  • Modern compileSdk (34) with reasonable minSdk (24)
  • Properly enabled Compose features
  • ConsumerProguardFiles for library consumption

This ensures good compatibility while maintaining a modern feature set.


34-48: Compatible Java and Kotlin configuration.

The Java and Kotlin version configurations are aligned at 1.8, which is appropriate for maintaining compatibility with a wide range of Android versions while enabling modern language features.


51-53: Good use of Kotlin explicit API mode.

The explicitApi() configuration enhances API clarity and forces developers to be intentional about public API surfaces.


55-64: Comprehensive dependency setup.

The dependencies include all necessary components for Compose development and testing:

  • Base chat-android module dependency
  • Core Compose libraries
  • Comprehensive testing libraries (JUnit, MockK, Turbine, etc.)

This ensures the module can be properly developed and tested.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (2)

1-15: Well-structured imports and package organization.

The imports are well-organized and include all necessary components for Compose integration.


19-21: Appropriate annotation usage.

The experimental API is properly annotated, making it clear to developers that this API might change in future releases.

chat-android/src/test/java/com/ably/chat/ConnectionTest.kt (2)

3-13: Appropriate test imports added.

The new imports support the flow testing needs with Turbine and MockK.


151-172: Well-structured flow unsubscription test.

The test effectively verifies that the flow automatically unsubscribes when canceled:

  • Properly mocks the Connection and Subscription
  • Captures the callback for verification
  • Uses Turbine for testing flow emissions
  • Verifies the unsubscription behavior with exact verification count

This ensures that resources are properly cleaned up when flows are canceled, preventing potential memory leaks.

chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt (1)

95-116: Good test coverage for the Flow API

This test properly verifies that when using asFlow() and canceling the Flow, the underlying subscription is automatically unsubscribed, preventing memory leaks and unnecessary resource consumption.

Consider renaming the test to "asFlow() should automatically unsubscribe when it's done" (changing "then" to "when") for better clarity.

chat-android/src/test/java/com/ably/chat/MessagesTest.kt (2)

3-3: Good addition of turbine for Flow testing

The turbine library is the appropriate choice for testing Flow-based APIs.


240-261: Good test coverage for the Messages Flow API

This test correctly verifies the automatic unsubscription behavior when a Flow is canceled, following a consistent pattern with other similar tests in the codebase.

Consider renaming the test to "asFlow() should automatically unsubscribe when it's done" (changing "then" to "when") for better clarity.

chat-android/src/test/java/com/ably/chat/PresenceTest.kt (1)

150-171: Good test coverage for the Presence Flow API

This test correctly verifies that the Flow properly handles subscription lifecycle, ensuring resources are cleaned up when the Flow is canceled.

Consider renaming the test to "asFlow() should automatically unsubscribe when it's done" (changing "then" to "when") for better clarity.

example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (2)

18-21: Good addition of necessary imports for Flow & Compose integration

The new imports support the changes to use Compose-specific extensions and the experimental API annotation.


25-31: Excellent simplification using the new Compose extensions

The refactoring significantly simplifies the implementation by:

  1. Changing the parameter from ChatClient to Room for more direct access
  2. Using the new asComposable() extension to automatically handle presence state updates
  3. Properly marking usage of experimental APIs with @OptIn

This is a great example of how the new Flow and Compose support improves code quality.

gradle/libs.versions.toml (1)

17-17: Dependency updates look good.

All newly introduced library references (Compose Foundation, Molecule, and Android-specific MockK) appear consistent with project requirements. If you haven’t already, consider verifying that these versions are stable or compatible with your Android Gradle plugin before finalizing.

Also applies to: 23-23, 40-40, 53-53, 61-61

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/RoomReactions.kt (2)

14-17: Data class definitions look solid.

Default values for ReactionCounter fields provide flexibility and simplify instantiation.


30-39: Validate map-based state updates for concurrency and removal scenarios.

  1. Each new reaction results in a newly copied map, which is typically acceptable in Compose but can be expensive if reaction volume is high.
  2. There's no removal or decrement logic if a reaction is retracted. Confirm that this one-sided incrementation aligns with your app’s requirements.
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (2)

74-79: Interface structure is clear.

The PaginatedMessages interface straightforwardly exposes loaded data, scrolling state, loading status, and pagination info.


81-86: Implementation of PaginatedMessages is clean and straightforward.

Returning a simple data class ensures immutability and easy usage in Compose contexts.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1)

1-54: Well-structured test for Room's Compose integration!

The test effectively verifies that room status changes are properly reflected in Compose. The EmittingRoom mock implementation follows good practices for testing reactive streams.

A few observations:

  • Good use of the Molecule library for testing composable state updates
  • Clean implementation of the test double with proper listener management
  • Appropriate use of @OptIn(ExperimentalChatApi::class) annotation to acknowledge the experimental nature of the API

The test coverage is focused on the "happy path" but adequately verifies the fundamental functionality.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (1)

1-150: Strong implementation of Presence testing with Compose!

The PresenceTest class provides thorough testing of presence functionality in a Compose context, with two well-structured test cases that cover both normal operation and an edge case (when events arrive before initial presence set is loaded).

I particularly appreciate:

  • The use of Mutex for thread safety in the test implementation
  • The comprehensive testing of presence state changes with detailed assertions
  • The pause/resume functionality to simulate asynchronous behavior scenarios

This is a strong example of how to test reactive state management with Compose.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomReactionsTest.kt (1)

1-67: Good implementation of reaction counting for Compose integration!

The test effectively verifies that reaction events are correctly counted and reflect the appropriate state in Compose. The implementation correctly handles:

  • Initial empty state
  • Updating counts when new reactions arrive
  • Tracking whether the current user has reacted

This test properly verifies the core functionality needed for displaying reaction counts in a UI.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1)

1-54: Well-structured test for Connection's Compose integration!

The test effectively verifies that connection status changes are properly reflected in Compose UI components. The approach follows the same pattern as other Compose integration tests, which maintains consistency across the codebase.

The test progression through different connection states (Initialized → Connecting → Connected) provides good coverage of the typical connection lifecycle.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (4)

4-5: Good use of Molecule for testing Composable state

Using Molecule's moleculeFlow function is an excellent choice for testing reactive Composable state transformations, as it allows for controlled testing of state changes over time.


17-18: Appropriate use of ExperimentalChatApi annotation

Using the OptIn annotation for ExperimentalChatApi correctly signals to consumers that they're using APIs that might change.


21-34: Well-structured test with clear assertions

This test effectively verifies the reactive behavior of occupancy updates through the Composable API. The sequential emission and verification pattern makes the test intent clear.


36-50: Good edge case testing

Testing the cancellation of initial occupancy retrieval when a new event arrives demonstrates thorough consideration of concurrent scenarios that might occur in real applications.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (4)

15-19: Consistent experimental API opt-in approach

The use of @OptIn(ExperimentalChatApi::class) matches the pattern seen in other files, maintaining consistency across the codebase.


20-32: Clear test structure with explicit state verification

This test effectively validates the reactive typing updates through the Composable API, with clear assertions at each step of the state evolution.


37-43: Clean delegate pattern implementation

Good use of the Kotlin delegation pattern (by mock) combined with overriding specific methods for the test fixture. This keeps the test class focused and minimal.


45-49:

❓ Verification inconclusive

Consider adding a missing initial state

The EmittingTyping class doesn't have an initial state like EmittingOccupancy does. Consider adding one for consistency or documenting why it's not needed.


🏁 Script executed:

#!/bin/bash
# Let's check if the Typing interface has a get() method for initial state like Occupancy does
rg -A 5 "interface Typing" || echo "Typing interface not found"

Length of output: 553


Action: Evaluate the need for an initial state in EmittingTyping

The current implementation in TypingTest.kt (lines 45–49) properly emits typing events, but note that unlike EmittingOccupancy, there isn’t any provision for an initial state. Our verification confirms that the Typing interface (in chat-android/src/main/java/com/ably/chat/Typing.kt) only defines a channel property and does not offer a method or property to retrieve an initial state. For consistency across event types, please consider either:

  • Adding a default initial state to EmittingTyping, similar to what exists for occupancy events, or
  • Documenting explicitly why an initial state isn’t applicable for typing events.
example/src/main/java/com/ably/chat/example/MainActivity.kt (8)

47-49: Good import organization for new functionality

The imports clearly reflect the addition of Flow and Compose support, with appropriate imports for experimental annotations and the new extension functions.

Also applies to: 53-54, 56-57


77-77: Improved configuration with explicit log level

Using the new ChatClientOptions with an explicit log level improves code clarity and makes configuration intent more obvious.


92-93: Smart use of safe call with elvis operator for nullability handling

The ?. safe call operator with the ?: elvis operator provides a clean way to handle the initially null room state while preventing null pointer exceptions.


95-102: Improved lifecycle management with LaunchedEffect

Replacing (presumably) DisposableEffect with LaunchedEffect simplifies the room lifecycle management. The coroutine-based approach is more idiomatic for Kotlin and Jetpack Compose.


131-133: Consistent nullability checks

Both UI sections correctly use the room?.let { ... } pattern to ensure null safety when working with the room object.

Also applies to: 136-140


153-154: Effective use of Compose extensions for reactive data

Using asComposable() extensions for messages, reactions, and typing provides a clean reactive programming model that integrates well with Compose's state management.


187-188: Proper integration with pagination state

The integration with paginatedMessages.listState and the use of paginatedMessages.loaded for accessing messages shows a good understanding of the pagination APIs.

Also applies to: 189-195


229-232: Improved reaction formatting display

The formatting of received reactions now includes counts, which provides more valuable information to users.

@ttypic ttypic force-pushed the CHA-801/flow-and-compose branch from 328be26 to f8dce90 Compare March 6, 2025 15:12
@github-actions github-actions bot temporarily deployed to staging/pull/116/dokka March 6, 2025 15:15 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1)

52-59: 🛠️ Refactor suggestion

Add error handling for getPreviousMessages() failures.

If fetching previous messages fails, the UI remains in the loading state without displaying any errors. Provide a retry option or user notification to improve user experience.

52     coroutineScope.launch {
53         loading = true
-        val receivedPaginatedResult = subscription.getPreviousMessages()
-        lastReceivedPaginatedResult = receivedPaginatedResult
-        loaded = receivedPaginatedResult.items.reversed() + loaded
-        loading = false
-        if (loaded.isNotEmpty()) listState.animateScrollToItem(loaded.size - 1)
+        try {
+            val receivedPaginatedResult = subscription.getPreviousMessages()
+            lastReceivedPaginatedResult = receivedPaginatedResult
+            loaded = receivedPaginatedResult.items.reversed() + loaded
+        } catch (e: Exception) {
+            // Handle error, e.g., logging or presenting a retry option
+        } finally {
+            loading = false
+        }
+        if (loaded.isNotEmpty()) {
+            listState.animateScrollToItem(loaded.size - 1)
+        }
🧹 Nitpick comments (14)
example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (1)

28-28: Simplified presence subscription with reactive paradigm.

The use of asComposable() elegantly replaces what was likely complex subscription management code. This declarative approach aligns perfectly with Compose's reactive paradigm.

Consider adding a comment explaining that this automatically handles subscription lifecycle with Compose, which might help other developers understand this pattern.

example/src/main/java/com/ably/chat/example/MainActivity.kt (2)

95-102: Improved lifecycle management with LaunchedEffect

Replacing DisposableEffect with LaunchedEffect simplifies the room attachment logic. However, consider adding error handling for the attachment process.

 LaunchedEffect(Unit) {
-    val chatRoom = chatClient.rooms.get(
-        Settings.ROOM_ID,
-        RoomOptions.default,
-    )
-    chatRoom.attach()
-    room = chatRoom
+    try {
+        val chatRoom = chatClient.rooms.get(
+            Settings.ROOM_ID,
+            RoomOptions.default,
+        )
+        chatRoom.attach()
+        room = chatRoom
+    } catch (e: Exception) {
+        // Handle error (e.g., log, show error message)
+        println("Failed to attach to room: ${e.message}")
+    }
 }

228-233: Enhanced reaction display with count information

The formatting of received reactions now includes counts, providing more useful information to users.

Consider extracting this formatting logic into a separate function for better readability:

- Text(
-     "Received reactions: ${receivedReactions.map { "${it.key} - ${it.value.count}" }.joinToString()}",
-     modifier = Modifier.padding(16.dp),
- )
+ Text(
+     "Received reactions: ${formatReactions(receivedReactions)}",
+     modifier = Modifier.padding(16.dp),
+ )

// Add this function outside the composable
+ private fun formatReactions(reactions: Map<String, ReactionSummary>) =
+     reactions.map { "${it.key} - ${it.value.count}" }.joinToString()
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (3)

36-36: Avoid unbounded growth in loaded messages.

Repeatedly appending messages to loaded can lead to unbounded memory usage if old messages are never pruned. Consider adding periodic cleanup or pagination boundaries to maintain a manageable list size over time.


58-58: Consider user experience before forcibly scrolling to the last item.

Automatically scrolling to the bottom on every addition might interrupt users reading older messages. Consider conditionally scrolling only when the user is already near the end or when they desire to jump to the latest message.


66-72: Implement a "load more" mechanism with hasMore.

You’re exposing hasMore but not providing a function to load additional messages when this flag is true. Add a method to gracefully load older messages once the user scrolls up or triggers a "Load More" action.

chat-android/src/test/java/com/ably/chat/PresenceTest.kt (1)

150-171: Typo in test name - "then" should be "when".

The test correctly verifies that flows automatically unsubscribe when completed, which is crucial for preventing memory leaks. However, there's a small typo in the function name: asFlow() should automatically unsubscribe then it's done should be asFlow() should automatically unsubscribe when it's done.

-fun `asFlow() should automatically unsubscribe then it's done`() = runTest {
+fun `asFlow() should automatically unsubscribe when it's done`() = runTest {
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (2)

125-135: Consider adding timeout to mutex lock operation.

The mutex lock in the get method could potentially cause indefinite blocking if the mutex is locked and never unlocked. Consider adding a timeout parameter to the withLock call to prevent potential deadlocks in tests.

-override suspend fun get(waitForSync: Boolean, clientId: String?, connectionId: String?): List<PresenceMember> = mutex.withLock {
+override suspend fun get(waitForSync: Boolean, clientId: String?, connectionId: String?): List<PresenceMember> = mutex.withLock(timeoutMillis = 5000) {

137-148: Consider clarifying the emit method name.

The emit method name is somewhat generic. Consider renaming it to be more specific about what type of event is being emitted, such as emitPresenceEvent to improve code readability and maintainability.

-fun emit(event: PresenceEvent) {
+fun emitPresenceEvent(event: PresenceEvent) {

Remember to update all call sites accordingly.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1)

48-52: Consider clarifying the emit method name.

Similar to the PresenceTest, the emit method name is generic. Consider renaming it to be more specific about what type of event is being emitted, such as emitStatusChange to improve code readability.

-fun emit(event: ConnectionStatusChange) {
+fun emitStatusChange(event: ConnectionStatusChange) {

Remember to update all call sites accordingly.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (2)

48-52: Consider clarifying the emit method name.

As with the other test classes, the emit method name could be more specific. Consider renaming it to emitStatusChange for consistency and improved code readability.

-fun emit(event: RoomStatusChange) {
+fun emitStatusChange(event: RoomStatusChange) {

Remember to update all call sites accordingly.


36-37: Consider adding documentation to factory function.

The factory function EmittingRoom() would benefit from KDoc documentation explaining its purpose and usage in tests.

+/**
+ * Creates an instance of EmittingRoom with a mock Room implementation.
+ * Used for testing room status changes in a composable context.
+ */
fun EmittingRoom() = EmittingRoom(mockk())
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (2)

37-50: Add auto-unsubscription test for consistency

The EmittingTyping implementation is correct, but unlike what's mentioned in the AI summary, there's no test to verify that flows automatically unsubscribe when they're done.

For consistency with other tests (like the one mentioned for Occupancy), consider adding an explicit test for this behavior.

+ @Test
+ fun `asFlow() should automatically unsubscribe then it's done`() = runTest {
+     val typing = mockk<Typing>()
+     val subscription = mockk<Subscription>()
+     val listener = slot<Typing.Listener>()
+     val typingEvent = TypingEvent(setOf("client_1"))
+ 
+     every { typing.subscribe(capture(listener)) } answers {
+         subscription
+     }
+     every { subscription.unsubscribe() } just runs
+ 
+     val flow = typing.asFlow()
+     flow.test {
+         listener.captured.onEvent(typingEvent)
+         assertEquals(typingEvent, awaitItem())
+         cancel()
+     }
+ 
+     verify(exactly = 1) { subscription.unsubscribe() }
+ }

37-50: Consider adding thread safety for consistency

Unlike EmittingOccupancy, this class doesn't use a Mutex for thread safety. While it may not be necessary for these specific tests, adding it would provide consistency between the test implementations.

Consider adding a mutex similar to the OccupancyTest implementation if thread safety might be a concern, especially if these tests could run in parallel.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 328be26 and f8dce90.

📒 Files selected for processing (38)
  • chat-android/src/main/java/com/ably/chat/Connection.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Messages.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Occupancy.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Presence.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Room.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/RoomReactions.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Typing.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/ConnectionTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/MessagesTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/OccupancyTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/PresenceTest.kt (3 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomTest.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/TypingTest.kt (3 hunks)
  • chat-extensions-compose/build.gradle.kts (1 hunks)
  • chat-extensions-compose/gradle.properties (1 hunks)
  • chat-extensions-compose/proguard-rules.pro (1 hunks)
  • chat-extensions-compose/src/main/AndroidManifest.xml (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/RoomReactions.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomReactionsTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/src/main/java/com/ably/chat/example/MainActivity.kt (7 hunks)
  • example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
  • settings.gradle.kts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (25)
  • chat-extensions-compose/src/main/AndroidManifest.xml
  • chat-extensions-compose/proguard-rules.pro
  • chat-android/src/main/java/com/ably/chat/RoomReactions.kt
  • chat-android/src/main/java/com/ably/chat/Messages.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt
  • settings.gradle.kts
  • chat-android/src/test/java/com/ably/chat/TypingTest.kt
  • chat-android/src/main/java/com/ably/chat/Presence.kt
  • chat-android/src/main/java/com/ably/chat/Typing.kt
  • chat-android/src/main/java/com/ably/chat/Room.kt
  • chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt
  • chat-extensions-compose/build.gradle.kts
  • chat-extensions-compose/gradle.properties
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt
  • example/build.gradle.kts
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/RoomReactions.kt
  • chat-android/src/main/java/com/ably/chat/Connection.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomReactionsTest.kt
  • gradle/libs.versions.toml
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt
  • chat-android/src/test/java/com/ably/chat/MessagesTest.kt
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt
🧰 Additional context used
🧠 Learnings (3)
chat-android/src/test/java/com/ably/chat/RoomTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/OccupancyTest.kt:20-31
Timestamp: 2024-11-28T11:12:06.843Z
Learning: In `OccupancyTest.kt`, additional room state initialization using `setState` is unnecessary for testing occupancy functionality, as the specification doesn't require it.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: check
  • GitHub Check: coverage
  • GitHub Check: build
🔇 Additional comments (33)
example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (5)

18-20: Good integration of Compose extensions.

The addition of these imports shows proper integration with the new Compose extensions module. The explicit import of the experimental API annotation is good practice for awareness.


25-25: Appropriate use of OptIn annotation.

Using the @OptIn annotation is the correct approach when working with experimental APIs, as it acknowledges the potential for API changes in future releases.


27-27: Improved parameter specificity.

Changing from chatClient to room parameter is a good design decision. It provides a more specific scope for the component, better following the principle of least privilege by only providing access to what's needed.


30-30: Direct access to presence is consistent with the new parameter.

This change correctly aligns with the parameter update from chatClient to room.


18-87: Verify end-to-end function compatibility.

The refactoring looks solid, but it would be good to verify that the functionality remains the same with the new Flow-based implementation, especially regarding:

  • Initial loading of presence data
  • Reactive updates when members join/leave/update status
  • Proper cleanup of resources when the component is disposed

Can you confirm this component has been manually tested in the sample app to ensure the presence functionality works as expected with the new Flow-based implementation?

example/src/main/java/com/ably/chat/example/MainActivity.kt (10)

34-35: Great job updating the imports for the new Flow and Compose support

The imports appropriately include the new ExperimentalChatApi annotation and the asComposable extension function, which are central to the Jetpack Compose integration.

Also applies to: 47-49, 53-54, 56-57


77-78: Improved ChatClient initialization with LogLevel configuration

Adding the ChatClientOptions with explicit log level is a good practice for the example app, making it clearer to users how to configure logging.


88-89: Appropriate usage of OptIn annotation for experimental API

The @OptIn(ExperimentalChatApi::class) annotation correctly signals to users that the new Compose integration APIs are experimental.


92-94: Clean implementation of typing indicator with Flow conversion

Good use of the new asComposable() extension for the typing indicator, with appropriate null-safety handling using the Elvis operator.


131-134: Cleaner room handling with null safety

The room null safety check with the let scope function ensures UI components are only rendered when the room is available.


136-140: Updated presence popup to use room directly

Passing the room object directly to the PresencePopup instead of chatClient aligns with the new approach of having component-specific data management.


145-146: Properly marked Chat composable with experimental annotation

The experimental annotation makes it clear to users that the Compose integration APIs are still under development.


150-155: Excellent use of Flow-based APIs for messages and reactions

The implementation properly leverages the new Flow-based APIs through the asComposable() extensions, which simplifies state management and reactivity.


187-188: Smart integration with Compose's LazyColumn state

Using the paginated messages list state directly provides automatic scrolling behavior, a great example of the benefits of the new integration.


189-195: Simplified message iteration with pagination support

The new approach properly handles the paginated message list, making it easy to display messages with their indices.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1)

33-50: Be mindful of re-subscription behavior during recomposition.

When the composable is recomposed with a new Messages instance, this disposable effect will re-subscribe. Verify that this behavior is intentional and that previous subscriptions do not accumulate or conflict.

chat-android/src/main/java/com/ably/chat/Occupancy.kt (2)

15-15: LGTM: Added Flow import for new asFlow extension function

This import is necessary for the newly added flow functionality, consistent with the PR objective to add Flow support across callback-based subscriptions.


59-64: Well-implemented Flow transformation for Occupancy events

The asFlow() extension function provides a clean reactive programming interface for the Occupancy events, transforming the callback-based subscription model into Kotlin's Flow API. This implementation aligns perfectly with the PR's goal to support Kotlin Flow across callback-based subscriptions.

The documentation is clear and concise, though consider adding a small usage example in the future for developers unfamiliar with Flow APIs.

chat-android/src/test/java/com/ably/chat/OccupancyTest.kt (2)

3-3: LGTM: Added turbine test import for Flow testing

This import supports the new test case for the asFlow functionality.


156-177: LGTM: Good test coverage for automatic unsubscription behavior

This test properly verifies that the Flow implementation automatically unsubscribes when the flow is canceled, preventing potential memory leaks and resource issues. The test structure is clean and follows the established patterns in the codebase.

chat-android/src/test/java/com/ably/chat/RoomTest.kt (1)

1-36: LGTM: Well-structured test class for Room statusAsFlow functionality

This new test class follows the same pattern as other Flow-related tests in the codebase. It properly verifies that:

  1. The statusAsFlow function correctly emits room status changes
  2. The subscription is automatically unsubscribed when the flow is canceled

This test is important for ensuring proper resource management when using the reactive Flow-based API for room status updates.

chat-android/src/test/java/com/ably/chat/ConnectionTest.kt (2)

3-3: LGTM: Added necessary imports for Flow testing

These imports support the new test case for the statusAsFlow functionality.

Also applies to: 10-10, 13-13


151-172: LGTM: Consistent test implementation for connection status Flow

This test properly verifies that the Flow implementation automatically unsubscribes when the flow is canceled, maintaining the pattern established in other components. The test structure is clean and follows the established patterns in the codebase.

chat-android/src/test/java/com/ably/chat/PresenceTest.kt (1)

3-16: Good addition of necessary imports.

The imports are correctly added to support the new flow testing functionality, particularly app.cash.turbine.test for flow testing and io.mockk.verify for verification.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (2)

20-111: Well-structured tests for Compose integration.

The tests effectively verify that asComposable() correctly transforms presence events into a reactive state that can be observed in Jetpack Compose. The use of moleculeFlow with RecompositionMode.Immediate is appropriate for testing composable state, and the tests cover both the happy path and a potential race condition.


129-135: Risk of deadlock with manual mutex operations.

The pause() and resume() methods directly call lock() and unlock() on the mutex. This approach is risky as it could lead to deadlocks if not used carefully, especially in test scenarios. Consider using a more structured approach such as a Channel or a flow-based solution that doesn't require manual mutex handling.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1)

16-33: Good test coverage for connection status in Compose.

The test properly verifies that the Connection status is correctly observed in a composable context, with appropriate assertions for each state transition. The use of moleculeFlow with RecompositionMode.Immediate ensures immediate updates when testing.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1)

16-33: Good test coverage for room status in Compose.

The test effectively verifies that the Room status changes are correctly observed in a composable context, with appropriate assertions for each state transition.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (3)

17-20: Good use of ExperimentalChatApi annotation

The class is properly annotated with @OptIn(ExperimentalChatApi::class) to acknowledge the experimental API usage.


21-34: Test logic looks good

The test correctly verifies that occupancy events are emitted and processed appropriately in the composable state. Good use of moleculeFlow and the testing pattern looks solid.


36-50: Good test for cancellation behavior

This test effectively verifies that the composable handles the cancellation of initial occupancy retrieval when events arrive faster. The pause/resume mechanism provides good control over the test flow.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (2)

15-19: Good use of ExperimentalChatApi annotation

The class is properly annotated with @OptIn(ExperimentalChatApi::class) to acknowledge the experimental API usage.


20-32: Thorough testing of typing updates

The test effectively verifies that typing events are correctly propagated to the composable state. The assertions properly check both the initial empty state and subsequent updates.

@sacOO7
Copy link
Contributor

sacOO7 commented Mar 7, 2025

I am going through the related DR and related implementation to understand context properly. It will take bit of time, but once we agree on this, we don't need to worry about future flow related issues.

*/
@ExperimentalChatApi
@Composable
public fun Messages.asComposable(): PaginatedMessages {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like this component doesn't handle discontinuity in messages. Is this something we can easily add?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added discontinuity support


coroutineScope.launch {
loading = true
val receivedPaginatedResult = subscription.getPreviousMessages()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Users should probably be able to specify how far back they want to go with a getPreviousMessages call

Copy link
Collaborator Author

@ttypic ttypic Mar 10, 2025

Choose a reason for hiding this comment

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

It's connected to the scroll state, it will get previous message if mobile user scrolling, sdk users can specify page size

*/
@ExperimentalChatApi
@Composable
public fun RoomReactions.asComposable(): Map<String, ReactionCounter> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps on this one, we should keep it to just ephemeral events for now - we've got a whole feature in the works for counted reactions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it makes sense, removed this extension for now

Added `kotlinx.coroutines.flow.Flow` support for all callback-based subscription
@ttypic ttypic force-pushed the CHA-801/flow-and-compose branch from f8dce90 to 1ce91a0 Compare March 8, 2025 00:13
@github-actions github-actions bot temporarily deployed to staging/pull/116/dokka March 8, 2025 00:14 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (1)

1-157: 💡 Verification agent

🧩 Analysis chain

Missing test for unsubscription behavior.

According to the AI summary, there should be a test for the asFlow() function that verifies automatic unsubscription, but this test is missing from the implementation.


🏁 Script executed:

#!/bin/bash
# Search for any implementation of asFlow() function or any files mentioning unsubscription testing
echo "Searching for asFlow() implementation..."
rg -t kotlin "fun .*asFlow\(\)" --glob "!**/build/**"

echo "Searching for any unsubscription tests..."
rg -t kotlin "unsubscribe.*test|test.*unsubscribe" --glob "!**/build/**"

Length of output: 1664


Action Required: Add Automatic Unsubscription Test for asFlow()

The current test in chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt does not verify that the asFlow() functionality automatically unsubscribes after collection completes. Notably, similar unsubscription tests exist in the chat-android module (e.g., in chat-android/src/test/java/com/ably/chat/PresenceTest.kt), which can serve as a reference.

  • Add a test case to verify that when using the asFlow() function (or its equivalent in the compose extension), the flow properly unsubscribes once it's done.
  • Refer to existing tests in the chat-android module (like the test named asFlow() should automatically unsubscribe then it's done) to guide the implementation.
🧹 Nitpick comments (20)
chat-android/src/main/java/com/ably/chat/Discontinuities.kt (1)

46-48: Fix KDoc documentation inconsistency.

The KDoc mentions returning [ConnectionStatusChange] events as a [Flow], but the actual method returns Flow<ErrorInfo?>. This appears to be a copy-paste error from another similar extension function.

-/**
- * @return [ConnectionStatusChange] events as a [Flow]
- */
+/**
+ * @return discontinuity events as a [Flow] of [ErrorInfo]
+ */
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (2)

84-89: Consider adding mutex protection for currentOccupancyEvent updates

The emit method updates currentOccupancyEvent without mutex protection, which could potentially lead to race conditions in a concurrent environment. Consider wrapping this operation with mutex.withLock for consistency with the rest of the implementation.

-fun emit(event: OccupancyEvent) {
-    currentOccupancyEvent = event
-    listeners.forEach {
-        it.onEvent(event)
-    }
-}
+suspend fun emit(event: OccupancyEvent) {
+    mutex.withLock {
+        currentOccupancyEvent = event
+    }
+    listeners.forEach {
+        it.onEvent(event)
+    }
+}

62-62: Add documentation to the EmittingOccupancy class

Consider adding KDoc comments to explain the purpose and usage of this test helper class and its methods to improve maintainability and readability.

+/**
+ * A test implementation of the Occupancy interface that allows controlling
+ * occupancy events emission and timing for testing purposes.
+ */
class EmittingOccupancy(val mock: Occupancy) : Occupancy by mock {
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (2)

14-16: Enhance documentation for this experimental API

The current documentation is minimal. Since this is marked with @ExperimentalChatApi, more comprehensive documentation would be beneficial to explain:

  • The purpose of this composable function
  • Examples of how it should be used
  • Potential lifecycle considerations

22-24: Consider adding error handling for the flow collection

The current implementation doesn't handle potential errors in the flow collection. If statusAsFlow() encounters an error, it might crash the application.

LaunchedEffect(this) {
-    statusAsFlow().collect { status = it.current }
+    statusAsFlow().catch { e ->
+        // Log error or handle appropriately
+    }.collect { status = it.current }
}
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (2)

36-37: Simplify EmittingConnection creation

The factory function for EmittingConnection creates a mock that isn't actually used in the implementation.

-fun EmittingConnection() = EmittingConnection(mockk())
+fun EmittingConnection() = EmittingConnection()

Then update the class definition:

-class EmittingConnection(mock: Connection) : Connection by mock {
+class EmittingConnection : Connection {

16-19: Consider adding more comprehensive tests

While the current test verifies basic functionality, consider adding tests for:

  1. Recomposition behavior
  2. Rapid status changes
  3. Error scenarios
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (4)

17-43: Consider enhancing error handling and displaying loading states.

The collectAsPresenceMembers() function swallows exceptions with runCatching but doesn't provide any error state for the UI to react to. Users of this function have no way to know if an error occurred or if the list is empty because there are genuinely no members present.

Consider enhancing the function to return a data class that includes loading and error states:

data class PresenceMembersState(
    val members: List<PresenceMember> = emptyList(),
    val isLoading: Boolean = false,
    val error: Throwable? = null
)

@ExperimentalChatApi
@Composable
public fun Room.collectAsPresenceMembers(): PresenceMembersState {
    var state by remember(this) { mutableStateOf(PresenceMembersState(isLoading = true)) }
    val roomStatus = collectAsStatus()

    LaunchedEffect(this, roomStatus) {
        if (roomStatus != Attached) {
            state = PresenceMembersState(isLoading = roomStatus.isTransitioning())
            return@LaunchedEffect
        }

        val initialPresenceGet = launch {
            state = state.copy(isLoading = true)
            runCatching {
                presence.get()
            }.fold(
                onSuccess = { members -> state = PresenceMembersState(members = members) },
                onFailure = { error -> state = PresenceMembersState(error = error) }
            )
        }
        
        presence.asFlow().collect {
            if (initialPresenceGet.isActive) initialPresenceGet.cancelAndJoin()
            state = state.copy(isLoading = true)
            runCatching {
                presence.get()
            }.fold(
                onSuccess = { members -> state = PresenceMembersState(members = members) },
                onFailure = { error -> state = PresenceMembersState(error = error) }
            )
        }
    }

    return state
}

34-39: Consider debouncing or throttling presence updates.

Each presence event triggers a potentially expensive presence.get() call that fetches the entire member list. In busy rooms with many presence events, this could lead to performance issues.

Consider adding debouncing to reduce the frequency of calls:

import kotlinx.coroutines.flow.debounce

// Inside the LaunchedEffect block
presence.asFlow()
    .debounce(300) // Debounce for 300ms
    .collect {
        if (initialPresenceGet.isActive) initialPresenceGet.cancelAndJoin()
        runCatching {
            presenceMembers = presence.get()
        }
    }

22-24: Consider caching presence member results by room ID.

The current implementation uses remember(this) which recreates state when the Room reference changes, but Room instances might be recreated for the same underlying room.

Consider using a more stable key like the room ID:

var presenceMembers by remember(id) { mutableStateOf(emptyList<PresenceMember>()) }

1-16: Function documentation could be more descriptive.

The current documentation just states the return value but doesn't explain when it will be updated or any conditions/limitations.

Consider enhancing the KDoc to be more informative:

/**
 * Collects and provides the current list of presence members in this room as a Compose state.
 * 
 * This function:
 * - Returns an empty list until the room is attached
 * - Updates the list whenever presence changes are detected
 * - Requires the room to be in Attached state to fetch members
 * 
 * @return Currently present members in the room as a Compose-aware state
 */
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (2)

14-16: Enhance documentation to be more descriptive.

The current documentation is minimal and doesn't provide enough context about what the function does and how it should be used. Consider expanding it to explain that this function collects room status updates and recomposes when changes occur.

 /**
- * @return room status
+ * Collects the current status of the Room as a state that will be updated whenever 
+ * the status changes, triggering recomposition.
+ * 
+ * @return The current room status that will be updated when changes occur
+ * @see RoomStatus
  */

19-27: Consider adding error handling to the flow collection.

The current implementation doesn't handle potential errors that might be emitted from statusAsFlow(). Adding error handling would make the component more robust.

    LaunchedEffect(this) {
-        statusAsFlow().collect { status = it.current }
+        statusAsFlow().collect { 
+            try {
+                status = it.current
+            } catch (e: Exception) {
+                // Consider logging the error or providing fallback behavior
+                // Log.e("Room", "Error collecting room status", e)
+            }
+        }
    }
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (3)

74-81: Rename scrollIsAtTheTop variable for clarity

The variable name scrollIsAtTheTop is misleading since it's checking if the user has scrolled far enough from the beginning of the list (not at the top). Consider renaming it to something like shouldLoadMoreBasedOnScroll or reachedScrollThreshold to better reflect its purpose.

- val scrollIsAtTheTop = listState.firstVisibleItemIndex > loaded.size - scrollThreshold
+ val reachedScrollThreshold = listState.firstVisibleItemIndex > loaded.size - scrollThreshold
- val shouldGetPreviousMessages = scrollIsAtTheTop && hasMoreItems
+ val shouldGetPreviousMessages = reachedScrollThreshold && hasMoreItems

67-72: Add visual indicator for message discontinuity

While the code correctly handles clearing messages when discontinuity occurs, there's no indication to the user that a break in the message history has happened. Consider adding a state property to indicate discontinuity that UI consumers can use to display a visual separator or notification.

public interface PagingMessagesState {
    public val loaded: SnapshotStateList<Message>
    public val listState: LazyListState
    public val loading: Boolean
    public val hasMore: Boolean
    public val error: ErrorInfo?
+   public val hasDiscontinuity: Boolean
    public suspend fun refresh()
}

private data class DefaultPagingMessagesState(
    override val loaded: SnapshotStateList<Message>,
    override val listState: LazyListState,
    override val loading: Boolean,
    override val hasMore: Boolean,
    override val error: ErrorInfo?,
+   override val hasDiscontinuity: Boolean,
    private val refreshLambda: suspend () -> Unit,
) : PagingMessagesState {
    override suspend fun refresh() = refreshLambda()
}

37-112: Consider breaking down the long composable function

The function is currently suppressed with @Suppress("LongMethod"). Consider extracting some of the functionality into separate helper functions to improve readability and maintainability, such as:

  • Message subscription logic
  • Pagination logic
  • Error handling logic

This would make the main function more concise and easier to understand.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (4)

23-29: Good test setup but missing documentation.

The test setup is properly structured with appropriate mocking of the Room object and presence configuration. The use of @OptIn(ExperimentalChatApi::class) correctly acknowledges that you're working with experimental APIs.

Consider adding a brief class-level KDoc comment explaining the purpose of these tests, especially since they're testing experimental functionality.


132-134: Incomplete mock implementation for presence filtering.

The get() method ignores the clientId and connectionId filter parameters, which means the tests aren't verifying that filtering works correctly when these parameters are used.

override suspend fun get(waitForSync: Boolean, clientId: String?, connectionId: String?): List<PresenceMember> = mutex.withLock {
-    clientIdToPresenceMember.values.toList()
+    clientIdToPresenceMember.values
+        .filter { member -> clientId == null || member.clientId == clientId }
+        // Note: You would need to add connectionId to the PresenceMember class to filter by it
+        .toList()
}

136-142: Potential for test hangs in mutex handling.

The pause() and resume() methods don't include error handling or timeouts, which could potentially cause tests to hang if there's an issue.

suspend fun pause() {
-    mutex.lock()
+    // Add timeout to prevent test hangs
+    val acquired = mutex.tryLock(timeoutMillis = 5000)
+    if (!acquired) {
+        throw IllegalStateException("Failed to acquire mutex lock within timeout")
+    }
}

fun resume() {
+    // Only unlock if currently locked to prevent IllegalStateException
+    if (mutex.isLocked) {
         mutex.unlock()
+    }
}

3-21: Consider organizing imports.

The imports are grouped logically, but they could be better organized for readability by grouping by package source (standard Kotlin, Ably, testing libraries, etc.).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8dce90 and 1ce91a0.

📒 Files selected for processing (37)
  • chat-android/src/main/java/com/ably/chat/Connection.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/Discontinuities.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Messages.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Occupancy.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Presence.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Room.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/RoomReactions.kt (2 hunks)
  • chat-android/src/main/java/com/ably/chat/Typing.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/ConnectionTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/MessagesTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/OccupancyTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/PresenceTest.kt (3 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt (2 hunks)
  • chat-android/src/test/java/com/ably/chat/RoomTest.kt (1 hunks)
  • chat-android/src/test/java/com/ably/chat/TypingTest.kt (3 hunks)
  • chat-extensions-compose/build.gradle.kts (1 hunks)
  • chat-extensions-compose/gradle.properties (1 hunks)
  • chat-extensions-compose/proguard-rules.pro (1 hunks)
  • chat-extensions-compose/src/main/AndroidManifest.xml (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/src/main/java/com/ably/chat/example/MainActivity.kt (7 hunks)
  • example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
  • settings.gradle.kts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (24)
  • chat-extensions-compose/src/main/AndroidManifest.xml
  • example/build.gradle.kts
  • settings.gradle.kts
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt
  • chat-android/src/main/java/com/ably/chat/RoomReactions.kt
  • chat-extensions-compose/gradle.properties
  • chat-android/src/main/java/com/ably/chat/Occupancy.kt
  • chat-android/src/main/java/com/ably/chat/Presence.kt
  • chat-android/src/main/java/com/ably/chat/Messages.kt
  • chat-android/src/main/java/com/ably/chat/Room.kt
  • chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt
  • chat-extensions-compose/build.gradle.kts
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt
  • chat-android/src/main/java/com/ably/chat/Typing.kt
  • chat-android/src/test/java/com/ably/chat/TypingTest.kt
  • chat-android/src/main/java/com/ably/chat/Connection.kt
  • chat-android/src/test/java/com/ably/chat/MessagesTest.kt
  • chat-android/src/test/java/com/ably/chat/PresenceTest.kt
  • gradle/libs.versions.toml
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt
  • chat-extensions-compose/proguard-rules.pro
  • example/src/main/java/com/ably/chat/example/MainActivity.kt
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt
🧰 Additional context used
🧠 Learnings (3)
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/main/java/com/ably/chat/Occupancy.kt:145-145
Timestamp: 2024-11-28T11:11:20.423Z
Learning: In `chat-android/src/main/java/com/ably/chat/Occupancy.kt`, within the `DefaultOccupancy` class, when methods use `room.chatApi`, which utilizes the REST API, there's no need to call `room.ensureAttached()` before performing the operation.
chat-android/src/test/java/com/ably/chat/RoomTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2024-11-28T11:08:42.524Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/OccupancyTest.kt:20-31
Timestamp: 2024-11-28T11:12:06.843Z
Learning: In `OccupancyTest.kt`, additional room state initialization using `setState` is unnecessary for testing occupancy functionality, as the specification doesn't require it.
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: check
  • GitHub Check: coverage
🔇 Additional comments (29)
example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (5)

18-20: Clean API migration with appropriate imports.

The imports have been properly updated to support the new Flow-based API, including the experimental API annotation and the compose extension function.


25-25: Take note of experimental API usage.

The @OptIn annotation correctly indicates that this code uses experimental API features that may change in future releases. This is good practice for API evolution awareness.


27-27: Improved function signature with direct Room access.

The signature change from accepting ChatClient to accepting Room directly is a positive change that follows the principle of least privilege - providing only what's necessary for the component to function.


28-28: Simplified state management with Compose extensions.

The refactoring greatly simplifies presence state handling by using the collectAsPresenceMembers() extension function, eliminating manual subscription management and state updates that were previously required. This change improves code readability and reduces potential bugs related to lifecycle management.


30-30: Direct access to presence from Room object.

Accessing presence directly from the room object follows a more intuitive object hierarchy and maintains consistency with the new API design.

chat-android/src/main/java/com/ably/chat/Discontinuities.kt (2)

49-51: Well-implemented Flow extension function.

The implementation effectively converts the callback-based API to a Flow-based approach, which aligns well with modern Kotlin coroutines patterns. This provides a more reactive way to handle discontinuity events.


6-6: Appropriate import for Flow API.

The import for kotlinx.coroutines.flow.Flow is correctly added to support the new flow-based functionality.

chat-android/src/test/java/com/ably/chat/OccupancyTest.kt (1)

156-177: Good test pattern for verifying Flow unsubscription.

The test correctly verifies that the asFlow() method properly unsubscribes when the flow is canceled. This is important for preventing memory leaks by ensuring resources are cleaned up properly.

chat-android/src/test/java/com/ably/chat/RoomTest.kt (1)

1-36: LGTM - Clean test implementation for Flow unsubscription.

This test follows good practices for verifying that resources are properly cleaned up when the Flow is canceled. The mocking setup is concise and the verification is clear.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (2)

16-19: LGTM - Good data model with default values.

The CurrentOccupancy data class is well-designed with appropriate default values, making it safe to use in UI components even before data is loaded.


26-53: Well-structured Composable with proper cleanup handling.

The implementation follows Compose best practices:

  • Uses remember keyed on the room instance
  • Uses LaunchedEffect for side effects
  • Properly handles the room's attachment state
  • Cancels the initial request if a streaming update arrives first

Regarding the previous comment about "Should we be doing the initial collect if we're not attached?", this is now correctly addressed with the early return on line 31.

chat-android/src/test/java/com/ably/chat/ConnectionTest.kt (1)

151-172: LGTM - Consistent test pattern for Flow unsubscription.

This test follows the same good pattern used in other Flow tests, verifying that resources are properly cleaned up when the Flow is canceled.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (3)

28-41: LGTM! Well-structured test for occupancy collection

The test appropriately validates that occupancy events are correctly emitted and collected through the composable function. The use of moleculeFlow with RecompositionMode.Immediate is a good approach for testing composable functions.


43-57: LGTM! Good edge case test

This test effectively verifies the cancellation behavior when an occupancy event arrives during initial occupancy retrieval. The use of pause() and resume() to control the timing is a clean approach.


62-90: Add an explicit test for auto-unsubscription

The EmittingOccupancy implementation is well-structured with proper thread safety using Mutex. However, there's no explicit test for verifying that the flow automatically unsubscribes when completed.

According to the AI summary, there should be a test method named "asFlow() should automatically unsubscribe then it's done", but it's not present in this file. Similar tests are already implemented in the chat-android module.

#!/bin/bash
# Check if there's a test for auto-unsubscription in other files
rg -A 3 "asFlow\(\) should automatically unsubscribe" --type kotlin
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (1)

17-27: Good implementation of the Compose composable function

The collectAsStatus() composable function is well-implemented, using appropriate Compose patterns:

  • Properly uses remember with the connection instance as a key
  • Correctly implements LaunchedEffect for side-effect handling
  • Cleanly transforms the Flow-based API into a Compose state
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (2)

21-33: Well-structured test using Molecule for composable testing

The test effectively verifies the composable function behavior:

  • Uses Molecule to test the composable outside of a UI environment
  • Properly asserts state changes using Turbine
  • Tests multiple status transitions
  • Ensures proper cleanup with cancel()

38-53: The mock implementation is clean and effective

The EmittingConnection class is well-implemented:

  • Properly tracks and notifies listeners
  • Handles subscription cleanup correctly
  • Provides a simple API for emitting events in tests
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (1)

30-32: Handle errors when getting presence members.

The runCatching block swallows any exceptions without logging or providing error feedback to the caller. This matches the concern raised in a previous review about get() failing if not attached.

While you do check for Attached status in line 27, there could still be race conditions or other failure reasons. Consider how to communicate these errors to consumers of this API.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (4)

17-18: LGTM! Good use of experimental API annotation.

Correctly marking this API as experimental with @ExperimentalChatApi allows for future modifications while warning users about potential changes.


20-20: Good use of the remember key parameter.

Using remember(this) ensures the state is correctly reset if the Room instance changes, which is important for proper lifecycle management.


22-24: Verify flow resource cleanup.

The LaunchedEffect will handle cancellation of the flow collection when the composition leaves scope, which is the correct approach for resource management in Compose.


1-27:

✅ Verification successful

Verify integration with statusAsFlow extension.

Ensure that the statusAsFlow() extension function (imported from line 12) is properly implemented and tested. The success of this composable depends on it working correctly.


🏁 Script executed:

#!/bin/bash
# Search for the implementation of statusAsFlow to verify it exists and works as expected
echo "Searching for statusAsFlow implementation..."
rg -p "fun.*statusAsFlow" --type kotlin

# Also check for any tests for this functionality
echo "Checking for tests of the statusAsFlow functionality..."
rg -p "test.*statusAsFlow|statusAsFlow.*test" --type kotlin

Length of output: 1105


StatusAsFlow Integration Verified
The integration with the statusAsFlow() extension has been validated. The implementation is present in the main code (e.g., in chat-android/src/main/java/com/ably/chat/Room.kt), and the tests confirming its behavior are available (notably in chat-android/src/test/java/com/ably/chat/RoomTest.kt and the corresponding connection tests). No changes are required here.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (4)

34-37: Good use of experimental API annotation

The @ExperimentalChatApi annotation clearly indicates to users that this API might change in the future. Consider adding documentation that explains what aspects of the API might change and how users can prepare for potential breaking changes.


39-44: Consider implementing message count limiting mechanism

The current implementation stores all loaded messages in memory without any upper bound, which could potentially lead to memory issues with very large chat histories. Consider implementing a mechanism to limit the total number of messages kept in memory, such as removing older messages when a certain threshold is reached.


92-98: Enhance error handling with retry mechanism

While the code captures errors, the user must manually call refresh() to retry after an error. Consider enhancing this with automatic retry logic with exponential backoff or providing more context in the error state to help users understand and recover from failures.


36-43: Add support for custom limit in getPreviousMessages

While the function accepts a limit parameter, it doesn't allow consumers to change this value dynamically after initial composition. Consider adding a way for users to control how many messages to fetch when loading more, perhaps through a method in the PagingMessagesState interface.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (2)

31-78: Well-structured test for presence state collection.

This test effectively validates that presence events are properly collected into state using Molecule's composable testing utilities. The assertions correctly verify both the initial empty state and subsequent updates.


80-117: Good edge case testing for concurrent event handling.

This test properly verifies an important edge case where presence events arrive while the initial presence set is being loaded. The use of mutex pause/resume is an effective technique for controlling the test flow.

*/
@ExperimentalChatApi
@Composable
public fun Room.collectAsPresenceMembers(): List<PresenceMember> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I

Suggested change
public fun Room.collectAsPresenceMembers(): List<PresenceMember> {
public fun Room.collectAsPresenceMembers(): List<PresenceMember> {

the method returns current presence members, so is it possible to rename it that way.
Not sure if there's specific method syntax for @ Composable methods

Copy link
Contributor

Choose a reason for hiding this comment

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

like Room.getCurrentPresenceMembers()
Same goes for other methods

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

get is a very generic name, and it can also conflict with property getters and setters. Extension functions that transform an observable state into composable data usually include this information in their name. See https://developer.android.com/develop/ui/compose/state#use-other-types-of-state-in-jetpack-compose.

All Android extensions follow one of these naming patterns:
• collectAs
• subscribeAs
• observeAs

I chose collectAs because, in these composable extensions, we are mostly collecting a Flow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Regarding currentPresenceMembers, I get the impression that "presence" already implies currently present members. @AndyTWF wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, "presence" in its own right applies "whoever is currently present".

OOI, do we want to move these properties to the respective classes, e.g. Presence.collectAsMembers, rather than having it at the Room level?

Copy link
Collaborator Author

@ttypic ttypic Mar 10, 2025

Choose a reason for hiding this comment

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

The main reason is that having them at the room level makes it possible to use the room’s status internally. I don’t want to introduce a backlink to the room in the public API of a specific "feature". I also don’t think it looks ugly or misleading.

Additionally, it’s more consistent to have a custom extension that combines data from two different "features" —something like CurrentlyTypingPresenceMember. I’m not sure if it’s useful, but who knows? :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, can we not pass down the RoomStatus object to avoid using the rooms API internally?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can, but I think it will look weird :)

@github-actions github-actions bot temporarily deployed to staging/pull/116/dokka March 10, 2025 11:33 Inactive
@ttypic ttypic requested review from AndyTWF and sacOO7 March 10, 2025 12:01
@ExperimentalChatApi
@Composable
@Suppress("LongMethod")
public fun Room.collectAsPagingMessagesState(scrollThreshold: Int = 10, limit: Int = 100): PagingMessagesState {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe call this fetchSize or something like that? limit has implications that may not be clear I think

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree


LaunchedEffect(this) {
messages.discontinuityAsFlow().collect {
loaded.clear()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it worth doing another fetch here to try and repopulate ourself?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It should be done automatically, we reset state here, and if we are good (room is attached, we instantly reinitialize messages)

*/
@ExperimentalChatApi
@Composable
public fun Room.collectAsPresenceMembers(): List<PresenceMember> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, can we not pass down the RoomStatus object to avoid using the rooms API internally?

/**
* @return [ConnectionStatusChange] events as a [Flow]
*/
public fun EmitsDiscontinuities.discontinuityAsFlow(): Flow<ErrorInfo?> = transformCallbackAsFlow {
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't work on flows yet.
So, had a question if there are libraries returning cold flows.
I am thinking, most kotlin SDKs return shared flows, so instance can be shared by multiple subscribers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I asked perplexity, and it seems most kotlin public APIs return cold flows by default : )
So, we are okay there, but you can double check 👍

Copy link
Contributor

@sacOO7 sacOO7 Mar 11, 2025

Choose a reason for hiding this comment

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

Btw, feature level discontinuity will be move to room level. So, we will not need this
https://ably-real-time.slack.com/archives/C02NY1VT3LY/p1741626477605909

Copy link
Contributor

@sacOO7 sacOO7 Mar 11, 2025

Choose a reason for hiding this comment

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

Maybe you like to remove discontinuityAsFlow to avoid introducing breaking public API changes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We are not going to do next release before single channel is landed, so there'll be no breaking changes for users. I think we even should preserve interface, it's generic enough, maybe some other API will have discontinuities as well

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, then we will update once single channel changes are introduced

@sacOO7
Copy link
Contributor

sacOO7 commented Mar 11, 2025

Overall looks okay to me, apart from few posted comments ( you can take a look 👍 )
I will approve once @AndyTWF comments are resolved.

ttypic added 2 commits March 12, 2025 14:01
Added `com.ably.chat:chat-extensions-compose` package with extension functions for better integration with Jetpack Compose.
@ttypic ttypic force-pushed the CHA-801/flow-and-compose branch from 828e13e to d710621 Compare March 12, 2025 14:02
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (10)
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (3)

19-19: Consider adding a descriptive comment explaining the test setup.

A brief comment explaining that EmittingConnection is a test double that simulates connection status changes would improve readability and maintainability of the test.


36-36: Consider making this a private function.

This factory function is only used internally within this test class. Consider making it private to restrict its scope.

-fun EmittingConnection() = EmittingConnection(mockk())
+private fun EmittingConnection() = EmittingConnection(mockk())

48-52: Consider adding a test for edge case: empty listeners list.

The emit method works correctly when there are listeners, but there's no test verifying behavior when the listeners list is empty. While this isn't an issue functionally (it will simply do nothing), adding an assertion might make the test more comprehensive.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (2)

36-36: Rename factory function to avoid overshadowing class name.

Currently, the fun EmittingRoom() = EmittingRoom(mockk()) factory function has the same name as the class, which could lead to confusion. Consider renaming it (e.g., createEmittingRoom()) for greater clarity.

-fun EmittingRoom() = EmittingRoom(mockk())
+fun createEmittingRoom() = EmittingRoom(mockk())

41-52: Ensure status tracks the latest status change.

The status property is never updated after initialization, so reading room.status directly can be misleading if events have changed its state. Consider synchronizing the property with the last reported current status in the emit method.

 class EmittingRoom(mock: Room) : Room by mock {
     private val listeners = mutableListOf<Room.Listener>()

-    override val status: RoomStatus = RoomStatus.Initialized
+    private var currentStatus: RoomStatus = RoomStatus.Initialized
+    override val status: RoomStatus get() = currentStatus

     override fun onStatusChange(listener: Room.Listener): Subscription {
         listeners.add(listener)
         return Subscription { listeners.remove(listener) }
     }

     fun emit(event: RoomStatusChange) {
+        currentStatus = event.current
         listeners.forEach {
             it.roomStatusChanged(event)
         }
     }
 }
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (2)

28-41: Tests look good, but tests for edge cases would be beneficial.

The test effectively validates the basic functionality of collectAsOccupancy() by verifying that it correctly processes and emits occupancy events. The use of moleculeFlow to test Compose integration is appropriate.

Consider adding tests for edge cases like:

  • Multiple concurrent emissions
  • Error handling scenarios
  • Cases where Room.status is not Attached

60-60: Consider adding documentation to the factory function.

The factory function is helpful but lacks documentation explaining its purpose.

-fun EmittingOccupancy() = EmittingOccupancy(mockk())
+/**
+ * Factory function that creates an EmittingOccupancy instance with a mocked underlying Occupancy.
+ * Useful for testing scenarios that require controlling occupancy events.
+ */
+fun EmittingOccupancy() = EmittingOccupancy(mockk())
chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (3)

26-33: Revise the KDoc to accurately reflect the state return type.

The comment states that this function returns a list of messages, but in reality, it returns a PagingMessagesState, which provides additional loading and error info. Consider revising the KDoc so that it explicitly describes returning a state object rather than just a list.


74-82: Rename scrollIsAtTheTop to match its logic.

scrollIsAtTheTop is calculated using listState.firstVisibleItemIndex > loaded.size - scrollThreshold, which actually checks if the user is near the end of the list. Inverting the name or logic would reduce confusion; for example, consider scrollIsAtTheBottom or scrollIsPastThreshold.


84-102: Clarify the null-safety expectations for subscription.

Relying on subscription!! can risk a NullPointerException if disposal or re-composition logic sets it to null unexpectedly. Even though your conditions presumably guard against that, consider explicitly checking for null or using an assertion to make your assumptions clearer.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 828e13e and d710621.

📒 Files selected for processing (24)
  • chat-android/src/main/java/com/ably/chat/Connection.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt (1 hunks)
  • chat-android/src/main/java/com/ably/chat/Typing.kt (3 hunks)
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt (1 hunks)
  • chat-extensions-compose/build.gradle.kts (1 hunks)
  • chat-extensions-compose/gradle.properties (1 hunks)
  • chat-extensions-compose/proguard-rules.pro (1 hunks)
  • chat-extensions-compose/src/main/AndroidManifest.xml (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt (1 hunks)
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1 hunks)
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/src/main/java/com/ably/chat/example/MainActivity.kt (7 hunks)
  • example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
  • settings.gradle.kts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (19)
  • chat-extensions-compose/src/main/AndroidManifest.xml
  • settings.gradle.kts
  • example/build.gradle.kts
  • chat-android/src/main/java/com/ably/chat/Typing.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Room.kt
  • chat-android/src/main/java/com/ably/chat/annotations/Annotations.kt
  • chat-extensions-compose/proguard-rules.pro
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/PresenceTest.kt
  • chat-android/src/main/java/com/ably/chat/FlowUtils.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Typing.kt
  • chat-android/src/main/java/com/ably/chat/Connection.kt
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Occupancy.kt
  • chat-extensions-compose/build.gradle.kts
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Connection.kt
  • chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/TypingTest.kt
  • gradle/libs.versions.toml
  • example/src/main/java/com/ably/chat/example/MainActivity.kt
  • chat-extensions-compose/gradle.properties
  • chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Presence.kt
🧰 Additional context used
🧠 Learnings (2)
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/OccupancyTest.kt:20-31
Timestamp: 2025-03-12T13:33:27.491Z
Learning: In `OccupancyTest.kt`, additional room state initialization using `setState` is unnecessary for testing occupancy functionality, as the specification doesn't require it.
chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/RoomTest.kt (1)
Learnt from: sacOO7
PR: ably/ably-chat-kotlin#66
File: chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt:44-50
Timestamp: 2025-03-12T13:33:27.491Z
Learning: The test cases for verifying behavior when the room is not in the ATTACHED state are covered in `chat-android/src/test/java/com/ably/chat/room/RoomEnsureAttachedTest.kt`.
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: coverage
  • GitHub Check: check
  • GitHub Check: build
🔇 Additional comments (12)
example/src/main/java/com/ably/chat/example/ui/PresencePopup.kt (5)

18-20: Appropriate imports for new Flow-based approach

The imports align with the PR objectives to add Flow and Jetpack Compose support to the Ably Chat library. The inclusion of the collectAsPresenceMembers extension function is particularly relevant for simplifying presence management.


25-25: Proper usage of OptIn annotation for experimental API

Good practice to explicitly opt-in to experimental APIs. This makes it clear to other developers that this functionality might change in future releases.


27-27: Improved function signature with Room parameter

Changing the parameter from ChatClient to Room is a good design decision as it:

  1. Better follows the single responsibility principle
  2. Makes the code more direct (presence is a property of a room)
  3. Aligns with the Flow-based architecture being implemented

This change improves the API's usability and makes the code more maintainable.


28-28: Simplified presence handling with Flow

Excellent simplification! The new implementation using collectAsPresenceMembers() eliminates the need for manual subscription management and state updates. This makes the code:

  1. More concise
  2. Less error-prone
  3. More idiomatic Kotlin

The previous implementation required complex DisposableEffect and subscription management code that has been completely removed.


30-30: Direct access to room.presence

Accessing presence directly from the room object simplifies the code and follows a more intuitive object hierarchy, improving readability and maintainability.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/ConnectionTest.kt (3)

22-33: Well-structured test using modern testing libraries.

This test effectively validates that the connection status changes are properly propagated through the Flow and collected in a composable context. The use of Molecule and Turbine for testing flows in a composable environment is a good practice.


38-53: Add test for initial status notification.

The EmittingConnection class effectively simulates a connection with status change capability, but doesn't test whether the initial status is notified to a listener when it's first added. Consider adding a test that verifies a listener receives the initial status when registered.

Does the actual Connection implementation notify listeners of the current status immediately upon registration? If so, this test double should mimic that behavior.


43-46: Good implementation of subscription management.

The subscription pattern is well implemented, ensuring proper cleanup when the subscription is disposed, preventing memory leaks.

chat-extensions-compose/src/test/kotlin/com/ably/chat/extensions/compose/OccupancyTest.kt (3)

43-57: Well-structured test for race condition handling.

This test elegantly verifies that ongoing occupancy retrieval is correctly canceled when new events arrive, which is an important edge case for reactive implementations. Using the mutex pause/resume mechanism provides good control over the test flow.


62-90: Well-implemented test utility with proper thread safety.

The EmittingOccupancy class is well-designed for testing purposes with:

  • Proper thread safety using Mutex
  • Clear subscription management
  • Simple event emission mechanism

20-90: Add explicit test for auto-unsubscription when flow completes.

While the existing tests validate the normal operation flow, there's no explicit test verifying that the flow automatically unsubscribes when completed.

Consider adding an equivalent auto-unsubscription test to this file to ensure consistent behavior across the codebase. This would follow the pattern seen in other test files like chat-android/src/test/java/com/ably/chat/OccupancyTest.kt.

chat-extensions-compose/src/main/kotlin/com/ably/chat/extensions/compose/Messages.kt (1)

49-58: Reevaluate handling of MessageEventType.Deleted.

Deleted messages are currently replaced with a new “deleted” message object instead of being removed from the list, which some UIs might find unintuitive. Consider whether you actually intend to remove the entry from the UI entirely or mark it in a different way.

Copy link
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

lgtm

@ttypic ttypic merged commit b7814eb into main Mar 13, 2025
4 checks passed
@ttypic ttypic deleted the CHA-801/flow-and-compose branch March 13, 2025 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants