5.6.0 Release#4153
Conversation
…4131) * Fix SwiftUI Spacer resolution conflict when building with Xcode 27 The iOS 27 SDK's ViewBuilder resolves Spacer() to StreamChatUI's public func Spacer() -> UIView in files importing both StreamChatUI and SwiftUI, failing to compile. Mark the UIKit DSL function with @_disfavoredOverload so SwiftUI.Spacer stays preferred in ViewBuilder contexts while VContainer/HContainer builders keep using the UIKit one.
…matched message (#4127)
…ference levels (#4137) * Add all_mentions and direct_mentions push preference levels Introduce the new push notification preference levels and surface them in the demo app's push preferences UI. * Add changelog entry for push preference levels * Improve deprecation message
* Add role search support Add the role model, search query, endpoint, repository and a RoleSearchController plus a RoleSearch state layer for searching roles. * Add changelog entry for role search * Add EndpointPath test for role search * Add tests for RolePayloads * Add tests for RoleEndpoints * Add tests for RoleSearchController * Add Combine tests for RoleSearchController * Add tests for Role model * Add tests for RoleSearchQuery * Add tests for RolesRepository * Add tests for RoleSearch state layer * Add tests for RoleSearchState * Remove Cursor prefix from RoleSearchQuery doc comment * Make Role init internal Domain model initializers should not be public, matching the convention used by other models like ChatChannel and UserGroup. * Change RoleType from enum to RawRepresentable struct Use the static-constant struct pattern (like EventType) so new role types can be added without breaking changes. * Move role search changelog entry to the Upcoming section * Clarify Role documentation to mention app and channel scopes * Remove persistence implementation details from role search docs
* Add enhanced message mentions Support @here, @channel, role and group mentions on messages, including model, payload, persistence and decoding of mentioned groups by id and name. * Add changelog entry for enhanced message mentions * Cover GroupMentionDTO in removeAllData test * Use readSynchronously instead of viewContext in MessageDTO test * Make UserGroupMention init internal * Fix changelog wording for enhanced message mentions * Reduce cyclomatic complexity in ChatMessage equality check Split the == operator into two private helpers so each stays under the SwiftLint cyclomatic_complexity threshold.
* Add mention suggestions providers Introduce an extensible MentionSuggestionsProvider with default and enhanced implementations, mention suggestion icons, and channel-capability-driven mention types. * Add changelog entry for mention suggestions providers * Update CHANGELOG.md * Remove redundant DispatchQueue.main.async in UserGroupsConfigView * Update Sources/StreamChat/MentionSuggestions/EnhancedMentionSuggestionsProvider.swift * Update Sources/StreamChat/MentionSuggestions/EnhancedMentionSuggestionsProvider.swift * Feedback changes * Make mention suggestion providers properly Sendable * Remove mentionType and mentionText from MentionSuggestion.Kind The mentionType is redundant since the kind is already known from the type cast, and mentionText should be localized on the UI side. The now-unused MentionType struct is also removed.
* Fix typing.stop sent unconditionally on every message send Guard the stopTyping call in ChannelController.createNewMessage behind shouldSendTypingEvents, and add an early-return in TypingEventsSender when no typing session is active (currentUserLastTypingDate is nil). This prevents pointless typing.stop API requests on channels where typing events are disabled (e.g. livestream), which the server rejects with 400. * Update CHANGELOG for #4146 * Revert TypingEventsSender early-return to avoid multi-device regression The early-return guard in stopTyping when currentUserLastTypingDate is nil would skip the API call even when another device started the typing session, leaving a stale typing indicator until server timeout. The capability check in ChannelController.createNewMessage is sufficient to fix the reported issue. * Tighten test to load channel with typing events disabled The test was passing trivially because no channel was loaded, not because the capability guard worked. Now it explicitly sets up a channel with empty ownCapabilities before asserting. * Apply suggestion from @nuno-vieira
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 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
|
|
Build for regression testing №123457152 has been uploaded to TestFlight 🎁 |
StreamChat XCSize
Show 134 more objects
|
StreamChatCommonUI XCSize
|
SDK Size
|
StreamChatUI XCSize
|
Public Interface+ public struct MentionSuggestionsRequest: Sendable
+
+ public var text: String
+ public var channel: ChatChannel
+
+
+ public init(text: String,channel: ChatChannel)
+ public extension MentionSuggestionsProvider
+ public final class EnhancedMentionSuggestionsProvider: MentionSuggestionsProvider, Sendable
+
+ public let mentionAllAppUsers: Bool
+
+
+ public init(client: ChatClient,mentionAllAppUsers: Bool = false)
+
+
+ public func mentionSuggestions(for request: MentionSuggestionsRequest)async throws -> [MentionSuggestion]
+ public struct UserGroupMention: Hashable, Identifiable, Sendable
+
+ public let id: String
+ public let name: String
+ public struct UserGroupListQuery: Encodable, Sendable, Equatable
+
+ case limit
+ case idGreaterThan = "id_gt"
+ case createdAtGreaterThan = "created_at_gt"
+ case teamId = "team_id"
+
+
+ public var limit: Int?
+ public var idGreaterThan: String?
+ public var createdAtGreaterThan: Date?
+ public var teamId: String?
+
+
+ public init(limit: Int? = nil,idGreaterThan: String? = nil,createdAtGreaterThan: Date? = nil,teamId: String? = nil)
+ public protocol UserGroupListControllerDelegate: DataControllerStateDelegate
+ public class UserGroupListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable
+
+ public let query: UserGroupListQuery
+ public let client: ChatClient
+ public var userGroups: [UserGroup]
+ public private var hasLoadedAllUserGroups: Bool
+ public weak var delegate: UserGroupListControllerDelegate?
+
+
+ override public func synchronize(_ completion: (@MainActor (_ error: Error?) -> Void)? = nil)
+ public extension UserGroupListController
+ public extension UserGroupControllerDelegate
+ public extension UserGroupListControllerDelegate
+ @MainActor public final class UserGroupSearchState: ObservableObject
+
+ @Published public internal var query: UserGroupSearchQuery?
+ @Published public internal var userGroups: [UserGroup]
+ public struct UserGroupMember: Equatable, Sendable
+
+ public let groupId: String
+ public let userId: UserId
+ public let isAdmin: Bool
+ public let createdAt: Date
+ public final class DefaultMentionSuggestionsProvider: MentionSuggestionsProvider, Sendable
+
+ public let mentionAllAppUsers: Bool
+
+
+ public init(client: ChatClient,mentionAllAppUsers: Bool = false)
+
+
+ public func mentionSuggestions(for request: MentionSuggestionsRequest)async throws -> [MentionSuggestion]
+ @MainActor public final class RoleSearchState: ObservableObject
+
+ @Published public internal var query: RoleSearchQuery?
+ @Published public internal var roles: [Role]
+ public class UserGroupController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable
+
+ public let userGroupId: String
+ public let teamId: String?
+ public let client: ChatClient
+ public var userGroup: UserGroup?
+ public var delegate: UserGroupControllerDelegate?
+
+
+ override public func synchronize(_ completion: (@MainActor (_ error: Error?) -> Void)? = nil)
+ public struct UserGroupSearchQuery: Encodable, Sendable, Equatable
+
+ case query
+ case limit
+ case nameGreaterThan = "name_gt"
+ case idGreaterThan = "id_gt"
+ case teamId = "team_id"
+
+
+ public let query: String
+ public var limit: Int?
+ public var nameGreaterThan: String?
+ public var idGreaterThan: String?
+ public var teamId: String?
+
+
+ public init(query: String,limit: Int? = nil,nameGreaterThan: String? = nil,idGreaterThan: String? = nil,teamId: String? = nil)
+ extension RoleSearchController
+
+ public var statePublisher: AnyPublisher<DataController.State, Never>
+ public var rolesChangesPublisher: AnyPublisher<[Role], Never>
+ public struct UserGroup: Equatable, Identifiable, Sendable
+
+ public let id: String
+ public let name: String
+ public let description: String?
+ public let teamId: String?
+ public let members: [UserGroupMember]
+ public let createdAt: Date
+ public let updatedAt: Date
+ public let createdBy: UserId?
+ public protocol RoleSearchControllerDelegate: DataControllerStateDelegate
+ public struct RoleSearchQuery: Encodable, Sendable, Equatable
+
+ case query
+ case limit
+ case includeGlobalRoles = "include_global_roles"
+ case nameGreaterThan = "name_gt"
+ case roleType = "role_type"
+
+
+ public let query: String
+ public var limit: Int?
+ public var includeGlobalRoles: Bool?
+ public var nameGreaterThan: String?
+ public var roleType: RoleType?
+
+
+ public init(query: String,limit: Int? = nil,includeGlobalRoles: Bool? = nil,nameGreaterThan: String? = nil,roleType: RoleType? = nil)
+
+
+ public func encode(to encoder: Encoder)throws
+ public class UserGroupSearch: @unchecked Sendable
+
+ @MainActor public var state: UserGroupSearchState
+
+
+ @discardableResult public func search(text: String,teamId: String? = nil)async throws -> [UserGroup]
+ @discardableResult public func search(query: UserGroupSearchQuery)async throws -> [UserGroup]
+ public struct MentionSuggestion: Identifiable, Sendable
+
+ public static let here
+ public static let channel
+ public let kind: any Kind
+ public var id: String
+
+
+ public init(_ kind: any Kind)
+
+
+ public static func user(_ user: ChatUser)-> MentionSuggestion
+ public static func role(_ role: StreamChat.Role)-> MentionSuggestion
+ public static func group(_ group: UserGroup)-> MentionSuggestion
+
+
+ public protocol Kind: Sendable
+
+ public struct User: Kind
+
+ public let user: ChatUser
+ public var id: String
+
+
+ public init(user: ChatUser)
+
+
+ public struct Here: Kind
+
+ public var id: String
+
+
+ public init()
+
+
+ public struct Channel: Kind
+
+ public var id: String
+
+
+ public init()
+
+
+ public struct Role: Kind
+
+ public let role: StreamChat.Role
+ public var id: String
+
+
+ public init(role: StreamChat.Role)
+
+
+ public struct Group: Kind
+
+ public let group: UserGroup
+ public var id: String
+
+
+ public init(group: UserGroup)
+ public protocol UserGroupControllerDelegate: DataControllerStateDelegate
+ public struct RoleType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable
+
+ public let rawValue: String
+
+
+ public init(rawValue: String)
+ public init(stringLiteral value: String)
+ public protocol MentionSuggestionsProvider: Sendable
+ public class RoleSearch: @unchecked Sendable
+
+ @MainActor public var state: RoleSearchState
+
+
+ @discardableResult public func search(text: String,roleType: RoleType? = nil)async throws -> [Role]
+ @discardableResult public func search(query: RoleSearchQuery)async throws -> [Role]
+ public extension RoleType
+ public extension RoleSearchControllerDelegate
+ public class RoleSearchController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable
+
+ public let client: ChatClient
+ public private var roles: [Role]
+ public weak var delegate: RoleSearchControllerDelegate?
+
+
+ public func searchRoles(text: String,roleType: RoleType? = nil,completion: (@MainActor (Result<[Role], Error>) -> Void)? = nil)
+ public func searchRoles(query: RoleSearchQuery,completion: (@MainActor (Result<[Role], Error>) -> Void)? = nil)
+ public struct Role: Equatable, Identifiable, Sendable
+
+ public let name: String
+ public let isCustom: Bool
+ public let scopes: [String]
+ public let createdAt: Date?
+ public let updatedAt: Date?
+ public var id: String
+ public extension UserGroupController
public class LivestreamChat: AppStateObserverDelegate, @unchecked Sendable
- @discardableResult public func sendMessage(with text: String,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],pinning: MessagePinning? = nil,silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,restrictedVisibility: [UserId] = [],extraData: [String: RawJSON] = [:],messageId: MessageId? = nil)async throws -> MessageId
+ @discardableResult public func sendMessage(with text: String,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],mentionedHere: Bool = false,mentionedChannel: Bool = false,mentionedGroupIds: [String] = [],mentionedRoles: [String] = [],pinning: MessagePinning? = nil,silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,restrictedVisibility: [UserId] = [],extraData: [String: RawJSON] = [:],messageId: MessageId? = nil)async throws -> MessageId
public struct ChatMessage: Identifiable, Sendable
- public let threadParticipants: [ChatUser]
+ public let mentionedHere: Bool
- public var threadParticipantsCount: Int
+ public let mentionedChannel: Bool
- public var attachmentCounts: [AttachmentType: Int]
+ public let mentionedGroups: Set<UserGroupMention>
- public let latestReplies: [ChatMessage]
+ public let mentionedRoles: [String]
- public let localState: LocalMessageState?
+ public let threadParticipants: [ChatUser]
- public let isFlaggedByCurrentUser: Bool
+ public var threadParticipantsCount: Int
- public let latestReactions: Set<ChatMessageReaction>
+ public var attachmentCounts: [AttachmentType: Int]
- public let currentUserReactions: Set<ChatMessageReaction>
+ public let latestReplies: [ChatMessage]
- public var currentUserReactionsCount: Int
+ public let localState: LocalMessageState?
- public let isSentByCurrentUser: Bool
+ public let isFlaggedByCurrentUser: Bool
- public let pinDetails: MessagePinDetails?
+ public let latestReactions: Set<ChatMessageReaction>
- public let translations: [TranslationLanguage: String]?
+ public let currentUserReactions: Set<ChatMessageReaction>
- public let originalLanguage: TranslationLanguage?
+ public var currentUserReactionsCount: Int
- public let moderationDetails: MessageModerationDetails?
+ public let isSentByCurrentUser: Bool
- public let readBy: Set<ChatUser>
+ public let pinDetails: MessagePinDetails?
- public var readByCount: Int
+ public let translations: [TranslationLanguage: String]?
- public let poll: Poll?
+ public let originalLanguage: TranslationLanguage?
- public let sharedLocation: SharedLocation?
+ public let moderationDetails: MessageModerationDetails?
- public let channelRole: MemberRole?
+ public let readBy: Set<ChatUser>
-
+ public var readByCount: Int
-
+ public let poll: Poll?
- public func translatedText(for language: TranslationLanguage)-> String?
+ public let sharedLocation: SharedLocation?
- public func changing(text: String? = nil,type: MessageType? = nil,state: LocalMessageState? = nil,command: String? = nil,arguments: String? = nil,attachments: [AnyChatMessageAttachment]? = nil,translations: [TranslationLanguage: String]? = nil,originalLanguage: TranslationLanguage? = nil,moderationDetails: MessageModerationDetails? = nil,readBy: Set<ChatUser>? = nil,deletedAt: Date? = nil,extraData: [String: RawJSON]? = nil)-> ChatMessage
+ public let channelRole: MemberRole?
- public func replacing(text: String?,extraData: [String: RawJSON]?,attachments: [AnyChatMessageAttachment]?)-> ChatMessage
+
- public func replacing(text: String?,type: MessageType,state: LocalMessageState?,command: String?,arguments: String?,attachments: [AnyChatMessageAttachment]?,translations: [TranslationLanguage: String]?,originalLanguage: TranslationLanguage?,moderationDetails: MessageModerationDetails?,extraData: [String: RawJSON]?)-> ChatMessage
+
+ public func translatedText(for language: TranslationLanguage)-> String?
+ public func changing(text: String? = nil,type: MessageType? = nil,state: LocalMessageState? = nil,command: String? = nil,arguments: String? = nil,attachments: [AnyChatMessageAttachment]? = nil,translations: [TranslationLanguage: String]? = nil,originalLanguage: TranslationLanguage? = nil,moderationDetails: MessageModerationDetails? = nil,readBy: Set<ChatUser>? = nil,deletedAt: Date? = nil,extraData: [String: RawJSON]? = nil)-> ChatMessage
+ public func replacing(text: String?,extraData: [String: RawJSON]?,attachments: [AnyChatMessageAttachment]?)-> ChatMessage
+ public func replacing(text: String?,type: MessageType,state: LocalMessageState?,command: String?,arguments: String?,attachments: [AnyChatMessageAttachment]?,translations: [TranslationLanguage: String]?,originalLanguage: TranslationLanguage?,moderationDetails: MessageModerationDetails?,extraData: [String: RawJSON]?)-> ChatMessage
public class Chat: @unchecked Sendable
- @discardableResult public func sendMessage(with text: String,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],pinning: MessagePinning? = nil,extraData: [String: RawJSON] = [:],silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,messageId: MessageId? = nil,restrictedVisibility: [UserId] = [])async throws -> ChatMessage
+ @discardableResult public func sendMessage(with text: String,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],mentionedHere: Bool = false,mentionedChannel: Bool = false,mentionedGroupIds: [String] = [],mentionedRoles: [String] = [],pinning: MessagePinning? = nil,extraData: [String: RawJSON] = [:],silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,messageId: MessageId? = nil,restrictedVisibility: [UserId] = [])async throws -> ChatMessage
- @discardableResult public func reply(to parentMessageId: MessageId,text: String,showReplyInChannel: Bool = false,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],pinning: MessagePinning? = nil,extraData: [String: RawJSON] = [:],silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,messageId: MessageId? = nil)async throws -> ChatMessage
+ @discardableResult public func reply(to parentMessageId: MessageId,text: String,showReplyInChannel: Bool = false,attachments: [AnyAttachmentPayload] = [],quote quotedMessageId: MessageId? = nil,mentions: [UserId] = [],mentionedHere: Bool = false,mentionedChannel: Bool = false,mentionedGroupIds: [String] = [],mentionedRoles: [String] = [],pinning: MessagePinning? = nil,extraData: [String: RawJSON] = [:],silent: Bool = false,skipPushNotification: Bool = false,skipEnrichURL: Bool = false,messageId: MessageId? = nil)async throws -> ChatMessage
extension ChatClient
- public func makeUserSearch()-> UserSearch
+ public func makeUserGroupSearch()-> UserGroupSearch
public class ChatMessageController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable
- public func createNewReply(messageId: MessageId? = nil,text: String,pinning: MessagePinning? = nil,attachments: [AnyAttachmentPayload] = [],mentionedUserIds: [UserId] = [],showReplyInChannel: Bool = false,isSilent: Bool = false,quotedMessageId: MessageId? = nil,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: (@MainActor (Result<MessageId, Error>) -> Void)? = nil)
+ public func createNewReply(messageId: MessageId? = nil,text: String,pinning: MessagePinning? = nil,attachments: [AnyAttachmentPayload] = [],mentionedUserIds: [UserId] = [],mentionedHere: Bool = false,mentionedChannel: Bool = false,mentionedGroupIds: [String] = [],mentionedRoles: [String] = [],showReplyInChannel: Bool = false,isSilent: Bool = false,quotedMessageId: MessageId? = nil,skipPush: Bool = false,skipEnrichUrl: Bool = false,extraData: [String: RawJSON] = [:],completion: (@MainActor (Result<MessageId, Error>) -> Void)? = nil)
public struct PushPreferenceLevel: RawRepresentable, Equatable, ExpressibleByStringLiteral, Sendable
- public static let mentions: PushPreferenceLevel
+ @available(*, deprecated, renamed: "directMentions") public static let mentions: PushPreferenceLevel
- public static let all: PushPreferenceLevel
+ public static let allMentions: PushPreferenceLevel
-
+ public static let directMentions: PushPreferenceLevel
-
+ public static let all: PushPreferenceLevel
- public init(rawValue: String)
+
- public init(stringLiteral value: StringLiteralType)
+
+ public init(rawValue: String)
+ public init(stringLiteral value: StringLiteralType)
public class LivestreamChannelController: AppStateObserverDelegate, @unchecked Sendable
- public func createNewMessage(messageId: MessageId? = nil,text: String,pinning: MessagePinning? = nil,isSilent: Bool = false,attachments: [AnyAttachmentPayload] = [],mentionedUserIds: [UserId] = [],quotedMessageId: MessageId? = nil,skipPush: Bool = false,skipEnrichUrl: Bool = false,restrictedVisibility: [UserId] = [],location: NewLocationInfo? = nil,extraData: [String: RawJSON] = [:],completion: (@MainActor (Result<MessageId, Error>) -> Void)? = nil)
+ public func createNewMessage(messageId: MessageId? = nil,text: String,pinning: MessagePinning? = nil,isSilent: Bool = false,attachments: [AnyAttachmentPayload] = [],mentionedUserIds: [UserId] = [],mentionedHere: Bool = false,mentionedChannel: Bool = false,mentionedGroupIds: [String] = [],mentionedRoles: [String] = [],quotedMessageId: MessageId? = nil,skipPush: Bool = false,skipEnrichUrl: Bool = false,restrictedVisibility: [UserId] = [],location: NewLocationInfo? = nil,extraData: [String: RawJSON] = [:],completion: (@MainActor (Result<MessageId, Error>) -> Void)? = nil)
public struct ChannelCapability: RawRepresentable, ExpressibleByStringLiteral, Hashable, Sendable
-
+ public static let notifyChannel: Self
-
+ public static let notifyGroup: Self
- public init(rawValue: String)
+ public static let notifyHere: Self
- public init(stringLiteral value: String)
+ public static let notifyRole: Self
+
+
+ public init(rawValue: String)
+ public init(stringLiteral value: String)
public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable
- public func createNewMessage(messageId: MessageId? = nil,text: String,pinning: MessagePinning? = nil,isSilent: Bool = false,attachments: [AnyAttachmentPayload] = [],mentionedUserIds: [UserId] = [],quotedMessageId: MessageId? = nil,skipPush: Bool = false,skipEnrichUrl: Bool = false,restrictedVisibility: [UserId] = [],location: NewLocationInfo? = nil,extraData: [String: RawJSON] = [:],completion: (@MainActor (Result<MessageId, Error>) -> Void)? = nil)
+ public func createNewMessage(messageId: MessageId? = nil,text: String,pinning: MessagePinning? = nil,isSilent: Bool = false,attachments: [AnyAttachmentPayload] = [],mentionedUserIds: [UserId] = [],mentionedHere: Bool = false,mentionedChannel: Bool = false,mentionedGroupIds: [String] = [],mentionedRoles: [String] = [],quotedMessageId: MessageId? = nil,skipPush: Bool = false,skipEnrichUrl: Bool = false,restrictedVisibility: [UserId] = [],location: NewLocationInfo? = nil,extraData: [String: RawJSON] = [:],completion: (@MainActor (Result<MessageId, Error>) -> Void)? = nil) |
|
Build for regression testing №123457153 has been uploaded to TestFlight 🎁 |
|
|
/merge release |
|
Publication of the release has been launched 👍 |



StreamChat
✅ Added
all_mentionsanddirect_mentionspush notification preference levels #4137@here,@channel, role and group mentions in messages #4140🐞 Fixed
typing.stopbeing sent unconditionally on every message send #4146StreamChatUI
🐞 Fixed
Spacer()conflicting with SwiftUI'sSpacerwhen building with Xcode 27 #4131