Skip to content

Add GroupedQueryChannels and grouped unread counts#6437

Draft
VelikovPetar wants to merge 43 commits into
v6from
feature/grouped-channels-endpoint
Draft

Add GroupedQueryChannels and grouped unread counts#6437
VelikovPetar wants to merge 43 commits into
v6from
feature/grouped-channels-endpoint

Conversation

@VelikovPetar
Copy link
Copy Markdown
Contributor

@VelikovPetar VelikovPetar commented May 13, 2026

Goal

Add 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. Surface those grouped unread counts on relevant chat events, and provide a Compose ChannelListViewModel path that drives a UI off a group key without the consumer needing to know about filter/sort.

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:
    • QueryChannelsIdentifier.Standard(filter, sort) — existing offset-paginated path
    • QueryChannelsIdentifier.Grouped(groupKey) — new cursor-paginated path
  • Logic: QueryChannelsLogic branches on identifier. applyGroupedResult replaces channels on the first page (resetting channelsOffset defensively to keep the Standard offset paginator from picking up stale state), appends on subsequent pages (driven off the request's next cursor), and persists per-group state under a groupKey-derived DB key.
  • Events: new HasGroupedUnreadChannels marker on NewMessageEvent, NotificationMessageNewEvent, NotificationMarkReadEvent, NotificationMarkUnreadEvent, NotificationChannelDeletedEvent, NotificationChannelTruncatedEvent. EventHandlerSequential updates GlobalState.groupedUnreadChannels whenever an inbound event carries the map. GroupedUnreadChannelsUpdater is the single calculator: events with a non-null map replace the current state, channel.updated/channel.updated_by_user events migrate per-group counts when the channel's group field changes, and queryGroupedChannels results merge per-group counts.
  • Group-aware event routing: new GroupAwareChatEventHandler classifies channel-bearing events using a pluggable ChannelGroupResolver. The default resolver reads channel.extraData["group"] and always includes an "all" sentinel. Channels are routed Add/Remove/Skip per inbound group. The LogicRegistry auto-install of the default factory is idempotent — it won't clobber a factory another caller has already installed on the state. Member/CID events delegate to DefaultChatEventHandler unchanged.
  • Compose: new ChannelListViewModel(chatClient, groupKey, ...) constructor + matching ChannelViewModelFactory(chatClient, groupKey, ...). Wires the VM to the identifier-keyed state via initGroupedQueryChannelsAsState, with a group-aware event handler factory keyed on groupKey. Pagination uses cursor-based queryGroupedChannels(groups = mapOf(groupKey to GroupedChannelsGroupQuery(next = cursor))). The Standard path is untouched.
  • Sync/recovery: SyncManager.restoreActiveChannels() splits standard vs grouped reconnect paths. Grouped queries are refreshed via a single queryGroupedChannels() call; manually-watched channels are re-watched via WatchedChannelRecord/WatchedChannelStateFlow (weak-referenced from StateRegistry). Recovery assumes all active grouped queries share the same request-level limit/watch/presence flags — the first captured config wins.
  • QueryChannelsSpec: new optional groupKey field for grouped identity. cids remains a mutable var for backward compatibility with prior versions; the two-arg constructor and 2-arg copy are preserved for source/binary compat.
  • DB: schema bumped to 99 for the new groupKey column on QueryChannelsEntity. Uses the existing fallbackToDestructiveMigration strategy.

Testing

Unit-test coverage added for each layer:

  • Endpoint dispatch + plugin notification: ChatClientGroupedChannelsApiTests
  • Moshi serialization (request/response): MoshiChatApiTest, QueryGroupedChannelsResponseAdapterTest
  • Event mapping (new grouped_unread_channels field): EventMappingTestArguments
  • Group-aware event routing: GroupAwareChatEventHandlerTest, DefaultChannelGroupResolverTest
  • Grouped unread counts calculator: GroupedUnreadChannelsUpdaterTest
  • Listener state merge / first-page vs paginated detection / failure path: QueryGroupedChannelsListenerStateTest
  • Sync recovery split: SyncManagerTest
  • Identifier-keyed state registry: StateRegistryTest, QueryChannelsMutableStateTest
  • Logic registry Grouped identifier handling (creation, idempotent retrieval, auto-installed factory): LogicRegistryTest
  • QueryChannelsLogic grouped behavior: QueryChannelsLogicGroupedTest covers applyGroupedResult (first-page replace, subsequent-page append, cursor/end-of-channels, DB persistence, defensive channelsOffset reset, no-op on Standard) and loadOfflineGroupedChannels (cache load, race-condition guard, null cache, no-op on Standard)
  • Compose grouped init: ChatClientStateCallsTest
  • Global state groupedUnreadChannels propagation: EventHandlerSequentialTest

Manually verified the Compose sample app in both Standard and Grouped modes: initial render, cursor pagination, event-driven Add/Remove/Skip across groups, reconnect/recovery, and grouped unread counts updating from inbound events.

# Conflicts:
#	stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/querychannels/internal/QueryChannelsLogicTest.kt
#	stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/querychannels/internal/QueryChannelsStateLogicTest.kt
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

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 ignored for dependabot PRs).

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.26 MB 5.30 MB 0.04 MB 🟢
stream-chat-android-offline 5.49 MB 5.52 MB 0.03 MB 🟢
stream-chat-android-ui-components 10.64 MB 10.71 MB 0.06 MB 🟢
stream-chat-android-compose 12.87 MB 12.93 MB 0.06 MB 🟢

@github-actions
Copy link
Copy Markdown
Contributor

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

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

@VelikovPetar VelikovPetar added the pr:new-feature New feature label May 14, 2026
VelikovPetar and others added 15 commits May 14, 2026 19:42
- Guard GroupAwareChatEventHandlerFactory auto-install in LogicRegistry to
  be idempotent (preserve any factory already installed on the state).
- Defensively reset channelsOffset on first-page applyGroupedResult.
- Rename QueryChannelsIdentifier.Grouped.group to .groupKey for naming
  consistency with the rest of the grouped surface.
- Add grouped-channels tests: QueryChannelsLogicGroupedTest covers
  applyGroupedResult and loadOfflineGroupedChannels; extend LogicRegistryTest
  with Grouped identifier coverage.
- Doc tweaks: SyncManager.updateGroupedQueryChannels assumption,
  DefaultChannelGroupResolver currentGroup intent, drop stale
  @Suppress("LongMethod") on observeQueryChannelsInternal.

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

Quality Gate Failed Quality Gate failed

Failed conditions
71.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

# Conflicts:
#	stream-chat-android-state/src/test/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequentialTest.kt
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.

1 participant