Skip to content

Add GroupedQueryChannels and grouped unread counts#6516

Open
VelikovPetar wants to merge 8 commits into
developfrom
feature/grouped_query_channels
Open

Add GroupedQueryChannels and grouped unread counts#6516
VelikovPetar wants to merge 8 commits into
developfrom
feature/grouped_query_channels

Conversation

@VelikovPetar

@VelikovPetar VelikovPetar commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Goal

Resolves: https://linear.app/stream/issue/AND-1260/port-groupedquerychannels-to-v7

Port PR #6437 (V6 squash commit bfdc97edcf0754c4ba68bd638ba5a880841cfa04) from v6 to develop.

Adds support for the server-driven grouped-channels API (POST /channels/grouped), where the backend partitions the channel list into named groups (e.g. direct, support) and returns per-group channels, pagination cursors, and unread counts. Surfaces those grouped unread counts on relevant chat events, and provides a Compose ChannelListViewModel path that drives a UI off a group key without the consumer needing to know about filter/sort.

This was not a clean cherry-pick — develop has the stream-chat-android-state and stream-chat-android-offline modules merged into stream-chat-android-client (PR #6069), so every file from those V6 modules required path + package rewriting before applying. Path mapping:

  • stream-chat-android-state/src/.../state/Xstream-chat-android-client/src/.../internal/state/X (with client.internal.state.* package)
  • stream-chat-android-offline/src/.../offline/Xstream-chat-android-client/src/.../internal/offline/X (with client.internal.offline.* package)
  • Public state interfaces (StateRegistry, GlobalState, QueryChannelsState, ChatClientStateExtensions) live under client/api/state/ on develop, not client/internal/state/plugin/.
  • Public chat event handler types (ChatEventHandler, ChatEventHandlerFactory, DefaultChatEventHandler, EventHandlingResult) live under client/api/event/ on develop.

Implementation

  • Endpoint: new ChatClient.queryGroupedChannels(limit, groups, watch, presence) returning GroupedChannels (per-group channels + unreadChannels + next/prev cursors). Per-group request options via GroupedChannelsGroupQuery. Backed by POST /channels/grouped (ChannelApi).
  • Plugin contract: new QueryGroupedChannelsListener; the StatePlugin implementation merges returned per-group unread counts into GlobalState.groupedUnreadChannels and routes each returned group into a state keyed by a new sealed QueryChannelsIdentifier.Grouped(groupKey) variant (alongside Standard and Predefined).
  • Logic: QueryChannelsLogic branches on identifier; applyGroupedResult replaces channels on the first page (resetting channelsOffset defensively) and appends on subsequent pages (driven off the request's next cursor); persists per-group state under a groupKey-derived DB key.
  • Events: new HasGroupedUnreadChannels marker on NewMessageEvent, NotificationMessageNewEvent, NotificationMarkReadEvent, NotificationMarkUnreadEvent, NotificationChannelDeletedEvent, NotificationChannelTruncatedEvent, MarkAllReadEvent. EventHandlerSequential updates GlobalState.groupedUnreadChannels whenever an inbound event carries the map. GroupedUnreadChannelsUpdater handles delta migration on channel.updated events when a channel changes group.
  • Group-aware event handler: GroupAwareChatEventHandler + GroupAwareChatEventHandlerFactory route channels into the right grouped state based on the channel's group custom field via DefaultChannelGroupResolver. Auto-installed by LogicRegistry for QueryChannelsIdentifier.Grouped queries.
  • Watched channel re-watch: StateRegistry tracks watched-channel state flows via WatchedChannelStateFlow weak references so SyncManager can re-watch them on reconnect without pinning lifecycle.
  • Sync: SyncManager re-issues queryGroupedChannels for active grouped logics on reconnect, reusing each group's captured GroupedQueryConfig (limit, pageSize, watch, presence). updateActiveChannels excludes cids already covered by grouped/standard queries and watched flows.
  • Compose VM: new ChannelListViewModel(chatClient, groupKey, ...) constructor drives the UI off a group key. Init via chatClient.initGroupedQueryChannelsAsState(identifier) (no remote call). Load-more uses cursor pagination via queryGroupedChannelsInternal.
  • QueryChannelsSpec: added groupKey: String? = null as a 7th primary-ctor field with default. Pre-existing 2-arg and 6-arg constructors + copy() overloads preserved as manual secondary constructors / copy overloads to maintain binary compatibility with develop's previous published API.
  • DB: ChatDatabase version bumped 201 → 202 to reflect the new groupKey column on QueryChannelsEntity.

UI Changes

No UI changes. New ChannelListViewModel(chatClient, groupKey, ...) constructor is the new public surface for consumers wanting grouped channel lists.

Testing

  • ./gradlew :stream-chat-android-client:compileDebugKotlin :stream-chat-android-compose:compileDebugKotlin — clean
  • ./gradlew :stream-chat-android-client:detekt :stream-chat-android-compose:detekt — clean
  • ./gradlew :stream-chat-android-client:spotlessCheck :stream-chat-android-compose:spotlessCheck — clean
  • ./gradlew :stream-chat-android-client:apiDump :stream-chat-android-compose:apiDump :stream-chat-android-core:apiDump — regenerated
  • Unit tests run and passing on the new grouped-channels test suites plus all touched existing suites: QueryChannelsLogicGroupedTest, QueryGroupedChannelsListenerStateTest, GroupAwareChatEventHandlerTest, GroupedUnreadChannelsUpdaterTest, DefaultChannelGroupResolverTest, ChatClientGroupedChannelsApiTests, QueryGroupedChannelsResponseAdapterTest, QueryChannelsLogicTest, QueryChannelsStateLogicTest, QueryChannelsMutableStateTest, StateRegistryTest, LogicRegistryTest, ChatClientStateCallsTest, SyncManagerTest, EventHandlerSequentialTest, QueryChannelsImplRepositoryTest, ChannelListViewModelTest, ChannelViewModelFactoryTest.

Known follow-ups

A small number of net-new V6 tests were not ported in this PR because they depend on V6-only test helpers (notably the parameterized groupedUnreadChannelsArguments and related random-event helpers in EventHandlerSequentialTest, plus the randomNotificationRemovedFromChannelEvent helper). The underlying production logic IS ported; only those specific test additions are deferred. Worth a follow-up PR to add them back.

Summary by CodeRabbit

  • New Features

    • Added grouped channel querying, including grouped channel lists in the app and Compose ViewModel support.
    • Added grouped unread channel counts across channel events and global state.
    • Added support for grouped query pagination and per-group configuration.
  • Bug Fixes

    • Improved channel state updates during reconnects and event processing.
    • Better handling of unread counts when some values are missing.
  • Tests

    • Added and updated coverage for grouped channel queries, event mapping, state recovery, and unread count updates.

Co-Authored-By: Claude <noreply@anthropic.com>
@VelikovPetar VelikovPetar added the pr:new-feature New feature label Jun 24, 2026
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@github-actions

Copy link
Copy Markdown
Contributor

DB Entities have been updated. Do we need to upgrade DB Version?
Modified Entities :

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/queryChannels/internal/QueryChannelsEntity.kt

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.90 MB 5.93 MB 0.03 MB 🟢
stream-chat-android-ui-components 11.15 MB 11.19 MB 0.04 MB 🟢
stream-chat-android-compose 12.63 MB 12.65 MB 0.03 MB 🟢

@VelikovPetar VelikovPetar marked this pull request as ready for review June 24, 2026 14:02
@VelikovPetar VelikovPetar requested a review from a team as a code owner June 24, 2026 14:02
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR introduces a grouped channels feature for the Stream Chat Android SDK. It adds GroupedChannels domain models, extends 7 event types with HasGroupedUnreadChannels, adds a POST /channels/grouped network endpoint with full request/response DTOs, introduces QueryChannelsIdentifier.Grouped, and wires group-aware event handling, unread count batching, state management, sync recovery, and a new ChannelListViewModel grouped constructor throughout the stack.

Changes

Grouped Channels Feature

Layer / File(s) Summary
Domain models, event contracts, and query identity
stream-chat-android-core/src/main/java/io/getstream/chat/android/models/GroupedChannels.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/query/QueryChannelsSpec.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/QueryChannelsIdentifier.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/GroupedQueryConfig.kt, stream-chat-android-client/api/stream-chat-android-client.api, stream-chat-android-core/api/stream-chat-android-core.api
Adds GroupedChannels, GroupedChannelsGroup, GroupedChannelsGroupQuery models. Introduces HasGroupedUnreadChannels interface and extends 7 event types to carry the optional groupedUnreadChannels map. Adds groupKey to QueryChannelsSpec, introduces QueryChannelsIdentifier.Grouped, and adds GroupedQueryConfig.
Network API: DTOs, endpoint, and event mapping
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/QueryGroupedChannelsRequest.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryGroupedChannelsResponse.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/...
Adds Moshi-serializable request/response DTOs for POST /channels/grouped. Extends 7 event DTOs with optional grouped_unread_channels. Adds queryGroupedChannels to ChannelApi and implements it in MoshiChatApi (with postpone-on-blank-connectionId). Updates EventMapping to populate groupedUnreadChannels for all affected events.
ChatClient public API, listener, and Plugin wiring
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/listeners/QueryGroupedChannelsListener.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientGroupedChannelsApiTests.kt
Adds ChatApi.queryGroupedChannels. Adds ChatClient.queryGroupedChannels (public) and queryGroupedChannelsInternal (feature entry point) with plugin request/result hooks. Defines QueryGroupedChannelsListener interface. Extends Plugin to implement it with a default no-op.
Group-aware event handler and grouped unread updater
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/..., stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/...
Introduces ChannelGroupResolver fun interface, DefaultChannelGroupResolver (explicit group + "all" sentinel), GroupAwareChatEventHandler (routes Add/Remove/Skip/WatchAndAdd by group membership), GroupAwareChatEventHandlerFactory, and GroupedUnreadChannelsUpdater (batch-scoped dedup, per-cid override tracking, delta computation).
State models: QueryChannelsMutableState, GlobalState, and QueryChannelsStateLogic
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/internal/QueryChannelsMutableState.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/global/internal/MutableGlobalState.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/GlobalState.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryChannelsState.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogic.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/internal/QueryChannelsMutableStateTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogicTest.kt
Refactors QueryChannelsMutableState constructor to derive spec from QueryChannelsIdentifier (Standard/Predefined/Grouped), adds nextCursor/groupedQueryConfig backing flows, and reworks chatEventHandlerFactory lifecycle. Adds MutableGlobalState grouped unread backing flow and setter. Updates GlobalState and QueryChannelsState interfaces. Adds setNextCursor, setGroupedQueryConfig, trackChannel to QueryChannelsStateLogic.
QueryChannelsLogic grouped support and LogicRegistry
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogic.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/internal/LogicRegistry.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/model/querychannels/pagination/internal/Mapper.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryChannelsListenerState.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogicGroupedTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogicTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/internal/LogicRegistryTest.kt
Adds applyGroupedResult (mutex-guarded first-page replace vs append), loadOfflineChannels, loadOfflineGroupedChannels, trackChannel, grouped accessors, and short-circuits for queryFirstPage/fetchPage. LogicRegistry auto-installs GroupAwareChatEventHandlerFactory for grouped identifiers. QueryChannelsListenerState uses toOfflinePaginationRequest.
Plugin wiring, event handling, sync recovery, and DB persistence
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerState.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/internal/StatePlugin.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/factory/StreamStatePluginFactory.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequential.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/sync/internal/SyncManager.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/queryChannels/internal/..., stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/StateRegistry.kt, stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/internal/..., stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/ChatClientStateExtensions.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/...
Adds QueryGroupedChannelsListenerState (config capture on request, grouped unread update on first page, routes groups to applyGroupedResult). Wires GroupedUnreadChannelsUpdater through StreamStatePluginFactoryEventHandlerSequentialStatePlugin. EventHandlerSequential updates grouped unread per batch. SyncManager splits reconnect into grouped vs standard recovery paths. Adds QueryChannelsEntity.groupKey, bumps DB version to 202. Adds WatchedChannelStateFlow, tracked watched channel registry in StateRegistry, and initGroupedQueryChannelsAsState/initGroupedQueryChannelsState.
ChannelListViewModel grouped mode and factory
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt, stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelFactory.kt, stream-chat-android-compose/api/stream-chat-android-compose.api, stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt
Adds QueryMode.Grouped(groupKey) to ChannelListViewModel, a new grouped constructor, observeGroupedChannels initialization path, loadMoreGroupedChannels for cursor pagination, and Predefined/Grouped guards for setFilters/setQuerySort. Adds a ChannelListViewModelFactory grouped constructor.
Test Mother helpers and event JSON fixtures
stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/Mother.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/EventChatJsonProvider.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/Mother.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/QueryChannelsImplRepositoryTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/QueryGroupedChannelsResponseAdapterTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt
Extends test Mother.kt with randomChannelTruncatedEvent and adds groupedUnreadChannels/groupKey to existing helpers. Adds grouped_unread_channels to 7 event JSON fixtures. Updates EventArguments parser fixtures and EventMappingTestArguments to include grouped unread in all affected DTOs/domain events. Adds QueryGroupedChannelsResponseAdapterTest and grouped repository tests.

Sequence Diagram

sequenceDiagram
  rect rgba(100, 150, 200, 0.5)
    Note over ChannelListViewModel: Grouped mode initialization
    ChannelListViewModel->>ChatClient: initGroupedQueryChannelsAsState(Grouped(groupKey))
    ChatClient->>ChatClientStateCalls: initGroupedQueryChannelsState(identifier, factory)
    ChatClientStateCalls->>StateRegistry: queryChannels(Grouped(groupKey))
    StateRegistry-->>ChatClientStateCalls: QueryChannelsMutableState
    ChatClientStateCalls->>QueryChannelsLogic: loadOfflineGroupedChannels()
    QueryChannelsLogic-->>ChannelListViewModel: StateFlow~QueryChannelsState?~ (offline data)
  end
  rect rgba(100, 200, 150, 0.5)
    Note over ChannelListViewModel: Remote fetch
    ChannelListViewModel->>ChatClient: queryGroupedChannelsInternal(limit, groups, watch, presence)
    ChatClient->>Plugin: onQueryGroupedChannelsRequest(...)
    ChatClient->>MoshiChatApi: queryGroupedChannels(...)
    MoshiChatApi->>ChannelApi: POST /channels/grouped
    ChannelApi-->>MoshiChatApi: QueryGroupedChannelsResponse
    MoshiChatApi-->>ChatClient: GroupedChannels
    ChatClient->>Plugin: onQueryGroupedChannelsResult(result, ...)
    Plugin->>QueryGroupedChannelsListenerState: onQueryGroupedChannelsResult(...)
    QueryGroupedChannelsListenerState->>MutableGlobalState: setGroupedUnreadChannels(...)
    QueryGroupedChannelsListenerState->>QueryChannelsLogic: applyGroupedResult(group, isFirstPage)
    QueryChannelsLogic-->>ChannelListViewModel: updated channels via StateFlow
  end
  rect rgba(200, 150, 100, 0.5)
    Note over EventHandlerSequential: Realtime event updates
    EventHandlerSequential->>GroupedUnreadChannelsUpdater: calculateUpdatedCounts(batchId, event)
    GroupedUnreadChannelsUpdater-->>EventHandlerSequential: updated Map~groupKey,unreadCount~
    EventHandlerSequential->>MutableGlobalState: setGroupedUnreadChannels(...)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • GetStream/stream-chat-android#6437: Modifies the same Mother.kt test helpers and QueryChannelsSpec/grouped-query infrastructure that this PR extends with groupedUnreadChannels and groupKey.
  • GetStream/stream-chat-android#6477: Bumps the Room ChatDatabase schema version in the same file, directly adjacent to this PR's version increment to 202.

Suggested reviewers

  • andremion
  • gpunto

Poem

🐰 Hippity-hoppity, channels in rows,
Grouped by their keys as the data flows!
queryGroupedChannels hops through the stack,
Unread counts batched—no bunny goes slack.
From DTO to ViewModel, the carrots align,
Every group key resolves just fine! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main change: grouped channels support and grouped unread counts.
Description check ✅ Passed The description covers goal, implementation, testing, and follow-ups, matching the template's main required sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/grouped_query_channels

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

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/ChatClientStateExtensions.kt (1)

224-235: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Track aroundMessageId watch states too.

This overload still returns the raw getStateOrNull(...) flow, while the overload above wraps it in WatchedChannelStateFlow and registers it with state.trackWatchedChannel(...). With the new reconnect logic relying on tracked watched-channel flows, channels opened via watchChannelAsState(cid, messageLimit, aroundMessageId, ...) won't be re-watched after reconnect.

Suggested fix
 `@InternalStreamChatApi`
 `@JvmOverloads`
 public fun ChatClient.watchChannelAsState(
     cid: String,
     messageLimit: Int,
     aroundMessageId: String?,
     coroutineScope: CoroutineScope = CoroutineScope(DispatcherProvider.IO),
 ): StateFlow<ChannelState?> {
     StreamLog.i(TAG) {
         "[watchChannelAsState] cid: $cid, messageLimit: $messageLimit, aroundMessageId: $aroundMessageId"
     }
-    return getStateOrNull(coroutineScope) {
+    val flow = getStateOrNull(coroutineScope) {
         requestsAsState(coroutineScope).watchChannel(cid, messageLimit, chatClientConfig.userPresence, aroundMessageId)
     }
+    val watchedFlow = WatchedChannelStateFlow(flow, cid)
+    val watchedFlowRef = WeakReference(watchedFlow)
+    coroutineScope.launch {
+        runCatching {
+            clientState.initializationState.first { it == InitializationState.COMPLETE }
+            watchedFlowRef.get()?.let(state::trackWatchedChannel)
+        }
+    }
+    return watchedFlow
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/ChatClientStateExtensions.kt`
around lines 224 - 235, The `watchChannelAsState(cid, messageLimit,
aroundMessageId, coroutineScope)` overload in `ChatClientStateExtensions` should
not return the raw `getStateOrNull(...)` flow; instead, wrap the result in
`WatchedChannelStateFlow` like the other `watchChannelAsState` overload and
register it with `state.trackWatchedChannel(...)`. Make sure the
`aroundMessageId` variant uses the same watched-channel tracking path as the
existing overload so reconnect logic can re-watch those channels automatically.
stream-chat-android-client/api/stream-chat-android-client.api (1)

926-944: 🎯 Functional Correctness | 🟠 Major

Keep these public state interfaces backward-compatible. GlobalState and QueryChannelsState are public Kotlin interfaces, and the new abstract properties in the API dump make external implementations brittle at runtime. Use a default-compatible path, such as a sub-interface or default-backed accessors, instead of adding required members.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@stream-chat-android-client/api/stream-chat-android-client.api` around lines
926 - 944, The public state interfaces are being made source/binary incompatible
by adding required abstract members to GlobalState and QueryChannelsState.
Update the API so these new state accessors are exposed through a
backward-compatible approach, such as a new sub-interface or default-backed
accessors on the existing interface, rather than adding mandatory abstract
properties. Keep the existing interface contracts stable and preserve
compatibility for external implementations that already implement GlobalState or
QueryChannelsState.
stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt (1)

1379-1389: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tighten the grouped-state fixture.

Line 1389 stubs stateRegistry.queryChannels(any()), so the new groupKey tests still pass if ChannelListViewModel(groupKey = ...) looks up the standard query state or the wrong group key. Matching or capturing the expected grouped identifier here would make these tests actually protect the new state-wiring contract.

Also applies to: 1392-1401

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt`
around lines 1379 - 1389, The grouped-state fixture is too loose because
stateRegistry.queryChannels(any()) will match any key, so the new groupKey
coverage can pass even if ChannelListViewModel uses the wrong query state.
Update the stubbing in ChannelListViewModelTest around queryChannelsState to
match or capture the expected grouped identifier (the groupKey passed into
ChannelListViewModel) and apply the same tightening to the related grouped-state
setup later in the test so the fixtures verify the intended wiring.
🧹 Nitpick comments (6)
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt (1)

542-556: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a null-counter mapping test case for NotificationMarkUnreadEventDto.

Lines 553-554 always pass non-null counts, so the fallback path at Lines 1230-1231 (?: 0) isn’t directly exercised. Add one DTO/domain pair with null counts and include it in arguments().

Proposed test addition
+    private val notificationMarkUnreadDtoWithNullCounts = notificationMarkUnreadDto.copy(
+        total_unread_count = null,
+        unread_channels = null,
+    )
+
+    private val notificationMarkUnreadWithNullCounts = notificationMarkUnread.copy(
+        totalUnreadCount = 0,
+        unreadChannels = 0,
+    )
...
     fun arguments() = listOf(
...
         Arguments.of(notificationMarkUnreadDto, notificationMarkUnread),
+        Arguments.of(notificationMarkUnreadDtoWithNullCounts, notificationMarkUnreadWithNullCounts),
...
     )

Also applies to: 1218-1233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt`
around lines 542 - 556, Add a null-counter mapping test for
NotificationMarkUnreadEventDto because the existing notificationMarkUnreadDto
only covers non-null unread counts and does not exercise the ?: 0 fallback in
the mapper. Create an additional DTO/domain pair in EventMappingTestArguments
with null values for the count fields (such as unread_messages,
total_unread_count, unread_channels, and grouped_unread_channels), and include
that pair in arguments(). Use the existing EventMappingTestArguments setup and
NotificationMarkUnreadEventDto symbol to place the new case alongside the
current mapping fixtures.
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt (1)

449-479: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the missing-unread_channels fallback in the API-layer test inputs.

MoshiChatApi.queryGroupedChannels converts unread_channels with ?: 0, but this argument set only exercises non-null values. The adapter test in stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/QueryGroupedChannelsResponseAdapterTest.kt only protects DTO parsing, so the domain-mapping fallback can still regress unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt`
around lines 449 - 479, Add a test case in queryGroupedChannelsInput that
exercises QueryGroupedChannelsResponse with unread_channels set to null, so
MoshiChatApi.queryGroupedChannels’ fallback to 0 is covered. Keep the existing
RetroSuccess/RetroError structure, but include a grouped channel response with
unread_channels omitted or null in QueryGroupedChannelsGroup, and reference the
queryGroupedChannelsInput helper so the API-layer mapping path is validated
alongside the adapter test.
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt (1)

3352-3398: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add thread/state notes to the new grouped-query KDoc.

These new public APIs document parameters, but they still omit the thread expectations and state notes required for stream-chat-android-client public APIs. That contract matters here because the call now participates in plugin callbacks and grouped state restoration.

As per coding guidelines, "Document public APIs with KDoc, including thread expectations and state notes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt`
around lines 3352 - 3398, The new KDoc on ChatClient.queryGroupedChannels and
queryGroupedChannelsInternal is missing the public API thread/state contract.
Update the documentation to include the required thread expectations and state
notes alongside the existing parameter descriptions, matching the style used by
other public APIs in ChatClient. Ensure both overloads clearly state any
main-thread or background-thread expectations and any relevant state/restore
behavior.

Source: Coding guidelines

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupAwareChatEventHandlerFactory.kt (1)

19-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the singleton fallback for clientState.

Every shown call site already passes clientState, so ChatClient.instance().clientState only adds a hidden global dependency. If this constructor is ever used without the explicit argument, grouped handlers can attach to the wrong client state in tests or multi-client flows.

♻️ Proposed fix
-import io.getstream.chat.android.client.ChatClient
 import io.getstream.chat.android.client.api.event.ChatEventHandler
 import io.getstream.chat.android.client.api.event.ChatEventHandlerFactory
 import io.getstream.chat.android.client.setup.state.ClientState
@@
 internal class GroupAwareChatEventHandlerFactory(
     private val groupKey: String,
     private val resolver: ChannelGroupResolver = DefaultChannelGroupResolver(),
-    private val clientState: ClientState = ChatClient.instance().clientState,
+    private val clientState: ClientState,
 ) : ChatEventHandlerFactory(clientState) {

Also applies to: 33-37

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupAwareChatEventHandlerFactory.kt`
around lines 19 - 24, Remove the singleton fallback for clientState in
GroupAwareChatEventHandlerFactory and its grouped handler construction path. The
constructor and factory should require the explicit ClientState already being
passed by the call sites, instead of defaulting to
ChatClient.instance().clientState. Update the GroupAwareChatEventHandlerFactory
constructor and any related create/build logic so the grouped handlers always
use the provided clientState and no hidden global dependency remains.
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerStateTest.kt (1)

287-388: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a mixed first-page + paginated regression case.

These tests only cover responses where every requested group is either first-page or paginated. They won’t catch the case where one group carries a cursor and another is still a first-page refresh, which is where grouped unread merging diverges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerStateTest.kt`
around lines 287 - 388, Add a mixed regression test in
QueryGroupedChannelsListenerStateTest for onQueryGroupedChannelsResult where one
requested group has a cursor (next or prev) and another remains a first-page
refresh. Verify the paginated group is treated as not first page while the other
still uses first-page behavior, and that grouped unread merging/update logic
handles both cases in the same response. Reuse the existing listener,
queryChannelsLogic, and GroupedChannelsGroupQuery setup patterns from the nearby
tests to locate the correct assertions.
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/internal/SyncManagerTest.kt (1)

93-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document or remove the new suppression annotations.

These suppressions are newly added but not documented with rationale. Please add short justification comments (or refactor to avoid suppression).

Suggested patch
-@Suppress("LargeClass")
+// Large reconnect coverage matrix is intentionally centralized in this test class.
+@Suppress("LargeClass")
 `@OptIn`(InternalStreamChatApi::class)
 `@ExperimentalCoroutinesApi`
 internal class SyncManagerTest {
@@
-    `@Suppress`("LongMethod")
+    // Kept as a single scenario for readability of end-to-end reconnect assertions.
+    `@Suppress`("LongMethod")
     fun `dual-mode reconnect updateActiveChannels should query only cids not covered by grouped or standard`() =
@@
-    `@Suppress`("LongMethod")
+    // Kept as a single scenario for readability of end-to-end reconnect assertions.
+    `@Suppress`("LongMethod")
     fun `dual-mode reconnect should exclude tracked watched cids from updateActiveChannels`() =

As per coding guidelines, "Use @OptIn annotations explicitly in Kotlin code; avoid suppressions unless documented".

Also applies to: 818-819, 907-908

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/internal/SyncManagerTest.kt`
at line 93, The new suppression annotations in SyncManagerTest are undocumented
and should be justified or removed. Update the affected spots in
SyncManagerTest, including the LargeClass suppression and the other newly added
suppressions, by either refactoring to avoid them or adding brief inline
comments explaining why each suppression is necessary. Keep the rationale near
the relevant annotations so future readers understand the exception.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@stream-chat-android-client/api/stream-chat-android-client.api`:
- Around line 1842-1856: MarkAllReadEvent now exposes a changed JVM
constructor/copy signature because groupedUnreadChannels was added, which can
break already-compiled callers. Update MarkAllReadEvent in the API surface to
preserve the old constructor and copy(...) descriptors for binary compatibility,
and introduce the new groupedUnreadChannels path via overloads or another
backward-compatible mechanism rather than replacing the existing signatures.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryChannelsState.kt`:
- Around line 74-82: QueryChannelsState is exposing an internal
GroupedQueryConfig type through a public StateFlow, which leaks an internal
package into the SDK API surface. Update the QueryChannelsState contract so it
no longer returns
io.getstream.chat.android.client.internal.state.plugin.state.querychannels.GroupedQueryConfig
directly: either move the config model to a public package or replace
groupedQueryConfig with a public-facing representation/individual fields. Keep
the change additive and preserve binary compatibility by updating any related
state/query code paths that read or write groupedQueryConfig.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/model/querychannels/pagination/internal/Mapper.kt`:
- Around line 26-33: The offline pagination mapping in
QueryChannelsRequest.toOfflinePaginationRequest() is dropping memberLimit before
it reaches AnyChannelPaginationRequest. Update the conversion path through
QueryChannelsPaginationRequest.toAnyChannelPaginationRequest() so memberLimit is
copied alongside messageLimit, keeping the offline query aligned with the
original QueryChannelsRequest.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerState.kt`:
- Around line 68-76: The grouped unread counts update is currently gated on the
entire request being a first page, which causes mixed responses to skip unread
merging for groups that are still first-page refreshes. Update
QueryGroupedChannelsListenerState’s pagination check so each returned group is
evaluated independently, and only skip unread count merging for the specific
groups that have next/prev cursors while still applying
groupedUnreadChannelsUpdater.calculateUpdatedCounts and
globalState.setGroupedUnreadChannels for the eligible groups.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogic.kt`:
- Around line 251-255: trackChannel is still writing the passed Channel snapshot
even when a fresher active ChannelState already exists. Update
QueryChannelsStateLogic.trackChannel to look up the current channel by cid from
the active state/raw state first and reuse that value when present, only falling
back to the channel argument when nothing is tracked yet. Keep the cids update
as-is, but make the setChannels call prefer the existing in-state channel over
the stale parameter.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/internal/QueryChannelsMutableState.kt`:
- Around line 87-90: The grouped QueryChannelsSpec in QueryChannelsMutableState
is using a placeholder QuerySortByField.descByName("last_updated"), which causes
grouped results to be re-sorted by the client instead of preserving server
order. Update the grouped branch in the identifier handling to avoid injecting a
fake sort and make the grouped path bypass the sortedChannels pipeline or
otherwise keep the response order intact. Use the
QueryChannelsIdentifier.Grouped and QueryChannelsSpec construction as the main
places to adjust this behavior.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/sync/internal/SyncManager.kt`:
- Around line 419-425: The grouped-query recovery path in SyncManager should not
swallow failures from queryGroupedChannelsInternal by converting them to
emptySet(), because restoreActiveChannels then treats a failed refresh as
success. Update the grouped-query flow in
updateGroupedQueryChannels/queryGroupedChannelsInternal so errors are propagated
like the standard path, and let restoreActiveChannels abort or surface the
failure instead of continuing with stale grouped query state.
- Around line 445-449: The fallback in SyncManager.updateActiveChannels is being
skipped whenever hasGroupedQueries is true, which leaves some active
ChannelLogic states stale after reconnect. Update the recovery flow in
SyncManager so the fallback still runs when only grouped queries exist, and pass
groupedHandledCids as the cidsToExclude set instead of bypassing
updateActiveChannels entirely. Keep the existing standard-query behavior intact
by preserving the hasStandardQueries path and adjusting the conditional around
the active-channel recovery block.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequentialTest.kt`:
- Around line 534-619: The grouped unread cases in
groupedUnreadChannelsArguments() are defined but never executed, so add a
`@ParameterizedTest` in EventHandlerSequentialTest that consumes this provider,
sets up MutableGlobalState.groupedUnreadChannels with the initial value, runs
the event handling flow used by the other sequential tests, and asserts the
final grouped unread map matches the expected result for each case. Use the
existing helpers and symbols like groupedUnreadChannelsArguments(),
MutableGlobalState, and the event handler test harness in this class to wire the
new parameterized coverage into the suite.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt`:
- Around line 1026-1032: The grouped search branch in ChannelListViewModel’s
QueryMode.Grouped path is missing the selected group constraint, so it can
return channels from other groups. Update the QueryChannelsRequest built there
to include the channel group predicate whenever the current group key is not the
sentinel value, and keep the existing optimizedChannelSearchFilter(searchQuery)
behavior intact. Use the QueryMode.Grouped branch and
optimizedChannelSearchFilter logic as the main locations to adjust so grouped
lists stay scoped to the channel’s group custom field.
- Around line 1155-1160: Grouped load-more failures are only logged in
loadMoreGroupedChannels and never reflected in channelsState, so the UI can miss
pagination errors. Update ChannelListViewModel’s grouped pagination path to set
loadingError when result.isSuccess is false and clear it when the load succeeds,
alongside the existing isLoadingMore reset. Make the state update in the same
place where loadMoreGroupedChannels handles result and channelsState.copy so
grouped pagination behaves like the standard pagination flow.
- Around line 242-249: The grouped ChannelListViewModel constructor currently
collides with the predefined-filter overload because
ChannelListViewModel(chatClient, "...") can match both when the trailing
parameters all have defaults. Update the grouped entry point in
ChannelListViewModel so it stays unambiguous, and align
ChannelListViewModelFactory to pass groupKey first so the grouped constructor is
clearly distinct from the predefined one.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelFactory.kt`:
- Around line 135-150: The grouped ChannelListViewModelFactory constructor
currently defaults draftMessagesEnabled differently than the grouped
ChannelListViewModel constructor, so align the factory’s default with the
grouped ViewModel path. Update the grouped constructor in
ChannelListViewModelFactory to use the same default value as
ChannelListViewModel(groupKey, ...) and keep the forwarded draftMessagesEnabled
parameter unchanged so grouped mode behaves consistently regardless of
construction path.

---

Outside diff comments:
In `@stream-chat-android-client/api/stream-chat-android-client.api`:
- Around line 926-944: The public state interfaces are being made source/binary
incompatible by adding required abstract members to GlobalState and
QueryChannelsState. Update the API so these new state accessors are exposed
through a backward-compatible approach, such as a new sub-interface or
default-backed accessors on the existing interface, rather than adding mandatory
abstract properties. Keep the existing interface contracts stable and preserve
compatibility for external implementations that already implement GlobalState or
QueryChannelsState.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/ChatClientStateExtensions.kt`:
- Around line 224-235: The `watchChannelAsState(cid, messageLimit,
aroundMessageId, coroutineScope)` overload in `ChatClientStateExtensions` should
not return the raw `getStateOrNull(...)` flow; instead, wrap the result in
`WatchedChannelStateFlow` like the other `watchChannelAsState` overload and
register it with `state.trackWatchedChannel(...)`. Make sure the
`aroundMessageId` variant uses the same watched-channel tracking path as the
existing overload so reconnect logic can re-watch those channels automatically.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt`:
- Around line 1379-1389: The grouped-state fixture is too loose because
stateRegistry.queryChannels(any()) will match any key, so the new groupKey
coverage can pass even if ChannelListViewModel uses the wrong query state.
Update the stubbing in ChannelListViewModelTest around queryChannelsState to
match or capture the expected grouped identifier (the groupKey passed into
ChannelListViewModel) and apply the same tightening to the related grouped-state
setup later in the test so the fixtures verify the intended wiring.

---

Nitpick comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt`:
- Around line 3352-3398: The new KDoc on ChatClient.queryGroupedChannels and
queryGroupedChannelsInternal is missing the public API thread/state contract.
Update the documentation to include the required thread expectations and state
notes alongside the existing parameter descriptions, matching the style used by
other public APIs in ChatClient. Ensure both overloads clearly state any
main-thread or background-thread expectations and any relevant state/restore
behavior.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupAwareChatEventHandlerFactory.kt`:
- Around line 19-24: Remove the singleton fallback for clientState in
GroupAwareChatEventHandlerFactory and its grouped handler construction path. The
constructor and factory should require the explicit ClientState already being
passed by the call sites, instead of defaulting to
ChatClient.instance().clientState. Update the GroupAwareChatEventHandlerFactory
constructor and any related create/build logic so the grouped handlers always
use the provided clientState and no hidden global dependency remains.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt`:
- Around line 542-556: Add a null-counter mapping test for
NotificationMarkUnreadEventDto because the existing notificationMarkUnreadDto
only covers non-null unread counts and does not exercise the ?: 0 fallback in
the mapper. Create an additional DTO/domain pair in EventMappingTestArguments
with null values for the count fields (such as unread_messages,
total_unread_count, unread_channels, and grouped_unread_channels), and include
that pair in arguments(). Use the existing EventMappingTestArguments setup and
NotificationMarkUnreadEventDto symbol to place the new case alongside the
current mapping fixtures.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt`:
- Around line 449-479: Add a test case in queryGroupedChannelsInput that
exercises QueryGroupedChannelsResponse with unread_channels set to null, so
MoshiChatApi.queryGroupedChannels’ fallback to 0 is covered. Keep the existing
RetroSuccess/RetroError structure, but include a grouped channel response with
unread_channels omitted or null in QueryGroupedChannelsGroup, and reference the
queryGroupedChannelsInput helper so the API-layer mapping path is validated
alongside the adapter test.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/internal/SyncManagerTest.kt`:
- Line 93: The new suppression annotations in SyncManagerTest are undocumented
and should be justified or removed. Update the affected spots in
SyncManagerTest, including the LargeClass suppression and the other newly added
suppressions, by either refactoring to avoid them or adding brief inline
comments explaining why each suppression is necessary. Keep the rationale near
the relevant annotations so future readers understand the exception.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerStateTest.kt`:
- Around line 287-388: Add a mixed regression test in
QueryGroupedChannelsListenerStateTest for onQueryGroupedChannelsResult where one
requested group has a cursor (next or prev) and another remains a first-page
refresh. Verify the paginated group is treated as not first page while the other
still uses first-page behavior, and that grouped unread merging/update logic
handles both cases in the same response. Reuse the existing listener,
queryChannelsLogic, and GroupedChannelsGroupQuery setup patterns from the nearby
tests to locate the correct assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0961f914-409e-4a44-bff2-3ef5a0a718c1

📥 Commits

Reviewing files that changed from the base of the PR and between 2a4b714 and 8adcb32.

📒 Files selected for processing (71)
  • stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/Mother.kt
  • stream-chat-android-client/api/stream-chat-android-client.api
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/ChatClientStateExtensions.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/GlobalState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/QueryChannelsState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/state/StateRegistry.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/endpoint/ChannelApi.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/QueryGroupedChannelsRequest.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/response/QueryGroupedChannelsResponse.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/database/internal/ChatDatabase.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/queryChannels/internal/DatabaseQueryChannelsRepository.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/queryChannels/internal/QueryChannelsEntity.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/ChannelGroupExtensions.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/ChannelGroupResolver.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/DefaultChannelGroupResolver.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupAwareChatEventHandler.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupAwareChatEventHandlerFactory.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupedUnreadChannelsUpdater.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequential.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/model/querychannels/pagination/internal/Mapper.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/QueryChannelsIdentifier.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/factory/StreamStatePluginFactory.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/internal/StatePlugin.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryChannelsListenerState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/internal/LogicRegistry.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogic.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogic.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/global/internal/MutableGlobalState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/internal/ChatClientStateCalls.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/internal/WatchedChannelStateFlow.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/GroupedQueryConfig.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/internal/QueryChannelsMutableState.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/sync/internal/SyncManager.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/Plugin.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/plugin/listeners/QueryGroupedChannelsListener.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/query/QueryChannelsSpec.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/ChatClientGroupedChannelsApiTests.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/EventChatJsonProvider.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/MoshiChatApiTestArguments.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/Mother.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/QueryChannelsImplRepositoryTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/DefaultChannelGroupResolverTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupAwareChatEventHandlerTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/event/handler/grouped/internal/GroupedUnreadChannelsUpdaterTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/event/handler/internal/EventHandlerSequentialTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/internal/SyncManagerTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/listener/internal/QueryGroupedChannelsListenerStateTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/internal/LogicRegistryTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogicGroupedTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogicTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogicTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/StateRegistryTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/internal/ChatClientStateCallsTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/querychannels/internal/QueryChannelsMutableStateTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser/EventArguments.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/parser2/QueryGroupedChannelsResponseAdapterTest.kt
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelFactory.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt
  • stream-chat-android-core/api/stream-chat-android-core.api
  • stream-chat-android-core/src/main/java/io/getstream/chat/android/models/GroupedChannels.kt

Comment thread stream-chat-android-client/api/stream-chat-android-client.api

@aleksandar-apostolov aleksandar-apostolov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — deferred V6 tests tracked in AND-1261.

- Move GroupedQueryConfig to public api.state.querychannels package
- Propagate memberLimit through offline pagination mapper
- Add ParameterizedTest for grouped unread channel events
- Reorder ChannelListViewModel grouped constructor to (groupKey, chatClient)
  to disambiguate from the predefined constructor
- Set loadingError in loadMoreGroupedChannels for UI feedback on failures

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

val total_unread_count: Int = 0,
val unread_channels: Int = 0,
val channel_message_count: Int? = null,
val grouped_unread_channels: Map<String, Int>? = null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not a blocker: This DTO gets grouped_unread_channels and EventMapping maps it onto NewMessageEvent, but message.new also has a hand-written parser, NewMessageEventAdapter, that runs when fastEventParsing is on (registered in DirectEventParser). That adapter is not part of this PR and it does not read grouped_unread_channels, so it builds NewMessageEvent with the field as null. Once the direct parser succeeds, MoshiChatParser returns its result and skips the DTO path, so with fastEventParsing on the grouped counts sent on message.new get dropped.
It is a narrow case (only when both fastEventParsing and grouped channels are enabled), and it is not a crash. The counts still update from the notification events and from channel.updated. It also does not match the "both paths produce identical events" check in NewMessageEventParsingTest, which passes here only because jsonAllFields does not include the field yet.
Could we add the field to NewMessageEventAdapter, and to jsonAllFields so the parity test covers it? Happy to take it in a follow-up if you would rather keep this PR focused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice catch, I will add it here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants