Skip to content

Grouped channels endpoint (v5)#4076

Draft
laevandus wants to merge 29 commits intodevelopfrom
grouped-channels-v5
Draft

Grouped channels endpoint (v5)#4076
laevandus wants to merge 29 commits intodevelopfrom
grouped-channels-v5

Conversation

@laevandus
Copy link
Copy Markdown
Contributor

@laevandus laevandus commented Apr 28, 2026

🔗 Issue Links

Resolves https://linear.app/stream/issue/IOS-1635/support-for-grouped-channels-endpoint.

🎯 Goal

  • Add ChatClient.queryGroupedChannels(limit:watch:presence:) to fetch grouped channel groups as a GroupedChannels model, preserving backend group keys and exposing
    per-group channels and unread counts
  • Add ChatChannelListController.prefill(group:completion:) for priming a controller with a GroupedChannelsGroup and skipping the first remote queryChannels, while
    keeping pagination, observation, and offline refresh working
  • Add groupedUnreadChannels to CurrentChatUser, decoded from grouped-unread WebSocket events (NotificationMessageNew, NotificationMarkRead, NotificationMarkUnread)
    via the new HasGroupedUnreadChannels protocol, and persisted on CurrentUserDTO
  • Add ChannelListQuery.groupKey for stable query identity across date-bearing filters and a new FilterKey.messageCount (with local fallback to ChannelDTO.messageCount)

📝 Summary

Test on https://github.com/GetStream/GroupedChannelsSample.

This is the v5 port of the grouped-channels-endpoint work that originally landed on the v4 line. All changes are additive on the public API.

🛠 Implementation

  • New POST /channels/grouped endpoint wired through ChannelEndpoints/EndpointPath, with GroupedQueryChannelsRequestBody / GroupedQueryChannelsPayload /
    GroupedQueryChannelsGroupPayload
  • SyncGroupedChannelsOperation runs during the offline-sync chain for any controller whose query.groupKey is set: a single queryGroupedChannels call replaces
    per-controller refreshLoadedChannels and forwards each returned group back through the matching controller's prefill(group:)
  • ChannelListLinker now unlinks before relinking on MessageNew / NotificationMessageNew so a channel can move between query-backed lists when its group changes
  • Lightweight Core Data migration: optional CurrentUserDTO.groupedUnreadChannelsData attribute (JSON-encoded [String: Int]); no model version bump
  • Adapted to Swift 6 strict concurrency on develop: explicit Sendable conformance on the new payloads/models, @Sendable on completion handlers, nonisolated(unsafe) for
    mutated locals captured in database.write blocks, and @Sendable annotations on the inner dispatchGroup.notify / prefill closures inside SyncGroupedChannelsOperation
    to escape @MainActor inheritance from the queryGroupedChannels callback (otherwise it crashed at _swift_task_checkIsolatedSwift on .utility-qos)
  • Default BaseURL temporarily points at the Dublin edge endpoint (chat-edge-dublin-ce2.stream-io-api.com) for backend testing — revert before merge

🎨 Showcase

Add relevant screenshots and/or videos/gifs to easily see what this PR changes, if applicable.

Before After
img img

🧪 Manual Testing Notes

Explain how this change can be tested manually, if applicable.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

martinmitrevski and others added 24 commits April 28, 2026 09:39
…sent (avoids manually keeping the message count up to date which is part of the count_messages app setting)
…ersist using it

 - Add internal ChannelListQuery.groupKey and queryHash (groupKey ?? filter.filterHash);
    ChannelListQueryDTO now uses this stable identity so date-bearing filters from the
    grouped endpoint don't churn filterHash every second.
  - Rename channelListQuery(filterHash:) -> channelListQuery(_:) since every caller had
    the full ChannelListQuery in scope anyway.
  - Rename GroupedChannelsGroup.group -> groupKey for naming symmetry.
  - prefill(group:) sets query.groupKey before worker.prefill; drop the redundant
    ChatChannelListController.prefilledGroupKey in favor of query.groupKey as the single
    source of truth.
  - SyncRepository routes prefilled controllers through queryGroupedChannels and
    SyncGroupedChannelsOperation forwards each group back to the matching controller's
    prefill(group:) using query.groupKey.
@laevandus laevandus requested a review from a team as a code owner April 28, 2026 08:02
@laevandus laevandus marked this pull request as draft April 28, 2026 08:02
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8265b369-e92e-4494-87b3-555ee3f415e4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch grouped-channels-v5

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamChat 6.74 MB 6.8 MB +59 KB 🟢
StreamChatUI 4.25 MB 4.25 MB 0 KB 🟢
StreamChatCommonUI 0.8 MB 0.8 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChat XCSize

Object Diff (bytes)
ChannelListPayload.o +12929
ChatClient.o +6758
SyncOperations.o +5268
ChannelListController.o +3288
RequestEncoder.o +2482
Show 37 more objects
Object Diff (bytes)
GroupedChannels.o +1555
StreamCDNStorage.o +1476
EndpointPath.o +1395
ChannelListQuery.o +1202
CurrentUserDTO.o +1102
UnknownChannelEvent.o +1052
LivestreamChannelController.o +936
ChannelListUpdater.o +779
ChannelUpdater.o +768
SyncRepository.o +660
ChannelEvents.o +608
MessageUpdater.o +596
NotificationEvents.o +592
APIClient.o +386
ChannelListLinker.o +352
BackgroundDatabaseObserver.o +281
CurrentUser.o +258
MessageEvents.o +252
EventPayload.o +237
AnyAttachmentPayload.o +224
MessagePayloads.o -220
AttachmentDownloader.o +219
AttachmentTypes.o +192
CurrentUserPayloads.o -172
Event.o +162
ChannelListQueryDTO.o +148
PollsPayloads.o -120
UserListController.o -110
DraftPayloads.o -107
PushDevice.o +72
MessageReactionGroupDTO.o +68
Filter.o +60
PollController.o -56
MessageSender.o -56
CurrentUserUpdater.o -54
ChannelMemberListUpdater.o +48
LocationPayloads.o -48

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChatUI XCSize

Object Diff (bytes)
ChatChannelListRouter.o -44

public struct BaseURL: CustomStringConvertible, Sendable {
/// The default base URL for StreamChat service.
public static let `default` = BaseURL(urlString: "https://chat.stream-io-api.com/")!
public static let `default` = BaseURL(urlString: "https://chat-edge-dublin-ce2.stream-io-api.com/")!
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.

Note for not committing

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

1 Message
📖 Skipping Danger since the Pull Request is classed as Draft/Work In Progress

Generated by 🚫 Danger

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

Public Interface

+ public struct GroupedChannelsGroup: Equatable, Sendable  
+ 
+   public let groupKey: String
+   public let channels: [ChatChannel]
+   public let unreadChannels: Int

+ public protocol HasGroupedUnreadChannels: Event

+ public struct GroupedChannels: Equatable, Sendable  
+ 
+   public let groups: [String: GroupedChannelsGroup]



 @MainActor public final class ChannelListState: ObservableObject  
-   public let query: ChannelListQuery
+   public private var query: ChannelListQuery

- public final class MessageNewEvent: ChannelSpecificEvent, HasUnreadCount  
+ public final class MessageNewEvent: ChannelSpecificEvent, HasUnreadCount, HasGroupedUnreadChannels  
+   public let groupedUnreadChannels: GroupedUnreadChannels?

 public class CurrentChatUser: ChatUser, @unchecked Sendable  
-   public let isInvisible: Bool
+   public let groupedUnreadChannels: GroupedUnreadChannels?
-   public let privacySettings: UserPrivacySettings
+   public let isInvisible: Bool
-   public let pushPreference: PushPreference?
+   public let privacySettings: UserPrivacySettings
+   public let pushPreference: PushPreference?

- public final class NotificationChannelDeletedEvent: ChannelSpecificEvent  
+ public final class NotificationChannelDeletedEvent: ChannelSpecificEvent, HasGroupedUnreadChannels  
+   public let groupedUnreadChannels: GroupedUnreadChannels?

 public class ChannelList: @unchecked Sendable  
-   @discardableResult public func loadChannels(with pagination: Pagination)async throws -> [ChatChannel]
+   public func prefill(group: GroupedChannelsGroup)async throws 
-   @discardableResult public func loadMoreChannels(limit: Int? = nil)async throws -> [ChatChannel]
+   @discardableResult public func loadChannels(with pagination: Pagination)async throws -> [ChatChannel]
+   @discardableResult public func loadMoreChannels(limit: Int? = nil)async throws -> [ChatChannel]

 public class ChatClient: @unchecked Sendable  
-   public func uploadAttachment(localUrl: URL,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void)
+   public func queryGroupedChannels(limit: Int? = nil,watch: Bool = false,presence: Bool = false,completion: @escaping @MainActor (Result<GroupedChannels, Error>) -> Void)
-   public func deleteAttachment(remoteUrl: URL,completion: @escaping @Sendable (Error?) -> Void)
+   public func queryGroupedChannels(limit: Int? = nil,watch: Bool = false,presence: Bool = false)async throws -> GroupedChannels
+   public func uploadAttachment(localUrl: URL,progress: (@Sendable (Double) -> Void)?,completion: @escaping @Sendable (Result<UploadedFile, Error>) -> Void)
+   public func deleteAttachment(remoteUrl: URL,completion: @escaping @Sendable (Error?) -> Void)

- public final class NotificationMarkReadEvent: ChannelSpecificEvent, HasUnreadCount  
+ public final class NotificationMarkReadEvent: ChannelSpecificEvent, HasUnreadCount, HasGroupedUnreadChannels  
-   public let lastReadMessageId: MessageId?
+   public let groupedUnreadChannels: GroupedUnreadChannels?
-   public let createdAt: Date
+   public let lastReadMessageId: MessageId?
+   public let createdAt: Date

- public final class NotificationMessageNewEvent: ChannelSpecificEvent, HasUnreadCount  
+ public final class NotificationMessageNewEvent: ChannelSpecificEvent, HasUnreadCount, HasGroupedUnreadChannels  
+   public let groupedUnreadChannels: GroupedUnreadChannels?

- public final class NotificationMarkUnreadEvent: ChannelSpecificEvent  
+ public final class NotificationMarkUnreadEvent: ChannelSpecificEvent, HasGroupedUnreadChannels  
-   public let unreadMessagesCount: Int
+   public let groupedUnreadChannels: GroupedUnreadChannels?
+   public let unreadMessagesCount: Int

 public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable  
-   public let query: ChannelListQuery
+   public private var query: ChannelListQuery
+   public func prefill(group: GroupedChannelsGroup,completion: (@Sendable (Error?) -> Void)? = nil)

- public final class ChannelTruncatedEvent: ChannelSpecificEvent  
+ public final class ChannelTruncatedEvent: ChannelSpecificEvent, HasGroupedUnreadChannels  
+   public let groupedUnreadChannels: GroupedUnreadChannels?

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 4, 2026

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants