Skip to content

Conversation

@Stream-SDK-Bot
Copy link
Collaborator

StreamChat

✅ Added

  • Add ChatClient.uploadAttachment(localUrl:progress:) #3883
  • Add ChatClient.deleteAttachment(remoteUrl:) #3883
  • Add CDNClient.deleteAttachment(remoteUrl:) #3883
  • Add heic, heif and svg formats to the supported image file types #3883
  • Add ChatChannelController.markUnread(from:completion:) and Chat.markUnread(from:) where the from argument is Date #3885
  • Add support for filter tags in channels #3886
    • Add ChatChannel.filterTags
    • Add filterTags channel list filtering key
    • Add filterTags argument to ChatChannelController and Chat factory methods in ChatClient
    • Add filterTags argument to ChatChannelController.updateChannel and ChatChannelController.partialUpdateChannel
    • Add filterTags argument to Chat.update and Chat.updatePartial

🐞 Fixed

  • Fix rare crash in WebSocketClient.connectEndpoint #3882
  • Fix audio recordings not playing from AirPods automatically #3884
  • Fix audio recordings not using AirPods mic automatically #3884

StreamChatUI

🐞 Fixed

  • Fix marking channel read when scrolling to the bottom without unread counts #3881

Stream Bot and others added 9 commits November 18, 2025 15:48
#3881)

* Fix marking channel read when scrolling to bottom without unread count

* Update CHANGELOG.md
…#3883)

* Add `ChatClient.deleteAttachment(remoteUrl:attachmentType:)`

* Add delete action of the user avatar in the demo app

* Update the demo app user profile avatar update with the simpler interface

* Add test coverage

* Add documentation to the new interfaces

* Remove attachmentType from delete endpoint

* Fix hard coded attachment type

* Add tests for the delete endpoint

* Update CHANGELOG.md

* Add `heic`, `heif` and `svg` image types to supported file types

* Add support for heic, heif, and svg image formats

Added support for additional image file types in the ChatClient.
* Fix audio recordings not playing from AirPods automatically

* Update CHANGELOG.md

* Fix not compiling for MacOS

* Fix doc

* Use deprecated `.allowBluetooth` so that it compiles in Xcode 15

* Add comment to explain why using deprecated case

* Fix audio recording not using AirPods automatically

* Update CHANGELOG.md

* Add missing deprecated comment
@Stream-SDK-Bot Stream-SDK-Bot requested a review from a team as a code owner December 2, 2025 12:18
@coderabbitai
Copy link

coderabbitai bot commented Dec 2, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch release/4.94.0

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.

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

Public Interface

 public enum ChannelCodingKeys: String, CodingKey, CaseIterable  
-   case team
+   case filterTags = "filter_tags"
-   case memberCount = "member_count"
+   case team
-   case messageCount = "message_count"
+   case memberCount = "member_count"
-   case cooldownDuration = "cooldown"
+   case messageCount = "message_count"
-   case blocked
+   case cooldownDuration = "cooldown"
+   case blocked

 public class Chat  
-   @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
+   public func markUnread(from timestamp: Date)async throws 
-   @discardableResult public func loadReplies(for messageId: MessageId,pagination: MessagesPagination)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
-   public func loadReplies(before replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   @discardableResult public func loadReplies(for messageId: MessageId,pagination: MessagesPagination)async throws -> [ChatMessage]
-   public func loadReplies(after replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadReplies(before replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func loadReplies(around replyId: MessageId,for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadReplies(after replyId: MessageId?,for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func loadOlderReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadReplies(around replyId: MessageId,for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func loadNewerReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
+   public func loadOlderReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   @discardableResult public func translateMessage(_ messageId: MessageId,to language: TranslationLanguage)async throws -> ChatMessage
+   public func loadNewerReplies(for parentMessageId: MessageId,limit: Int? = nil)async throws 
-   public func mute(expiration: Int? = nil)async throws 
+   @discardableResult public func translateMessage(_ messageId: MessageId,to language: TranslationLanguage)async throws -> ChatMessage
-   public func unmute()async throws 
+   public func mute(expiration: Int? = nil)async throws 
-   public func hide(clearHistory: Bool = false)async throws 
+   public func unmute()async throws 
-   public func show()async throws 
+   public func hide(clearHistory: Bool = false)async throws 
-   public func pin(scope: ChannelPinningScope = .me)async throws 
+   public func show()async throws 
-   public func unpin(scope: ChannelPinningScope = .me)async throws 
+   public func pin(scope: ChannelPinningScope = .me)async throws 
-   public func subscribe(toEvent event: E.Type,handler: @escaping (E) -> Void)-> AnyCancellable
+   public func unpin(scope: ChannelPinningScope = .me)async throws 
-   public func subscribe(_ handler: @escaping (Event) -> Void)-> AnyCancellable
+   public func subscribe(toEvent event: E.Type,handler: @escaping (E) -> Void)-> AnyCancellable
-   public func sendEvent(_ payload: EventPayload)async throws 
+   public func subscribe(_ handler: @escaping (Event) -> Void)-> AnyCancellable
-   public func enableSlowMode(cooldownDuration: Int)async throws 
+   public func sendEvent(_ payload: EventPayload)async throws 
-   public func disableSlowMode()async throws 
+   public func enableSlowMode(cooldownDuration: Int)async throws 
-   public func truncate(systemMessage: String? = nil,hardDelete: Bool = true,skipPush: Bool = false)async throws 
+   public func disableSlowMode()async throws 
-   public func keystroke(parentMessageId: MessageId? = nil)async throws 
+   public func truncate(systemMessage: String? = nil,hardDelete: Bool = true,skipPush: Bool = false)async throws 
-   public func stopTyping(parentMessageId: MessageId? = nil)async throws 
+   public func keystroke(parentMessageId: MessageId? = nil)async throws 
-   public func update(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],extraData: [String: RawJSON] = [:])async throws 
+   public func stopTyping(parentMessageId: MessageId? = nil)async throws 
-   public func updatePartial(name: String? = nil,imageURL: URL? = nil,team: String? = nil,members: [UserId] = [],invites: [UserId] = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [])async throws 
+   public func update(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:])async throws 
-   public func deleteFile(at url: URL)async throws 
+   public func updatePartial(name: String? = nil,imageURL: URL? = nil,team: String? = nil,members: [UserId] = [],invites: [UserId] = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [])async throws 
-   public func deleteImage(at url: URL)async throws 
+   public func deleteFile(at url: URL)async throws 
-   public func uploadAttachment(with localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil)async throws -> UploadedAttachment
+   public func deleteImage(at url: URL)async throws 
-   @discardableResult public func loadWatchers(with pagination: Pagination)async throws -> [ChatUser]
+   public func uploadAttachment(with localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil)async throws -> UploadedAttachment
-   @discardableResult public func loadMoreWatchers(limit: Int? = nil)async throws -> [ChatUser]
+   @discardableResult public func loadWatchers(with pagination: Pagination)async throws -> [ChatUser]
+   @discardableResult public func loadMoreWatchers(limit: Int? = nil)async throws -> [ChatUser]

 public struct ChatChannel  
-   public let ownCapabilities: Set<ChannelCapability>
+   public let filterTags: Set<String>
-   public let isFrozen: Bool
+   public let ownCapabilities: Set<ChannelCapability>
-   public let isDisabled: Bool
+   public let isFrozen: Bool
-   public let isBlocked: Bool
+   public let isDisabled: Bool
-   public let memberCount: Int
+   public let isBlocked: Bool
-   public let messageCount: Int?
+   public let memberCount: Int
-   public let lastActiveMembers: [ChatChannelMember]
+   public let messageCount: Int?
-   public let currentlyTypingUsers: Set<ChatUser>
+   public let lastActiveMembers: [ChatChannelMember]
-   public internal var membership: ChatChannelMember?
+   public let currentlyTypingUsers: Set<ChatUser>
-   public var isArchived: Bool
+   public internal var membership: ChatChannelMember?
-   public var isPinned: Bool
+   public var isArchived: Bool
-   public let lastActiveWatchers: [ChatUser]
+   public var isPinned: Bool
-   public let watcherCount: Int
+   public let lastActiveWatchers: [ChatUser]
-   public let team: TeamId?
+   public let watcherCount: Int
-   public let unreadCount: ChannelUnreadCount
+   public let team: TeamId?
-   public let latestMessages: [ChatMessage]
+   public let unreadCount: ChannelUnreadCount
-   public let lastMessageFromCurrentUser: ChatMessage?
+   public let latestMessages: [ChatMessage]
-   public let pinnedMessages: [ChatMessage]
+   public let lastMessageFromCurrentUser: ChatMessage?
-   public let pendingMessages: [ChatMessage]
+   public let pinnedMessages: [ChatMessage]
-   public let reads: [ChatChannelRead]
+   public let pendingMessages: [ChatMessage]
-   public let muteDetails: MuteDetails?
+   public let reads: [ChatChannelRead]
-   public var isMuted: Bool
+   public let muteDetails: MuteDetails?
-   public let cooldownDuration: Int
+   public var isMuted: Bool
-   public let extraData: [String: RawJSON]
+   public let cooldownDuration: Int
-   public let previewMessage: ChatMessage?
+   public let extraData: [String: RawJSON]
-   public let draftMessage: DraftMessage?
+   public let previewMessage: ChatMessage?
-   public let activeLiveLocations: [SharedLocation]
+   public let draftMessage: DraftMessage?
-   public let pushPreference: PushPreference?
+   public let activeLiveLocations: [SharedLocation]
-   
+   public let pushPreference: PushPreference?
- 
+   
-   public func replacing(name: String?,imageURL: URL?,extraData: [String: RawJSON]?)-> ChatChannel
+ 
-   public func changing(name: String? = nil,imageURL: URL? = nil,lastMessageAt: Date? = nil,createdAt: Date? = nil,deletedAt: Date? = nil,updatedAt: Date? = nil,truncatedAt: Date? = nil,isHidden: Bool? = nil,createdBy: ChatUser? = nil,config: ChannelConfig? = nil,ownCapabilities: Set<ChannelCapability>? = nil,isFrozen: Bool? = nil,isDisabled: Bool? = nil,isBlocked: Bool? = nil,reads: [ChatChannelRead]? = nil,members: [ChatChannelMember]? = nil,membership: ChatChannelMember? = nil,memberCount: Int? = nil,watchers: [ChatUser]? = nil,watcherCount: Int? = nil,team: TeamId? = nil,cooldownDuration: Int? = nil,pinnedMessages: [ChatMessage]? = nil,pushPreference: PushPreference? = nil,extraData: [String: RawJSON]? = nil)-> ChatChannel
+   public func replacing(name: String?,imageURL: URL?,extraData: [String: RawJSON]?)-> ChatChannel
+   public func changing(name: String? = nil,imageURL: URL? = nil,lastMessageAt: Date? = nil,createdAt: Date? = nil,deletedAt: Date? = nil,updatedAt: Date? = nil,truncatedAt: Date? = nil,isHidden: Bool? = nil,createdBy: ChatUser? = nil,config: ChannelConfig? = nil,filterTags: Set<String>? = nil,ownCapabilities: Set<ChannelCapability>? = nil,isFrozen: Bool? = nil,isDisabled: Bool? = nil,isBlocked: Bool? = nil,reads: [ChatChannelRead]? = nil,members: [ChatChannelMember]? = nil,membership: ChatChannelMember? = nil,memberCount: Int? = nil,watchers: [ChatUser]? = nil,watcherCount: Int? = nil,team: TeamId? = nil,cooldownDuration: Int? = nil,pinnedMessages: [ChatMessage]? = nil,pushPreference: PushPreference? = nil,extraData: [String: RawJSON]? = nil)-> ChatChannel

 public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider  
-   public func updateChannel(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],extraData: [String: RawJSON] = [:],completion: ((Error?) -> Void)? = nil)
+   public func updateChannel(name: String?,imageURL: URL?,team: String?,members: Set<UserId> = [],invites: Set<UserId> = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:],completion: ((Error?) -> Void)? = nil)
-   public func partialChannelUpdate(name: String? = nil,imageURL: URL? = nil,team: String? = nil,members: Set<UserId> = [],invites: Set<UserId> = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [],completion: ((Error?) -> Void)? = nil)
+   public func partialChannelUpdate(name: String? = nil,imageURL: URL? = nil,team: String? = nil,members: Set<UserId> = [],invites: Set<UserId> = [],filterTags: Set<String> = [],extraData: [String: RawJSON] = [:],unsetProperties: [String] = [],completion: ((Error?) -> Void)? = nil)
-   public func loadChannelReads(pagination: Pagination? = nil,completion: @escaping (Error?) -> Void)
+   public func markUnread(from timestamp: Date,completion: ((Result<ChatChannel, Error>) -> Void)? = nil)
-   public func loadMoreChannelReads(limit: Int? = nil,completion: @escaping (Error?) -> Void)
+   public func loadChannelReads(pagination: Pagination? = nil,completion: @escaping (Error?) -> Void)
-   public func enableSlowMode(cooldownDuration: Int,completion: ((Error?) -> Void)? = nil)
+   public func loadMoreChannelReads(limit: Int? = nil,completion: @escaping (Error?) -> Void)
-   public func disableSlowMode(completion: ((Error?) -> Void)? = nil)
+   public func enableSlowMode(cooldownDuration: Int,completion: ((Error?) -> Void)? = nil)
-   public func startWatching(isInRecoveryMode: Bool,completion: ((Error?) -> Void)? = nil)
+   public func disableSlowMode(completion: ((Error?) -> Void)? = nil)
-   public func stopWatching(completion: ((Error?) -> Void)? = nil)
+   public func startWatching(isInRecoveryMode: Bool,completion: ((Error?) -> Void)? = nil)
-   public func freezeChannel(completion: ((Error?) -> Void)? = nil)
+   public func stopWatching(completion: ((Error?) -> Void)? = nil)
-   public func unfreezeChannel(completion: ((Error?) -> Void)? = nil)
+   public func freezeChannel(completion: ((Error?) -> Void)? = nil)
-   public func pin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
+   public func unfreezeChannel(completion: ((Error?) -> Void)? = nil)
-   public func unpin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
+   public func pin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
-   public func uploadAttachment(localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil,completion: @escaping ((Result<UploadedAttachment, Error>) -> Void))
+   public func unpin(scope: ChannelPinningScope = .me,completion: ((Error?) -> Void)? = nil)
-   public func enrichUrl(_ url: URL,completion: @escaping (Result<LinkAttachmentPayload, Error>) -> Void)
+   public func uploadAttachment(localFileURL: URL,type: AttachmentType,progress: ((Double) -> Void)? = nil,completion: @escaping ((Result<UploadedAttachment, Error>) -> Void))
-   public func loadPinnedMessages(pageSize: Int = .messagesPageSize,sorting: [Sorting<PinnedMessagesSortingKey>] = [],pagination: PinnedMessagesPagination? = nil,completion: @escaping (Result<[ChatMessage], Error>) -> Void)
+   public func enrichUrl(_ url: URL,completion: @escaping (Result<LinkAttachmentPayload, Error>) -> Void)
-   public func currentCooldownTime()-> Int
+   public func loadPinnedMessages(pageSize: Int = .messagesPageSize,sorting: [Sorting<PinnedMessagesSortingKey>] = [],pagination: PinnedMessagesPagination? = nil,completion: @escaping (Result<[ChatMessage], Error>) -> Void)
-   public func deleteFile(url: String,completion: ((Error?) -> Void)? = nil)
+   public func currentCooldownTime()-> Int
-   public func deleteImage(url: String,completion: ((Error?) -> Void)? = nil)
+   public func deleteFile(url: String,completion: ((Error?) -> Void)? = nil)
-   public func getFirstUnreadMessageId(for channel: ChatChannel)-> MessageId?
+   public func deleteImage(url: String,completion: ((Error?) -> Void)? = nil)
-   public func setPushPreference(level: PushPreferenceLevel,completion: ((Result<PushPreference, Error>) -> Void)? = nil)
+   public func getFirstUnreadMessageId(for channel: ChatChannel)-> MessageId?
-   public func snoozePushNotifications(until date: Date,completion: ((Result<PushPreference, Error>) -> Void)? = nil)
+   public func setPushPreference(level: PushPreferenceLevel,completion: ((Result<PushPreference, Error>) -> Void)? = nil)
+   public func snoozePushNotifications(until date: Date,completion: ((Result<PushPreference, Error>) -> Void)? = nil)

 public enum AttachmentFileType: String, Codable, Equatable, CaseIterable  
-   case jpeg, png, gif, bmp, webp
+   case jpeg, png, gif, bmp, webp, heic, heif, svg
-   public var isUnknown: Bool
+   public var isImage: Bool
-   
+   public var isUnknown: Bool
- 
+   
-   public init(mimeType: String)
+ 
-   public init(ext: String)
+   public init(mimeType: String)
+   public init(ext: String)

 public class ChatClient  
+   public func uploadAttachment(localUrl: URL,progress: ((Double) -> Void)?,completion: @escaping (Result<UploadedFile, Error>) -> Void)
+   public func deleteAttachment(remoteUrl: URL,completion: @escaping (Error?) -> Void)

@Stream-SDK-Bot
Copy link
Collaborator Author

SDK Size

title previous release current release diff status
StreamChat 7.25 MB 7.28 MB +34 KB 🟢
StreamChatUI 4.89 MB 4.89 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Collaborator Author

StreamChat XCSize

Object Diff (bytes)
MarkUnreadPayload.o +7822
ChannelController.o +5274
Chat.o +2636
MessageDTO.o +2300
AudioSessionConfiguring.o -2070
Show 35 more objects
Object Diff (bytes)
CDNClient.o +1684
RequestEncoder.o +1492
ChatClient.o +1174
ReadStateHandler.o +1160
ChannelReadDTO.o +964
AudioSessionProtocol.o -784
ChannelRepository.o +776
ChannelListPayload.o +620
ReminderPayloads.o +408
ChannelListQuery.o +360
MessageRepository.o +338
WebSocketClient.o +331
Channel.o +304
ChatMessage.o +264
ChannelEditDetailPayload.o +263
ThreadListPayload.o +244
ThreadEndpoints.o +244
MutedChannelPayload.o +240
Thread.o +232
ChannelReadUpdaterMiddleware.o +212
ChatClient+Environment.o +210
AttachmentTypes.o -190
ChannelDTO.o +177
ChannelQuery.o +144
ConnectionRepository.o -144
APIClient.o +102
ChatClient+Factory.o +96
MessageReminder.o +84
ChannelUpdater.o +84
LivestreamChannelController.o +64
ChannelListLinker.o +60
ChatClient+ChannelController.o +56
AttachmentUploader.o -46
ChannelCodingKeys.o +44
ChannelPayload+asModel.o +44

@Stream-SDK-Bot
Copy link
Collaborator Author

StreamChatUI XCSize

Object Diff (bytes)
ChatChannelVC.o +1396
Appearance+Images.o -692
QuotedChatMessageView.o -628
ContainerStackView.o +260
InputChatMessageView.o +252
ChatMessageDeliveryStatusView.o +224
ChatThreadListItemView.o +68
ChatMessageHeaderDecoratorView.o +48

@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 2, 2025

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

Build for regression testing №123457109 has been uploaded to TestFlight 🎁

@Stream-SDK-Bot
Copy link
Collaborator Author

SDK Performance

target metric benchmark branch performance status
MessageList Hitches total duration 10 ms 5.01 ms 49.9% 🔼 🟢
Duration 2.6 s 2.52 s 3.08% 🔼 🟢
Hitch time ratio 4 ms per s 1.99 ms per s 50.25% 🔼 🟢
Frame rate 75 fps 78.82 fps 5.09% 🔼 🟢
Number of hitches 1 0.6 40.0% 🔼 🟢

@testableapple
Copy link
Contributor

/merge release

@testableapple
Copy link
Contributor

Publication of the release has been launched 👍

@github-actions github-actions bot merged commit e320a85 into main Dec 2, 2025
16 checks passed
@github-actions github-actions bot deleted the release/4.94.0 branch December 2, 2025 14:11
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.

5 participants