Grouped channels endpoint#4071
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Generated by 🚫 Danger |
SDK Performance
|
|
|
||
| /// A Boolean value that returns whether pagination is finished | ||
| public private(set) var hasLoadedAllPreviousChannels: Bool = false | ||
| private var loadedChannelsCount = 0 |
There was a problem hiding this comment.
Thinking about why we need extra state here because it will get out of sync when channel linker inserts or removes channels for this query. Why can't we use channels.count. Prefill saves channels for query so DB observer should reflect that.
| let prefilledChannels = filter.map { runtimeFilter in | ||
| channels.filter(runtimeFilter) | ||
| } ?? channels |
There was a problem hiding this comment.
If this is the first snapshot returned by backend, do we need to do this?
| databaseContainer.write { session in | ||
| let groupedUnreadChannels = payload.groups.mapValues(\.unreadChannels) | ||
| try session.saveCurrentUserGroupedUnreadChannels(groupedUnreadChannels) | ||
| } | ||
| databaseContainer.write(converting: { session in | ||
| try Self.groupedChannels(from: payload, session: session) | ||
| }, completion: completion) |
There was a problem hiding this comment.
Would be better to have a single write there because currently errors from the first one are ignored,
| /// A grouped channels response returned by `ChatClient.groupedQueryChannels`. | ||
| public struct GroupedChannels: Equatable { | ||
| /// The grouped channel groups returned by the backend, keyed by group name. | ||
| public let groups: [String: GroupedChannelsGroup] | ||
|
|
||
| public init( | ||
| groups: [String: GroupedChannelsGroup] | ||
| ) { | ||
| self.groups = groups | ||
| } | ||
| } | ||
|
|
||
| /// A grouped channels group returned by `ChatClient.groupedQueryChannels`. | ||
| public struct GroupedChannelsGroup: Equatable { | ||
| /// The channels that belong to this group. | ||
| public let channels: [ChatChannel] | ||
|
|
||
| /// The total unread channel count in the group. | ||
| public let unreadChannels: Int | ||
|
|
||
| public init( | ||
| channels: [ChatChannel], | ||
| unreadChannels: Int | ||
| ) { | ||
| self.channels = channels | ||
| let derivedUnreadChannels = channels.reduce(into: 0) { partialResult, channel in | ||
| if channel.unreadCount.messages > 0 { | ||
| partialResult += 1 | ||
| } | ||
| } | ||
|
|
||
| self.unreadChannels = max(unreadChannels, derivedUnreadChannels) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
These should also be moved to Models folder for keeping ChatClient.swift shorter,
There was a problem hiding this comment.
we should also change them to final class
| public struct BaseURL: CustomStringConvertible { | ||
| /// 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/")! |
There was a problem hiding this comment.
Reminder for this change
…sent (avoids manually keeping the message count up to date which is part of the count_messages app setting)
| // MARK: - Grouped Channels | ||
|
|
||
| /// Queries grouped channel groups for the app. | ||
| public func queryGroupedChannels( |
There was a problem hiding this comment.
Reminder that sync repository needs to use this endpoint as well for not spamming query channels endpoint when app reconnects.
…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.
Public Interface+ public protocol HasGroupedUnreadChannels: Event
+ public struct GroupedChannels: Equatable
+
+ public let groups: [String: GroupedChannelsGroup]
+ public struct GroupedChannelsGroup: Equatable
+
+ public let groupKey: String
+ public let channels: [ChatChannel]
+ public let unreadChannels: Int
- public final class ChannelTruncatedEvent: ChannelSpecificEvent
+ public final class ChannelTruncatedEvent: ChannelSpecificEvent, HasGroupedUnreadChannels
+ public let groupedUnreadChannels: GroupedUnreadChannels?
- public final class NotificationChannelDeletedEvent: ChannelSpecificEvent
+ public final class NotificationChannelDeletedEvent: ChannelSpecificEvent, HasGroupedUnreadChannels
+ public let groupedUnreadChannels: GroupedUnreadChannels?
- 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 class CurrentChatUser: ChatUser
- 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 MessageNewEvent: ChannelSpecificEvent, HasUnreadCount
+ public final class MessageNewEvent: ChannelSpecificEvent, HasUnreadCount, HasGroupedUnreadChannels
+ public let groupedUnreadChannels: GroupedUnreadChannels?
public class ChatClient
- public func upload(_ attachment: StreamAttachment<Payload>,progress: ((Double) -> Void)?,completion: @escaping (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 uploadAttachment(localUrl: URL,progress: ((Double) -> Void)?,completion: @escaping (Result<UploadedFile, Error>) -> Void)
+ public func queryGroupedChannels(limit: Int? = nil,watch: Bool = false,presence: Bool = false)async throws -> GroupedChannels
- public func deleteAttachment(remoteUrl: URL,completion: @escaping (Error?) -> Void)
+ public func upload(_ attachment: StreamAttachment<Payload>,progress: ((Double) -> Void)?,completion: @escaping (Result<UploadedFile, Error>) -> Void)
+ public func uploadAttachment(localUrl: URL,progress: ((Double) -> Void)?,completion: @escaping (Result<UploadedFile, Error>) -> Void)
+ public func deleteAttachment(remoteUrl: URL,completion: @escaping (Error?) -> Void)
- 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
- public let query: ChannelListQuery
+ public private var query: ChannelListQuery
- @available(*, deprecated, message: "Please use `markAllRead` available in `CurrentChatUserController`") public func markAllRead(completion: ((Error?) -> Void)? = nil)
+ public func prefill(group: GroupedChannelsGroup,completion: ((Error?) -> Void)? = nil)
+ @available(*, deprecated, message: "Please use `markAllRead` available in `CurrentChatUserController`") public func markAllRead(completion: ((Error?) -> Void)? = nil) |
SDK Size
|
StreamChat XCSize
Show 31 more objects
|
StreamChatUI XCSize
|
|
|
Closing the PR because we are targeting develop for this feature. |



🔗 Issue Links
Resolves https://linear.app/stream/issue/IOS-1635/support-for-grouped-channels-endpoint.
🎯 Goal
ChatChannelListController.prefill(channels:completion:)for priming controller-local channel data before the first synchronize call while preserving normal pagination, observation, and offline refresh behaviorChatClient.groupedQueryChannels(limit:watch:presence:)to fetch grouped channel groups asGroupedChannels, preserving backend group keys and exposing normalized per-group channels and unread counts for integratorsgroupedUnreadChannelsdata to grouped unread websocket events and persist it onCurrentChatUserfor integrators📝 Summary
Test on https://github.com/GetStream/GroupedChannelsSample.
🛠 Implementation
Provide a detailed description of the implementation and explain your decisions if you find them relevant.
🎨 Showcase
Add relevant screenshots and/or videos/gifs to easily see what this PR changes, if applicable.
🧪 Manual Testing Notes
Explain how this change can be tested manually, if applicable.
☑️ Contributor Checklist
docs-contentrepo