diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3426ba2a5f..57b28e716ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🔄 Changed
+# [4.87.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.87.0)
+_August 29, 2025_
+
+## StreamChat
+### ✅ Added
+- Add support for `user.messages.deleted` event [#3792](https://github.com/GetStream/stream-chat-swift/pull/3792)
+- Add upload endpoint for standalone attachments [#3788](https://github.com/GetStream/stream-chat-swift/pull/3788)
+- Add option to access the total message count of a channel [#3796](https://github.com/GetStream/stream-chat-swift/pull/3796)
+### 🐞 Fixed
+- Fix channel getting removed from channel list which includes blocked channels [#3794](https://github.com/GetStream/stream-chat-swift/pull/3794)
+- Fix system messages not incrementing and decrementing unread counts [#3795](https://github.com/GetStream/stream-chat-swift/pull/3795)
+
+## StreamChatUI
+### 🐞 Fixed
+- Fix input text view's placeholder alignment in RTL [#3790](https://github.com/GetStream/stream-chat-swift/pull/3790)
+- Flip directional icons in RTL [#3790](https://github.com/GetStream/stream-chat-swift/pull/3790)
+- Fix swipe to reply gesture in RTL [#3790](https://github.com/GetStream/stream-chat-swift/pull/3790)
+### 🔄 Changed
+- Use chevron icons in `ChatMessageAttachmentPreviewVC` [#3790](https://github.com/GetStream/stream-chat-swift/pull/3790)
+
# [4.86.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.86.0)
_August 21, 2025_
diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift b/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
index 9528dfdbbfd..230c27bf41d 100644
--- a/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
+++ b/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
@@ -48,6 +48,16 @@ final class DemoChatChannelListVC: ChatChannelListVC {
.equal(.blocked, to: true)
])
]))
+
+ lazy var blockedUnblockedWithHiddenChannelsQuery: ChannelListQuery = .init(
+ filter: .and([
+ .containMembers(userIds: [currentUserId]),
+ .or([
+ .and([.equal(.blocked, to: false), .equal(.hidden, to: false)]),
+ .and([.equal(.blocked, to: true), .equal(.hidden, to: true)])
+ ])
+ ])
+ )
lazy var unreadCountChannelsQuery: ChannelListQuery = .init(
filter: .and([
@@ -161,6 +171,15 @@ final class DemoChatChannelListVC: ChatChannelListVC {
self?.setAllBlockedChannelsQuery()
}
)
+
+ let blockedUnBlockedExcludingDeletedChannelsAction = UIAlertAction(
+ title: "Blocked Unblocked with Matching Hidden Channels",
+ style: .default,
+ handler: { [weak self] _ in
+ self?.title = "Blocked Unblocked with Matching Hidden Channels"
+ self?.setBlockedUnblockedWithHiddenChannelsQuery()
+ }
+ )
let unreadCountChannelsAction = UIAlertAction(
title: "Unread Count Channels",
@@ -230,6 +249,7 @@ final class DemoChatChannelListVC: ChatChannelListVC {
hasUnreadChannelsAction,
hiddenChannelsAction,
allBlockedChannelsAction,
+ blockedUnBlockedExcludingDeletedChannelsAction,
mutedChannelsAction,
coolChannelsAction,
pinnedChannelsAction,
@@ -248,6 +268,10 @@ final class DemoChatChannelListVC: ChatChannelListVC {
func setAllBlockedChannelsQuery() {
replaceQuery(allBlockedChannelsQuery)
}
+
+ func setBlockedUnblockedWithHiddenChannelsQuery() {
+ replaceQuery(blockedUnblockedWithHiddenChannelsQuery)
+ }
func setUnreadCountChannelsQuery() {
replaceQuery(unreadCountChannelsQuery)
diff --git a/README.md b/README.md
index 64b1851b102..0e9dac39836 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-
+
diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift b/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
index 33ae7ef0832..d8e152d4b46 100644
--- a/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
+++ b/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
@@ -16,6 +16,17 @@ public protocol AttachmentUploader {
progress: ((Double) -> Void)?,
completion: @escaping (Result) -> Void
)
+
+ /// Uploads a standalone attachment (not tied to message or channel), and returns the attachment with the remote information.
+ /// - Parameters:
+ /// - attachment: A standalone attachment.
+ /// - progress: The progress of the upload.
+ /// - completion: The callback with the uploaded attachment.
+ func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ )
}
public class StreamAttachmentUploader: AttachmentUploader {
@@ -41,4 +52,16 @@ public class StreamAttachmentUploader: AttachmentUploader {
})
}
}
+
+ public func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ ) {
+ cdnClient.uploadStandaloneAttachment(
+ attachment,
+ progress: progress,
+ completion: completion
+ )
+ }
}
diff --git a/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift b/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
index facef3c0f28..3c84ccbf124 100644
--- a/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
+++ b/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
@@ -40,6 +40,17 @@ public protocol CDNClient {
progress: ((Double) -> Void)?,
completion: @escaping (Result) -> Void
)
+
+ /// Uploads standalone attachment as a multipart/form-data and returns the uploaded remote file and its thumbnail.
+ /// - Parameters:
+ /// - attachment: An attachment to upload.
+ /// - progress: A closure that broadcasts upload progress.
+ /// - completion: Returns the uploaded file's information.
+ func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ )
}
public extension CDNClient {
@@ -104,13 +115,52 @@ class StreamCDNClient: CDNClient {
let fileData = try? Data(contentsOf: uploadingState.localFileURL) else {
return completion(.failure(ClientError.AttachmentUploading(id: attachment.id)))
}
+ let endpoint = Endpoint.uploadAttachment(with: attachment.id.cid, type: attachment.type)
+
+ uploadAttachment(
+ endpoint: endpoint,
+ fileData: fileData,
+ uploadingState: uploadingState,
+ progress: progress,
+ completion: completion
+ )
+ }
+
+ func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)? = nil,
+ completion: @escaping (Result) -> Void
+ ) {
+ guard
+ let uploadingState = attachment.uploadingState,
+ let fileData = try? Data(contentsOf: uploadingState.localFileURL) else {
+ return completion(.failure(ClientError.Unknown()))
+ }
+
+ let endpoint = Endpoint.uploadAttachment(type: attachment.type)
+
+ uploadAttachment(
+ endpoint: endpoint,
+ fileData: fileData,
+ uploadingState: uploadingState,
+ progress: progress,
+ completion: completion
+ )
+ }
+
+ private func uploadAttachment(
+ endpoint: Endpoint,
+ fileData: Data,
+ uploadingState: AttachmentUploadingState,
+ progress: ((Double) -> Void)? = nil,
+ completion: @escaping (Result) -> Void
+ ) {
// Encode locally stored attachment into multipart form data
let multipartFormData = MultipartFormData(
fileData,
fileName: uploadingState.localFileURL.lastPathComponent,
mimeType: uploadingState.file.type.mimeType
)
- let endpoint = Endpoint.uploadAttachment(with: attachment.id.cid, type: attachment.type)
encoder.encodeRequest(for: endpoint) { [weak self] (requestResult) in
var urlRequest: URLRequest
diff --git a/Sources/StreamChat/APIClient/Endpoints/AttachmentEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/AttachmentEndpoints.swift
index 383d446c07f..ff770298acb 100644
--- a/Sources/StreamChat/APIClient/Endpoints/AttachmentEndpoints.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/AttachmentEndpoints.swift
@@ -7,7 +7,17 @@ import Foundation
extension Endpoint {
static func uploadAttachment(with cid: ChannelId, type: AttachmentType) -> Endpoint {
.init(
- path: .uploadAttachment(channelId: cid.apiPath, type: type == .image ? "image" : "file"),
+ path: .uploadChannelAttachment(channelId: cid.apiPath, type: type == .image ? "image" : "file"),
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: nil
+ )
+ }
+
+ static func uploadAttachment(type: AttachmentType) -> Endpoint {
+ .init(
+ path: .uploadAttachment(type == .image ? "image" : "file"),
method: .post,
queryItems: nil,
requiresConnectionId: false,
diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
index fc60e82be57..6751427b209 100644
--- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
@@ -11,11 +11,11 @@ extension EndpointPath {
return true
case .createChannel, .connect, .sync, .users, .guest, .members, .partialMemberUpdate, .search, .devices, .channels, .updateChannel,
.deleteChannel, .channelUpdate, .muteChannel, .showChannel, .truncateChannel, .markChannelRead, .markChannelUnread,
- .markAllChannelsRead, .channelEvent, .stopWatchingChannel, .pinnedMessages, .uploadAttachment, .message,
+ .markAllChannelsRead, .channelEvent, .stopWatchingChannel, .pinnedMessages, .uploadChannelAttachment, .message,
.replies, .reactions, .messageAction, .banMember, .flagUser, .flagMessage, .muteUser, .translateMessage,
.callToken, .createCall, .deleteFile, .deleteImage, .og, .appSettings, .threads, .thread, .markThreadRead, .markThreadUnread,
.polls, .pollsQuery, .poll, .pollOption, .pollOptions, .pollVotes, .pollVoteInMessage, .pollVote,
- .unread, .blockUser, .unblockUser, .drafts, .reminders, .reminder, .liveLocations:
+ .unread, .blockUser, .unblockUser, .drafts, .reminders, .reminder, .liveLocations, .uploadAttachment:
return false
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
index 708790dda51..c607d55237d 100644
--- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
@@ -36,7 +36,8 @@ enum EndpointPath: Codable {
case channelEvent(String)
case stopWatchingChannel(String)
case pinnedMessages(String)
- case uploadAttachment(channelId: String, type: String)
+ case uploadChannelAttachment(channelId: String, type: String)
+ case uploadAttachment(String)
case sendMessage(ChannelId)
case message(MessageId)
@@ -125,7 +126,8 @@ enum EndpointPath: Codable {
case let .channelEvent(channelId): return "channels/\(channelId)/event"
case let .stopWatchingChannel(channelId): return "channels/\(channelId)/stop-watching"
case let .pinnedMessages(channelId): return "channels/\(channelId)/pinned_messages"
- case let .uploadAttachment(channelId, type): return "channels/\(channelId)/\(type)"
+ case let .uploadChannelAttachment(channelId, type): return "channels/\(channelId)/\(type)"
+ case let .uploadAttachment(type): return "uploads/\(type)"
case let .sendMessage(channelId): return "channels/\(channelId.apiPath)/message"
case let .message(messageId): return "messages/\(messageId)"
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift
index 8a05eee0ee1..55d57d980a4 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift
@@ -45,6 +45,7 @@ public enum ChannelCodingKeys: String, CodingKey, CaseIterable {
/// The team the channel belongs to.
case team
case memberCount = "member_count"
+ case messageCount = "message_count"
/// Cooldown duration for the channel, if it's in slow mode.
/// This value will be 0 if the channel is not in slow mode.
case cooldownDuration = "cooldown"
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
index e9cc7d91614..77994fd3d6e 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
@@ -142,6 +142,8 @@ struct ChannelDetailPayload {
let members: [MemberPayload]?
let memberCount: Int
+
+ let messageCount: Int?
/// A list of users to invite in the channel.
let invitedMembers: [MemberPayload] = [] // TODO?
@@ -192,6 +194,7 @@ extension ChannelDetailPayload: Decodable {
isHidden: try container.decodeIfPresent(Bool.self, forKey: .hidden),
members: try container.decodeArrayIfPresentIgnoringFailures([MemberPayload].self, forKey: .members),
memberCount: try container.decodeIfPresent(Int.self, forKey: .memberCount) ?? 0,
+ messageCount: try container.decodeIfPresent(Int.self, forKey: .messageCount),
team: try container.decodeIfPresent(String.self, forKey: .team),
cooldownDuration: try container.decodeIfPresent(Int.self, forKey: .cooldownDuration) ?? 0
)
diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift
index d85da2ab1e4..00fd91e1e56 100644
--- a/Sources/StreamChat/ChatClient.swift
+++ b/Sources/StreamChat/ChatClient.swift
@@ -623,6 +623,26 @@ public class ChatClient {
loadAppSettings { continuation.resume(with: $0) }
}
}
+
+ // MARK: - Upload attachments
+
+ /// Uploads an attachment to the specified CDN.
+ ///
+ /// - Parameters:
+ /// - attachment: the attachment to be uploaded.
+ /// - progress: the progress of the upload.
+ /// - completion: called when the attachment is uploaded.
+ public func upload(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ ) {
+ apiClient.attachmentUploader.uploadStandaloneAttachment(
+ attachment,
+ progress: progress,
+ completion: completion
+ )
+ }
// MARK: - Internal
diff --git a/Sources/StreamChat/Controllers/ChannelController/LivestreamChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/LivestreamChannelController.swift
index 0f6de2b45c3..3369ef01871 100644
--- a/Sources/StreamChat/Controllers/ChannelController/LivestreamChannelController.swift
+++ b/Sources/StreamChat/Controllers/ChannelController/LivestreamChannelController.swift
@@ -790,11 +790,20 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
// MARK: - EventsControllerDelegate
public func eventsController(_ controller: EventsController, didReceiveEvent event: Event) {
- guard let channelEvent = event as? ChannelSpecificEvent, channelEvent.cid == cid else {
- return
+ if let channelEvent = event as? ChannelSpecificEvent, channelEvent.cid == cid {
+ handleChannelEvent(event)
}
- handleChannelEvent(event)
+ // User deleted messages event is a global event, not tied to a channel.
+ if let userMessagesDeletedEvent = event as? UserMessagesDeletedEvent {
+ let userId = userMessagesDeletedEvent.user.id
+ if userMessagesDeletedEvent.hardDelete {
+ hardDeleteMessages(from: userId)
+ } else {
+ let deletedAt = userMessagesDeletedEvent.createdAt
+ softDeleteMessages(from: userId, deletedAt: deletedAt)
+ }
+ }
}
// MARK: - AppStateObserverDelegate
@@ -823,9 +832,6 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
paginationStateHandler.begin(pagination: pagination)
}
- let endpoint: Endpoint =
- .updateChannel(query: channelQuery)
-
let requestCompletion: (Result) -> Void = { [weak self] result in
self?.callback { [weak self] in
guard let self = self else { return }
@@ -1085,6 +1091,24 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
messages.removeAll { $0.id == deletedMessage.id }
}
+ private func softDeleteMessages(from userId: UserId, deletedAt: Date) {
+ let messagesWithDeletedMessages = messages.map { message in
+ if message.author.id == userId {
+ return message.changing(
+ deletedAt: deletedAt
+ )
+ }
+ return message
+ }
+ messages = messagesWithDeletedMessages
+ }
+
+ private func hardDeleteMessages(from userId: UserId) {
+ messages.removeAll { message in
+ message.author.id == userId
+ }
+ }
+
private func handleNewReaction(_ reactionEvent: ReactionNewEvent) {
updateMessage(reactionEvent.message)
}
diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
index b0abf2be422..cbe0c05bd4a 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
@@ -41,6 +41,7 @@ class ChannelDTO: NSManagedObject {
@NSManaged var watcherCount: Int64
@NSManaged var memberCount: Int64
+ @NSManaged var messageCount: NSNumber?
@NSManaged var isFrozen: Bool
@NSManaged var cooldownDuration: Int
@@ -261,6 +262,10 @@ extension NSManagedObjectContext {
dto.defaultSortingAt = (payload.lastMessageAt ?? payload.createdAt).bridgeDate
dto.lastMessageAt = payload.lastMessageAt?.bridgeDate
dto.memberCount = Int64(clamping: payload.memberCount)
+
+ if let messageCount = payload.messageCount {
+ dto.messageCount = NSNumber(value: messageCount)
+ }
// Because `truncatedAt` is used, client side, for both truncation and channel hiding cases, we need to avoid using the
// value returned by the Backend in some cases.
@@ -285,8 +290,6 @@ extension NSManagedObjectContext {
// for blocked 1:1 channels on channel list query
if let isBlocked = payload.isBlocked {
dto.isBlocked = isBlocked
- } else {
- dto.isBlocked = false
}
// Backend only returns a boolean for hidden state
@@ -642,6 +645,7 @@ extension ChatChannel {
unreadCount: unreadCount,
watcherCount: Int(dto.watcherCount),
memberCount: Int(dto.memberCount),
+ messageCount: dto.messageCount?.intValue,
reads: reads,
cooldownDuration: Int(dto.cooldownDuration),
extraData: extraData,
diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift
index f0c85f09220..3c9162da3ce 100644
--- a/Sources/StreamChat/Database/DatabaseSession.swift
+++ b/Sources/StreamChat/Database/DatabaseSession.swift
@@ -852,6 +852,10 @@ extension DatabaseSession {
if isNewMessage && savedMessage.localMessageState != nil {
savedMessage.markMessageAsSent()
}
+
+ if let messageCount = payload.channelMessageCount {
+ channelDTO.messageCount = NSNumber(value: messageCount)
+ }
}
func updateChannelPreview(from payload: EventPayload) {
diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
index 17b35000091..e1f6b951516 100644
--- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
+++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
@@ -49,12 +49,13 @@
-
+
+
diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
index f1c2b21e543..384ecd81940 100644
--- a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
+++ b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
@@ -7,5 +7,5 @@ import Foundation
extension SystemEnvironment {
/// A Stream Chat version.
- public static let version: String = "4.86.0"
+ public static let version: String = "4.87.0"
}
diff --git a/Sources/StreamChat/Info.plist b/Sources/StreamChat/Info.plist
index 10dcd1335d0..33653b5dee1 100644
--- a/Sources/StreamChat/Info.plist
+++ b/Sources/StreamChat/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.86.0
+ 4.87.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift
index d7baa0ce87a..9e5aac6c34d 100644
--- a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift
+++ b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift
@@ -4,6 +4,39 @@
import Foundation
+public struct StreamAttachment {
+ /// The attachment type.
+ public let type: AttachmentType
+
+ /// The attachment payload.
+ public var payload: Payload
+
+ /// The downloading state of the attachment.
+ ///
+ /// Reflects the downloading progress for attachments.
+ public let downloadingState: AttachmentDownloadingState?
+
+ /// The uploading state of the attachment.
+ ///
+ /// Reflects uploading progress for local attachments that require file uploading.
+ /// Is `nil` for local attachments that don't need to be uploaded.
+ ///
+ /// Becomes `nil` when the message with the current attachment is sent.
+ public let uploadingState: AttachmentUploadingState?
+
+ public init(
+ type: AttachmentType,
+ payload: Payload,
+ downloadingState: AttachmentDownloadingState?,
+ uploadingState: AttachmentUploadingState?
+ ) {
+ self.type = type
+ self.payload = payload
+ self.downloadingState = downloadingState
+ self.uploadingState = uploadingState
+ }
+}
+
/// A type representing a chat message attachment.
/// `ChatMessageAttachment` is an immutable snapshot of message attachment at the given time.
@dynamicMemberLookup
@@ -81,6 +114,13 @@ public struct AttachmentUploadingState: Hashable {
/// The information about file size/mimeType.
public let file: AttachmentFile
+
+ /// Public init.
+ public init(localFileURL: URL, state: LocalAttachmentState, file: AttachmentFile) {
+ self.localFileURL = localFileURL
+ self.state = state
+ self.file = file
+ }
}
// MARK: - Type erasure/recovery
diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift
index 6a0779c2f2a..aa0eedecb5b 100644
--- a/Sources/StreamChat/Models/Channel.swift
+++ b/Sources/StreamChat/Models/Channel.swift
@@ -61,6 +61,10 @@ public struct ChatChannel {
/// The total number of members in the channel.
public let memberCount: Int
+
+ /// The total number of messages in the channel.
+ /// Only returns value if `count_messages` is configured for your app.
+ public let messageCount: Int?
/// A list of members of this channel.
///
@@ -197,6 +201,7 @@ public struct ChatChannel {
unreadCount: ChannelUnreadCount,
watcherCount: Int = 0,
memberCount: Int = 0,
+ messageCount: Int? = nil,
reads: [ChatChannelRead] = [],
cooldownDuration: Int = 0,
extraData: [String: RawJSON],
@@ -227,6 +232,7 @@ public struct ChatChannel {
self.team = team
self.watcherCount = watcherCount
self.memberCount = memberCount
+ self.messageCount = messageCount
self.reads = reads
self.cooldownDuration = cooldownDuration
self.extraData = extraData
diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift
index 80edd933d4f..14711b8c644 100644
--- a/Sources/StreamChat/Models/ChatMessage.swift
+++ b/Sources/StreamChat/Models/ChatMessage.swift
@@ -554,6 +554,7 @@ extension ChatMessage: Hashable {
guard lhs.id == rhs.id else { return false }
guard lhs.localState == rhs.localState else { return false }
guard lhs.updatedAt == rhs.updatedAt else { return false }
+ guard lhs.deletedAt == rhs.deletedAt else { return false }
guard lhs.allAttachments == rhs.allAttachments else { return false }
guard lhs.poll == rhs.poll else { return false }
guard lhs.author == rhs.author else { return false }
diff --git a/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift b/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift
index 8b251d40f19..f1348b83051 100644
--- a/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift
+++ b/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift
@@ -53,6 +53,7 @@ extension ChannelPayload {
unreadCount: unreadCount ?? .noUnread,
watcherCount: watcherCount ?? 0,
memberCount: channelPayload.memberCount,
+ messageCount: channelPayload.messageCount,
reads: mappedReads,
cooldownDuration: channelPayload.cooldownDuration,
extraData: channelPayload.extraData,
diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
index 471d6a5ac28..7b6cf562756 100644
--- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
+++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware.swift
@@ -228,10 +228,6 @@ struct ChannelReadUpdaterMiddleware: EventMiddleware {
return .messageIsThreadReply
}
- if message.type == .system {
- return .messageIsSystem
- }
-
if message.isShadowed {
return .messageIsShadowed
}
@@ -250,7 +246,6 @@ private enum UnreadSkippingReason: CustomStringConvertible {
case messageIsOwn
case messageIsSilent
case messageIsThreadReply
- case messageIsSystem
case messageIsShadowed
case messageIsSeen
case messageIsSoftDeleted
@@ -267,8 +262,6 @@ private enum UnreadSkippingReason: CustomStringConvertible {
return "Silent messages do not affect unread counts"
case .messageIsThreadReply:
return "Thread replies do not affect unread counts"
- case .messageIsSystem:
- return "System messages do not affect unread counts"
case .messageIsShadowed:
return "Shadowed messages do not affect unread counts"
case .messageIsSeen:
diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift
index 9ffdf9f4ce3..9f023e2636f 100644
--- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift
+++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/UserChannelBanEventsMiddleware.swift
@@ -30,6 +30,18 @@ struct UserChannelBanEventsMiddleware: EventMiddleware {
memberDTO.isShadowBanned = false
memberDTO.banExpiresAt = nil
+ case let userMessagesDeletedEvent as UserMessagesDeletedEventDTO:
+ let userId = userMessagesDeletedEvent.user.id
+ if let userDTO = session.user(id: userId) {
+ userDTO.messages?.forEach { message in
+ if userMessagesDeletedEvent.payload.hardDelete {
+ message.isHardDeleted = true
+ } else {
+ message.deletedAt = userMessagesDeletedEvent.createdAt.bridgeDate
+ }
+ }
+ }
+
default:
break
}
diff --git a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
index 689d6e61952..786d7bcb184 100644
--- a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
@@ -41,6 +41,7 @@ class EventPayload: Decodable {
case aiMessage = "ai_message"
case draft
case reminder
+ case channelMessageCount = "channel_message_count"
}
let eventType: EventType
@@ -79,6 +80,7 @@ class EventPayload: Decodable {
let aiMessage: String?
let draft: DraftPayload?
let reminder: ReminderPayload?
+ let channelMessageCount: Int?
init(
eventType: EventType,
@@ -112,7 +114,8 @@ class EventPayload: Decodable {
messageId: String? = nil,
aiMessage: String? = nil,
draft: DraftPayload? = nil,
- reminder: ReminderPayload? = nil
+ reminder: ReminderPayload? = nil,
+ channelMessageCount: Int? = nil
) {
self.eventType = eventType
self.connectionId = connectionId
@@ -146,6 +149,7 @@ class EventPayload: Decodable {
self.aiMessage = aiMessage
self.draft = draft
self.reminder = reminder
+ self.channelMessageCount = channelMessageCount
}
required init(from decoder: Decoder) throws {
@@ -184,6 +188,7 @@ class EventPayload: Decodable {
aiMessage = try container.decodeIfPresent(String.self, forKey: .aiMessage)
draft = try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
reminder = try container.decodeIfPresent(ReminderPayload.self, forKey: .reminder)
+ channelMessageCount = try container.decodeIfPresent(Int.self, forKey: .channelMessageCount)
}
func event() throws -> Event {
diff --git a/Sources/StreamChat/WebSocketClient/Events/EventType.swift b/Sources/StreamChat/WebSocketClient/Events/EventType.swift
index 504ef2e2502..b02d7f4bf37 100644
--- a/Sources/StreamChat/WebSocketClient/Events/EventType.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/EventType.swift
@@ -38,6 +38,8 @@ public extension EventType {
static let userBanned: Self = "user.banned"
/// When a user was unbanned.
static let userUnbanned: Self = "user.unbanned"
+ /// When the messages of a banned user should be deleted.
+ static let userMessagesDeleted: Self = "user.messages.deleted"
// MARK: Channel Events
@@ -191,6 +193,8 @@ extension EventType {
return try (try? UserBannedEventDTO(from: response)) ?? UserGloballyBannedEventDTO(from: response)
case .userUnbanned:
return try (try? UserUnbannedEventDTO(from: response)) ?? UserGloballyUnbannedEventDTO(from: response)
+ case .userMessagesDeleted:
+ return try UserMessagesDeletedEventDTO(from: response)
case .channelCreated: throw ClientError.IgnoredEventType()
case .channelUpdated: return try ChannelUpdatedEventDTO(from: response)
diff --git a/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift b/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift
index 0da70e117b9..ee17af1049a 100644
--- a/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/UserEvents.swift
@@ -271,3 +271,44 @@ class UserUnbannedEventDTO: EventDTO {
)
}
}
+
+/// Triggered when the messages of a banned user should be deleted.
+public struct UserMessagesDeletedEvent: Event {
+ /// The banned user.
+ public let user: ChatUser
+
+ /// If the messages should be hard deleted or not.
+ public let hardDelete: Bool
+
+ /// The event timestamp
+ public let createdAt: Date
+}
+
+class UserMessagesDeletedEventDTO: EventDTO {
+ let user: UserPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ user = try response.value(at: \.user)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: DatabaseSession) -> Event? {
+ if let userDTO = session.user(id: user.id),
+ let userModel = try? userDTO.asModel() {
+ return UserMessagesDeletedEvent(
+ user: userModel,
+ hardDelete: payload.hardDelete,
+ createdAt: createdAt
+ )
+ }
+
+ return UserMessagesDeletedEvent(
+ user: user.asModel(),
+ hardDelete: payload.hardDelete,
+ createdAt: createdAt
+ )
+ }
+}
diff --git a/Sources/StreamChat/Workers/UserUpdater.swift b/Sources/StreamChat/Workers/UserUpdater.swift
index ad703fd8741..81c455306a4 100644
--- a/Sources/StreamChat/Workers/UserUpdater.swift
+++ b/Sources/StreamChat/Workers/UserUpdater.swift
@@ -44,6 +44,7 @@ class UserUpdater: Worker {
participantId: userId,
context: self.database.writableContext
)
+ channel?.isBlocked = true
channel?.isHidden = true
}, completion: {
if let error = $0 {
@@ -75,6 +76,7 @@ class UserUpdater: Worker {
participantId: userId,
context: self.database.writableContext
)
+ channel?.isBlocked = false
channel?.isHidden = false
}, completion: {
if let error = $0 {
diff --git a/Sources/StreamChatUI/Appearance+Images.swift b/Sources/StreamChatUI/Appearance+Images.swift
index 065165575c6..0feaef3cb68 100644
--- a/Sources/StreamChatUI/Appearance+Images.swift
+++ b/Sources/StreamChatUI/Appearance+Images.swift
@@ -46,7 +46,7 @@ public extension Appearance {
public var smallBolt: UIImage = loadImageSafely(with: "bolt_small")
public var openAttachments: UIImage = loadImageSafely(with: "clip")
public var shrinkInputArrow: UIImage = loadImageSafely(with: "arrow_shrink_input")
- public var sendArrow: UIImage = loadImageSafely(with: "arrow_send")
+ public var sendArrow: UIImage = loadImageSafely(with: "arrow_send").imageFlippedForRightToLeftLayoutDirection()
public var scrollDownArrow: UIImage = loadImageSafely(with: "arrow_down")
public var whiteCheckmark: UIImage = loadImageSafely(with: "checkmark_white")
public var confirmCheckmark: UIImage = loadImageSafely(with: "checkmark_confirm")
@@ -61,14 +61,14 @@ public extension Appearance {
public var mic: UIImage = loadSafely(systemName: "mic", assetsFallback: "mic")
public var lock: UIImage = loadSafely(systemName: "lock", assetsFallback: "lock")
- public var chevronLeft: UIImage = loadSafely(systemName: "chevron.left", assetsFallback: "chevron.left")
- public var chevronRight: UIImage = loadSafely(systemName: "chevron.right", assetsFallback: "chevron.right")
+ public var chevronLeft: UIImage = loadSafely(systemName: "chevron.left", assetsFallback: "chevron.left").imageFlippedForRightToLeftLayoutDirection()
+ public var chevronRight: UIImage = loadSafely(systemName: "chevron.right", assetsFallback: "chevron.right").imageFlippedForRightToLeftLayoutDirection()
public var chevronUp: UIImage = loadSafely(systemName: "chevron.up", assetsFallback: "chevron.up")
public var trash: UIImage = loadSafely(systemName: "trash", assetsFallback: "trash")
public var stop: UIImage = loadSafely(systemName: "stop.circle", assetsFallback: "")
- public var playFill: UIImage = loadSafely(systemName: "play.fill", assetsFallback: "play.fill")
+ public var playFill: UIImage = loadSafely(systemName: "play.fill", assetsFallback: "play.fill").imageFlippedForRightToLeftLayoutDirection()
public var pauseFill: UIImage = loadSafely(systemName: "pause.fill", assetsFallback: "pause.fill")
- public var recordingPlay: UIImage = loadSafely(systemName: "play", assetsFallback: "play_big")
+ public var recordingPlay: UIImage = loadSafely(systemName: "play", assetsFallback: "play_big").imageFlippedForRightToLeftLayoutDirection()
public var recordingPause: UIImage = loadSafely(systemName: "pause", assetsFallback: "pause.fill")
public var rateButtonPillBackground: UIImage = loadImageSafely(with: "pill")
public var sliderThumb: UIImage = loadImageSafely(with: "sliderThumb")
@@ -228,8 +228,8 @@ public extension Appearance {
// MARK: - Message Actions
- public var messageActionSwipeReply: UIImage = loadImageSafely(with: "icn_inline_reply")
- public var messageActionInlineReply: UIImage = loadImageSafely(with: "icn_inline_reply")
+ public var messageActionSwipeReply: UIImage = loadImageSafely(with: "icn_inline_reply").imageFlippedForRightToLeftLayoutDirection()
+ public var messageActionInlineReply: UIImage = loadImageSafely(with: "icn_inline_reply").imageFlippedForRightToLeftLayoutDirection()
public var messageActionThreadReply: UIImage = loadImageSafely(with: "icn_thread_reply")
public var messageActionMarkUnread: UIImage = loadSafely(systemName: "message.badge", assetsFallback: "mark_unread")
@@ -285,9 +285,9 @@ public extension Appearance {
}
public var camera: UIImage = loadImageSafely(with: "camera")
- public var bigPlay: UIImage = loadImageSafely(with: "play_big")
+ public var bigPlay: UIImage = loadImageSafely(with: "play_big").imageFlippedForRightToLeftLayoutDirection()
- public var play: UIImage = loadImageSafely(with: "play")
+ public var play: UIImage = loadImageSafely(with: "play").imageFlippedForRightToLeftLayoutDirection()
public var pause: UIImage = loadImageSafely(with: "pause")
// MARK: - CommandIcons
diff --git a/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageAttachmentPreviewVC.swift b/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageAttachmentPreviewVC.swift
index cf7cd9b69fa..8f243b3209e 100644
--- a/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageAttachmentPreviewVC.swift
+++ b/Sources/StreamChatUI/ChatMessageList/Attachments/ChatMessageAttachmentPreviewVC.swift
@@ -28,14 +28,14 @@ open class ChatMessageAttachmentPreviewVC: _ViewController, WKNavigationDelegate
)
private lazy var goBackButton = UIBarButtonItem(
- title: "←",
+ image: appearance.images.chevronLeft,
style: .plain,
target: self,
action: #selector(goBack)
)
private lazy var goForwardButton = UIBarButtonItem(
- title: "→",
+ image: appearance.images.chevronRight,
style: .plain,
target: self,
action: #selector(goForward)
diff --git a/Sources/StreamChatUI/ChatMessageList/SwipeToReplyGestureHandler.swift b/Sources/StreamChatUI/ChatMessageList/SwipeToReplyGestureHandler.swift
index ca880ef1cf8..6962c75b6de 100644
--- a/Sources/StreamChatUI/ChatMessageList/SwipeToReplyGestureHandler.swift
+++ b/Sources/StreamChatUI/ChatMessageList/SwipeToReplyGestureHandler.swift
@@ -95,7 +95,11 @@ open class SwipeToReplyGestureHandler {
let translation = gesture.translation(in: messageCell)
animateViews(with: translation)
- shouldReply = translation.x > swipeThreshold
+ if messageCell?.effectiveUserInterfaceLayoutDirection == .rightToLeft {
+ shouldReply = -translation.x > swipeThreshold
+ } else {
+ shouldReply = translation.x > swipeThreshold
+ }
if shouldReply && shouldTriggerFeedback {
impactFeedbackGenerator.impactOccurred()
@@ -121,21 +125,23 @@ open class SwipeToReplyGestureHandler {
/// Animates the views when a swiping is happening.
open func animateViews(with translation: CGPoint) {
+ let isRTL = messageCell?.effectiveUserInterfaceLayoutDirection == .rightToLeft
swipeableViews.forEach {
guard let originalCenter = swipeableViewsOriginalPositions[$0] else { return }
+ let x = isRTL ? min(originalCenter.x, originalCenter.x + translation.x) : max(originalCenter.x, originalCenter.x + translation.x)
$0.center = CGPoint(
- x: max(originalCenter.x, originalCenter.x + translation.x),
+ x: x,
y: originalCenter.y
)
}
- let replyIconTranslation = max(0, min(translation.x, swipeThreshold))
+ let replyIconTranslation = isRTL ? min(0, max(translation.x, -swipeThreshold)) : max(0, min(translation.x, swipeThreshold))
replyIconImageView?.center = CGPoint(
x: replyIconOriginalPosition.x + replyIconTranslation,
y: replyIconOriginalPosition.y
)
replyIconImageView?.isHidden = false
- replyIconImageView?.alpha = replyIconTranslation / swipeThreshold
+ replyIconImageView?.alpha = abs(replyIconTranslation) / swipeThreshold
}
/// Animates the views to their original positions.
diff --git a/Sources/StreamChatUI/CommonViews/InputTextView/InputTextView.swift b/Sources/StreamChatUI/CommonViews/InputTextView/InputTextView.swift
index c4070f21f6c..979742a5a21 100644
--- a/Sources/StreamChatUI/CommonViews/InputTextView/InputTextView.swift
+++ b/Sources/StreamChatUI/CommonViews/InputTextView/InputTextView.swift
@@ -157,9 +157,11 @@ open class InputTextView: UITextView, ThemeProvider {
open func setUpLayout() {
addSubview(placeholderLabel)
+ placeholderLabel.setContentCompressionResistancePriority(.streamLow, for: .horizontal)
NSLayoutConstraint.activate([
placeholderLabel.leadingAnchor.pin(equalTo: leadingAnchor, constant: directionalLayoutMargins.leading),
- placeholderLabel.trailingAnchor.pin(lessThanOrEqualTo: trailingAnchor),
+ placeholderLabel.trailingAnchor.pin(equalTo: trailingAnchor, constant: -directionalLayoutMargins.trailing),
+ placeholderLabel.widthAnchor.pin(equalTo: layoutMarginsGuide.widthAnchor),
placeholderLabel.topAnchor.pin(equalTo: topAnchor),
placeholderLabel.bottomAnchor.pin(lessThanOrEqualTo: bottomAnchor),
placeholderLabel.centerYAnchor.pin(equalTo: centerYAnchor)
diff --git a/Sources/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist
index 10dcd1335d0..33653b5dee1 100644
--- a/Sources/StreamChatUI/Info.plist
+++ b/Sources/StreamChatUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.86.0
+ 4.87.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/Sources/StreamChatUI/VoiceRecording/Views/SlideToCancelView.swift b/Sources/StreamChatUI/VoiceRecording/Views/SlideToCancelView.swift
index eab2f134cd1..0caffcf5ae8 100644
--- a/Sources/StreamChatUI/VoiceRecording/Views/SlideToCancelView.swift
+++ b/Sources/StreamChatUI/VoiceRecording/Views/SlideToCancelView.swift
@@ -60,27 +60,14 @@ open class SlideToCancelView: _View, ThemeProvider {
override open func setUpAppearance() {
super.setUpAppearance()
- configureChevron()
+ chevronImageView.image = appearance.images.chevronLeft.tinted(with: appearance.colorPalette.textLowEmphasis)
titleLabel.textColor = appearance.colorPalette.textLowEmphasis
titleLabel.font = appearance.fonts.body
titleLabel.text = L10n.Recording.slideToCancel
}
-
- override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
- super.traitCollectionDidChange(previousTraitCollection)
-
- if traitCollection.layoutDirection != previousTraitCollection?.layoutDirection {
- configureChevron()
- }
- }
override open func updateContent() {
super.updateContent()
alpha = content.alpha
}
-
- private func configureChevron() {
- let chevron = traitCollection.layoutDirection == .leftToRight ? appearance.images.chevronLeft : appearance.images.chevronRight
- chevronImageView.image = chevron.tinted(with: appearance.colorPalette.textLowEmphasis)
- }
}
diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec
index e7a50557a56..0b3caf503b7 100644
--- a/StreamChat-XCFramework.podspec
+++ b/StreamChat-XCFramework.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChat-XCFramework"
- spec.version = "4.86.0"
+ spec.version = "4.87.0"
spec.summary = "StreamChat iOS Client"
spec.description = "stream-chat-swift is the official Swift client for Stream Chat, a service for building chat applications."
diff --git a/StreamChat.podspec b/StreamChat.podspec
index 5508dd3a6fd..15f0bf95979 100644
--- a/StreamChat.podspec
+++ b/StreamChat.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChat"
- spec.version = "4.86.0"
+ spec.version = "4.87.0"
spec.summary = "StreamChat iOS Chat Client"
spec.description = "stream-chat-swift is the official Swift client for Stream Chat, a service for building chat applications."
diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj
index 6d939db3995..140ad99c899 100644
--- a/StreamChat.xcodeproj/project.pbxproj
+++ b/StreamChat.xcodeproj/project.pbxproj
@@ -797,6 +797,7 @@
84B7383E2BE8C13A00EC66EC /* PollController+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7383C2BE8C13A00EC66EC /* PollController+SwiftUI.swift */; };
84B8779E2AC30F0E009EF76A /* DemoShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8779D2AC30F0E009EF76A /* DemoShareViewModel.swift */; };
84B877A02AC31AB8009EF76A /* StreamChat+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8779F2AC31AB8009EF76A /* StreamChat+Extensions.swift */; };
+ 84BC99282E5C56590000FB87 /* StreamAttachment_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC99272E5C56590000FB87 /* StreamAttachment_Mock.swift */; };
84BE85DA2AC30E88007DD47C /* DemoShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BE85D92AC30E88007DD47C /* DemoShareView.swift */; };
84C11BDF27FB2B4600000A9E /* ChannelPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C11BDE27FB2B4600000A9E /* ChannelPayload.swift */; };
84C11BE127FB2C2B00000A9E /* ChannelReadDTO_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C11BE027FB2C2B00000A9E /* ChannelReadDTO_Tests.swift */; };
@@ -3793,6 +3794,7 @@
84B7383C2BE8C13A00EC66EC /* PollController+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollController+SwiftUI.swift"; sourceTree = ""; };
84B8779D2AC30F0E009EF76A /* DemoShareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoShareViewModel.swift; sourceTree = ""; };
84B8779F2AC31AB8009EF76A /* StreamChat+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StreamChat+Extensions.swift"; sourceTree = ""; };
+ 84BC99272E5C56590000FB87 /* StreamAttachment_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamAttachment_Mock.swift; sourceTree = ""; };
84BE85D92AC30E88007DD47C /* DemoShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoShareView.swift; sourceTree = ""; };
84C11BDE27FB2B4600000A9E /* ChannelPayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelPayload.swift; sourceTree = ""; };
84C11BE027FB2C2B00000A9E /* ChannelReadDTO_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelReadDTO_Tests.swift; sourceTree = ""; };
@@ -7002,6 +7004,7 @@
A344075B27D753530044F150 /* ChatMessageLinkAttachment_Mock.swift */,
4F1FB7D72C7DEC6600C47C2A /* ChatMessageVideoAttachment_Mock.swift */,
40A2961929F8244500E0C186 /* ChatMessageVoiceRecordingAttachment_Mock.swift */,
+ 84BC99272E5C56590000FB87 /* StreamAttachment_Mock.swift */,
);
path = Attachments;
sourceTree = "";
@@ -11393,6 +11396,7 @@
A311B42D27E8BB7400CFCF6D /* StreamChatTestTools.swift in Sources */,
40D484022A1264F1009E4134 /* MockAudioRecorder.swift in Sources */,
82F714A72B0784D900442A74 /* UnwrapAsync.swift in Sources */,
+ 84BC99282E5C56590000FB87 /* StreamAttachment_Mock.swift in Sources */,
A344077627D753530044F150 /* ChatMessageReaction_Mock.swift in Sources */,
A311B43B27E8BC8400CFCF6D /* ChannelWatcherListController_Delegate.swift in Sources */,
82E655332B06748400D64906 /* Spy.swift in Sources */,
diff --git a/StreamChatArtifacts.json b/StreamChatArtifacts.json
index 6251d52ba52..16b34cf6420 100644
--- a/StreamChatArtifacts.json
+++ b/StreamChatArtifacts.json
@@ -1 +1 @@
-{"4.7.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.7.0/StreamChat-All.zip","4.8.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.8.0/StreamChat-All.zip","4.9.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.9.0/StreamChat-All.zip","4.10.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.0/StreamChat-All.zip","4.10.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.1/StreamChat-All.zip","4.11.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.11.0/StreamChat-All.zip","4.12.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.12.0/StreamChat-All.zip","4.13.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.0/StreamChat-All.zip","4.13.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.1/StreamChat-All.zip","4.14.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.14.0/StreamChat-All.zip","4.15.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.0/StreamChat-All.zip","4.15.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.1/StreamChat-All.zip","4.16.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.16.0/StreamChat-All.zip","4.17.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.17.0/StreamChat-All.zip","4.18.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.18.0/StreamChat-All.zip","4.19.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.19.0/StreamChat-All.zip","4.20.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.20.0/StreamChat-All.zip","4.21.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.0/StreamChat-All.zip","4.21.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.1/StreamChat-All.zip","4.21.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.2/StreamChat-All.zip","4.22.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.22.0/StreamChat-All.zip","4.23.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.23.0/StreamChat-All.zip","4.24.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.0/StreamChat-All.zip","4.24.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.1/StreamChat-All.zip","4.25.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.0/StreamChat-All.zip","4.25.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.1/StreamChat-All.zip","4.26.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.26.0/StreamChat-All.zip","4.27.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.0/StreamChat-All.zip","4.27.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.1/StreamChat-All.zip","4.28.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.28.0/StreamChat-All.zip","4.29.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.29.0/StreamChat-All.zip","4.30.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.30.0/StreamChat-All.zip","4.31.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.31.0/StreamChat-All.zip","4.32.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.32.0/StreamChat-All.zip","4.33.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.33.0/StreamChat-All.zip","4.34.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.34.0/StreamChat-All.zip","4.35.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.0/StreamChat-All.zip","4.35.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.1/StreamChat-All.zip","4.35.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.2/StreamChat-All.zip","4.36.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.36.0/StreamChat-All.zip","4.37.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.0/StreamChat-All.zip","4.37.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.1/StreamChat-All.zip","4.38.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.38.0/StreamChat-All.zip","4.39.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.39.0/StreamChat-All.zip","4.40.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.40.0/StreamChat-All.zip","4.41.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.41.0/StreamChat-All.zip","4.42.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.42.0/StreamChat-All.zip","4.43.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.43.0/StreamChat-All.zip","4.44.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.44.0/StreamChat-All.zip","4.45.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.45.0/StreamChat-All.zip","4.46.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.46.0/StreamChat-All.zip","4.47.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.0/StreamChat-All.zip","4.47.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.1/StreamChat-All.zip","4.48.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.0/StreamChat-All.zip","4.48.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.1/StreamChat-All.zip","4.49.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.49.0/StreamChat-All.zip","4.50.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.50.0/StreamChat-All.zip","4.51.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.51.0/StreamChat-All.zip","4.52.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.52.0/StreamChat-All.zip","4.53.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.53.0/StreamChat-All.zip","4.54.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.54.0/StreamChat-All.zip","4.55.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.55.0/StreamChat-All.zip","4.56.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.0/StreamChat-All.zip","4.56.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.1/StreamChat-All.zip","4.57.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.57.0/StreamChat-All.zip","4.58.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.58.0/StreamChat-All.zip","4.59.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.59.0/StreamChat-All.zip","4.60.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.60.0/StreamChat-All.zip","4.61.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.61.0/StreamChat-All.zip","4.62.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.62.0/StreamChat-All.zip","4.63.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.63.0/StreamChat-All.zip","4.64.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.64.0/StreamChat-All.zip","4.65.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.65.0/StreamChat-All.zip","4.66.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.66.0/StreamChat-All.zip","4.67.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.67.0/StreamChat-All.zip","4.68.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.68.0/StreamChat-All.zip","4.69.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.69.0/StreamChat-All.zip","4.70.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.70.0/StreamChat-All.zip","4.71.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.71.0/StreamChat-All.zip","4.72.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.72.0/StreamChat-All.zip","4.73.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.73.0/StreamChat-All.zip","4.74.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.74.0/StreamChat-All.zip","4.75.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.75.0/StreamChat-All.zip","4.76.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.76.0/StreamChat-All.zip","4.77.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.77.0/StreamChat-All.zip","4.78.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.78.0/StreamChat-All.zip","4.79.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.0/StreamChat-All.zip","4.79.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.1/StreamChat-All.zip","4.80.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.80.0/StreamChat-All.zip","4.81.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.81.0/StreamChat-All.zip","4.82.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.82.0/StreamChat-All.zip","4.83.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.83.0/StreamChat-All.zip","4.84.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.84.0/StreamChat-All.zip","4.85.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.85.0/StreamChat-All.zip","4.86.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.86.0/StreamChat-All.zip"}
\ No newline at end of file
+{"4.7.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.7.0/StreamChat-All.zip","4.8.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.8.0/StreamChat-All.zip","4.9.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.9.0/StreamChat-All.zip","4.10.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.0/StreamChat-All.zip","4.10.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.1/StreamChat-All.zip","4.11.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.11.0/StreamChat-All.zip","4.12.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.12.0/StreamChat-All.zip","4.13.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.0/StreamChat-All.zip","4.13.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.1/StreamChat-All.zip","4.14.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.14.0/StreamChat-All.zip","4.15.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.0/StreamChat-All.zip","4.15.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.1/StreamChat-All.zip","4.16.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.16.0/StreamChat-All.zip","4.17.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.17.0/StreamChat-All.zip","4.18.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.18.0/StreamChat-All.zip","4.19.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.19.0/StreamChat-All.zip","4.20.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.20.0/StreamChat-All.zip","4.21.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.0/StreamChat-All.zip","4.21.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.1/StreamChat-All.zip","4.21.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.2/StreamChat-All.zip","4.22.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.22.0/StreamChat-All.zip","4.23.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.23.0/StreamChat-All.zip","4.24.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.0/StreamChat-All.zip","4.24.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.1/StreamChat-All.zip","4.25.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.0/StreamChat-All.zip","4.25.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.1/StreamChat-All.zip","4.26.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.26.0/StreamChat-All.zip","4.27.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.0/StreamChat-All.zip","4.27.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.1/StreamChat-All.zip","4.28.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.28.0/StreamChat-All.zip","4.29.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.29.0/StreamChat-All.zip","4.30.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.30.0/StreamChat-All.zip","4.31.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.31.0/StreamChat-All.zip","4.32.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.32.0/StreamChat-All.zip","4.33.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.33.0/StreamChat-All.zip","4.34.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.34.0/StreamChat-All.zip","4.35.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.0/StreamChat-All.zip","4.35.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.1/StreamChat-All.zip","4.35.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.2/StreamChat-All.zip","4.36.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.36.0/StreamChat-All.zip","4.37.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.0/StreamChat-All.zip","4.37.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.1/StreamChat-All.zip","4.38.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.38.0/StreamChat-All.zip","4.39.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.39.0/StreamChat-All.zip","4.40.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.40.0/StreamChat-All.zip","4.41.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.41.0/StreamChat-All.zip","4.42.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.42.0/StreamChat-All.zip","4.43.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.43.0/StreamChat-All.zip","4.44.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.44.0/StreamChat-All.zip","4.45.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.45.0/StreamChat-All.zip","4.46.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.46.0/StreamChat-All.zip","4.47.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.0/StreamChat-All.zip","4.47.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.1/StreamChat-All.zip","4.48.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.0/StreamChat-All.zip","4.48.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.1/StreamChat-All.zip","4.49.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.49.0/StreamChat-All.zip","4.50.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.50.0/StreamChat-All.zip","4.51.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.51.0/StreamChat-All.zip","4.52.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.52.0/StreamChat-All.zip","4.53.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.53.0/StreamChat-All.zip","4.54.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.54.0/StreamChat-All.zip","4.55.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.55.0/StreamChat-All.zip","4.56.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.0/StreamChat-All.zip","4.56.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.1/StreamChat-All.zip","4.57.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.57.0/StreamChat-All.zip","4.58.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.58.0/StreamChat-All.zip","4.59.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.59.0/StreamChat-All.zip","4.60.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.60.0/StreamChat-All.zip","4.61.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.61.0/StreamChat-All.zip","4.62.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.62.0/StreamChat-All.zip","4.63.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.63.0/StreamChat-All.zip","4.64.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.64.0/StreamChat-All.zip","4.65.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.65.0/StreamChat-All.zip","4.66.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.66.0/StreamChat-All.zip","4.67.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.67.0/StreamChat-All.zip","4.68.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.68.0/StreamChat-All.zip","4.69.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.69.0/StreamChat-All.zip","4.70.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.70.0/StreamChat-All.zip","4.71.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.71.0/StreamChat-All.zip","4.72.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.72.0/StreamChat-All.zip","4.73.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.73.0/StreamChat-All.zip","4.74.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.74.0/StreamChat-All.zip","4.75.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.75.0/StreamChat-All.zip","4.76.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.76.0/StreamChat-All.zip","4.77.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.77.0/StreamChat-All.zip","4.78.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.78.0/StreamChat-All.zip","4.79.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.0/StreamChat-All.zip","4.79.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.1/StreamChat-All.zip","4.80.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.80.0/StreamChat-All.zip","4.81.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.81.0/StreamChat-All.zip","4.82.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.82.0/StreamChat-All.zip","4.83.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.83.0/StreamChat-All.zip","4.84.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.84.0/StreamChat-All.zip","4.85.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.85.0/StreamChat-All.zip","4.86.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.86.0/StreamChat-All.zip","4.87.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.87.0/StreamChat-All.zip"}
\ No newline at end of file
diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec
index 11ff3a581f7..8961481d2c2 100644
--- a/StreamChatUI-XCFramework.podspec
+++ b/StreamChatUI-XCFramework.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChatUI-XCFramework"
- spec.version = "4.86.0"
+ spec.version = "4.87.0"
spec.summary = "StreamChat UI Components"
spec.description = "StreamChatUI SDK offers flexible UI components able to display data provided by StreamChat SDK."
diff --git a/StreamChatUI.podspec b/StreamChatUI.podspec
index e77acefc812..5cd598f6e0c 100644
--- a/StreamChatUI.podspec
+++ b/StreamChatUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChatUI"
- spec.version = "4.86.0"
+ spec.version = "4.87.0"
spec.summary = "StreamChat UI Components"
spec.description = "StreamChatUI SDK offers flexible UI components able to display data provided by StreamChat SDK."
diff --git a/TestTools/StreamChatTestTools/Extensions/EndpoinPath+Equatable.swift b/TestTools/StreamChatTestTools/Extensions/EndpoinPath+Equatable.swift
index 70e4ae01a15..cb73e5e7385 100644
--- a/TestTools/StreamChatTestTools/Extensions/EndpoinPath+Equatable.swift
+++ b/TestTools/StreamChatTestTools/Extensions/EndpoinPath+Equatable.swift
@@ -28,7 +28,7 @@ extension EndpointPath: Equatable {
case let (.channelEvent(string1), .channelEvent(string2)): return string1 == string2
case let (.stopWatchingChannel(string1), .stopWatchingChannel(string2)): return string1 == string2
case let (.pinnedMessages(string1), .pinnedMessages(string2)): return string1 == string2
- case let (.uploadAttachment(channelId1, type1), .uploadAttachment(channelId2, type2)): return channelId1 == channelId2 &&
+ case let (.uploadChannelAttachment(channelId1, type1), .uploadChannelAttachment(channelId2, type2)): return channelId1 == channelId2 &&
type1 ==
type2
case let (.sendMessage(channelId1), .sendMessage(channelId2)): return channelId1 == channelId2
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Channel.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Channel.json
index 708c04bcd8b..2c74ec1cf14 100644
--- a/TestTools/StreamChatTestTools/Fixtures/JSONs/Channel.json
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Channel.json
@@ -1309,6 +1309,7 @@
"disabled" : true,
"cooldown" : 10,
"member_count" : 4,
+ "message_count" : 5,
"updated_at" : "2019-05-10T14:03:49.505006Z",
"config" : {
"automod_behavior" : "flag",
diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/StreamAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/StreamAttachment_Mock.swift
new file mode 100644
index 00000000000..43b03abbebb
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/StreamAttachment_Mock.swift
@@ -0,0 +1,39 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+import Foundation
+
+public extension StreamAttachment {
+ /// Creates a new `ChatMessageFileAttachment` object from the provided data.
+ static func mock(
+ payload: Payload,
+ title: String = "Sample.png",
+ assetURL: URL = URL(string: "http://asset.url")!,
+ file: AttachmentFile = AttachmentFile(type: .png, size: 120, mimeType: "image/png"),
+ localState: LocalAttachmentState? = .uploaded,
+ localDownloadState: LocalAttachmentDownloadState? = nil,
+ uploadingState: AttachmentUploadingState? = nil,
+ extraData: [String: RawJSON]? = nil
+ ) -> Self {
+ .init(
+ type: .image,
+ payload: payload,
+ downloadingState: localDownloadState.map {
+ .init(
+ localFileURL: $0 == .downloaded ? .newTemporaryFileURL() : nil,
+ state: $0,
+ file: file
+ )
+ },
+ uploadingState: uploadingState ?? localState.map {
+ .init(
+ localFileURL: assetURL,
+ state: $0,
+ file: file
+ )
+ }
+ )
+ }
+}
diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift
index 440f8cbb826..b1516ef48cb 100644
--- a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift
+++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentUploader_Spy.swift
@@ -28,4 +28,22 @@ final class AttachmentUploader_Spy: AttachmentUploader, Spy {
}
}
}
+
+ func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ ) {
+ record()
+
+ if let uploadAttachmentProgress = uploadAttachmentProgress {
+ progress?(uploadAttachmentProgress)
+ }
+
+ if let uploadAttachmentResult = uploadAttachmentResult {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ completion(uploadAttachmentResult.map { UploadedFile(fileURL: $0.remoteURL )})
+ }
+ }
+ }
}
diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift
index c03fc7fb0b4..ce4481073ea 100644
--- a/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift
+++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/CDNClient_Spy.swift
@@ -28,4 +28,21 @@ final class CDNClient_Spy: CDNClient, Spy {
}
}
}
+
+ func uploadStandaloneAttachment(
+ _ attachment: StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ ) {
+ record()
+ if let uploadAttachmentProgress = uploadAttachmentProgress {
+ progress?(uploadAttachmentProgress)
+ }
+
+ if let uploadAttachmentResult = uploadAttachmentResult {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ completion(uploadAttachmentResult.map { UploadedFile(fileURL: $0) })
+ }
+ }
+ }
}
diff --git a/TestTools/StreamChatTestTools/TestData/CustomCDNClient.swift b/TestTools/StreamChatTestTools/TestData/CustomCDNClient.swift
index 07eefbdc8d4..ddf9d0d3a29 100644
--- a/TestTools/StreamChatTestTools/TestData/CustomCDNClient.swift
+++ b/TestTools/StreamChatTestTools/TestData/CustomCDNClient.swift
@@ -13,4 +13,10 @@ public final class CustomCDNClient: CDNClient {
progress: ((Double) -> Void)?,
completion: @escaping (Result) -> Void
) {}
+
+ public func uploadStandaloneAttachment(
+ _ attachment: StreamChat.StreamAttachment,
+ progress: ((Double) -> Void)?,
+ completion: @escaping (Result) -> Void
+ ) {}
}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift b/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift
index 6854c793297..9c3b763dfc8 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift
@@ -22,11 +22,12 @@ extension ChannelDetailPayload {
config: ChannelConfig = .mock(),
ownCapabilities: [String] = [],
isFrozen: Bool = false,
- isBlocked: Bool = false,
+ isBlocked: Bool? = false,
isDisabled: Bool = false,
isHidden: Bool? = nil,
members: [MemberPayload] = [],
memberCount: Int? = nil,
+ messageCount: Int? = nil,
team: String? = nil,
cooldownDuration: Int = 0
) -> Self {
@@ -50,6 +51,7 @@ extension ChannelDetailPayload {
isHidden: isHidden,
members: members,
memberCount: memberCount ?? members.count,
+ messageCount: messageCount,
team: team,
cooldownDuration: cooldownDuration
)
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
index 9a4b1f5272f..2e18c4b784b 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
@@ -156,6 +156,8 @@ extension XCTestCase {
ownCapabilities: [String] = [],
channelExtraData: [String: RawJSON] = [:],
createdAt: Date = XCTestCase.channelCreatedDate,
+ blocked: Bool? = false,
+ hidden: Bool? = nil,
truncatedAt: Date? = nil,
cooldownDuration: Int? = nil,
channelReads: [ChannelReadPayload]? = nil
@@ -189,10 +191,11 @@ extension XCTestCase {
ownCapabilities: ownCapabilities,
isDisabled: false,
isFrozen: true,
- isBlocked: false,
- isHidden: nil,
+ isBlocked: blocked,
+ isHidden: hidden,
members: members,
memberCount: 100,
+ messageCount: 100,
team: .unique,
cooldownDuration: cooldownDuration ?? .random(in: 0...120)
),
@@ -311,6 +314,7 @@ extension XCTestCase {
isHidden: nil,
members: nil,
memberCount: 100,
+ messageCount: 100,
team: .unique,
cooldownDuration: .random(in: 0...120)
),
@@ -409,9 +413,9 @@ extension XCTestCase {
latestAnswers: [PollVotePayload?]? = nil,
options: [PollOptionPayload?] = [],
ownVotes: [PollVotePayload?] = [],
- custom: [String : RawJSON] = [:],
- latestVotesByOption: [String : [PollVotePayload]] = [:],
- voteCountsByOption: [String : Int] = [:],
+ custom: [String: RawJSON] = [:],
+ latestVotesByOption: [String: [PollVotePayload]] = [:],
+ voteCountsByOption: [String: Int] = [:],
isClosed: Bool? = nil,
maxVotesAllowed: Int? = nil,
votingVisibility: String? = nil,
@@ -501,7 +505,7 @@ private extension MemberPayload {
isInvisible: true,
isBanned: true,
teams: [],
- language: nil,
+ language: nil,
extraData: [:]
),
userId: userId,
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/AttachmentEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/AttachmentEndpoints_Tests.swift
index ad3cd16e6ba..344a4b9c6a8 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/AttachmentEndpoints_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/AttachmentEndpoints_Tests.swift
@@ -22,7 +22,7 @@ final class AttachmentEndpoints_Tests: XCTestCase {
for (type, pathComponent) in testCases {
let expectedEndpoint: Endpoint = .init(
- path: .uploadAttachment(channelId: id.cid.apiPath, type: pathComponent),
+ path: .uploadChannelAttachment(channelId: id.cid.apiPath, type: pathComponent),
method: .post,
queryItems: nil,
requiresConnectionId: false,
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
index 5993c05233b..7d24475b095 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
@@ -121,7 +121,7 @@ final class EndpointPathTests: XCTestCase {
assertResultEncodingAndDecoding(.channelEvent("channel_idq"))
assertResultEncodingAndDecoding(.stopWatchingChannel("channel_idq"))
assertResultEncodingAndDecoding(.pinnedMessages("channel_idq"))
- assertResultEncodingAndDecoding(.uploadAttachment(channelId: "channel_id", type: "file"))
+ assertResultEncodingAndDecoding(.uploadChannelAttachment(channelId: "channel_id", type: "file"))
assertResultEncodingAndDecoding(.sendMessage(ChannelId(type: .messaging, id: "the_id")))
assertResultEncodingAndDecoding(.message("message_idm"))
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
index 060d1519239..d993de68aa9 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
@@ -163,6 +163,7 @@ final class ChannelListPayload_Tests: XCTestCase {
)
},
memberCount: 100,
+ messageCount: 100,
team: .unique,
cooldownDuration: .random(in: 0...120)
)
@@ -304,6 +305,7 @@ final class ChannelPayload_Tests: XCTestCase {
XCTAssertEqual(channel.isDisabled, true)
XCTAssertEqual(channel.isFrozen, true)
XCTAssertEqual(channel.memberCount, 4)
+ XCTAssertEqual(channel.messageCount, 5)
XCTAssertEqual(channel.updatedAt, "2019-05-10T14:03:49.505006Z".toDate())
XCTAssertEqual(channel.cooldownDuration, 10)
XCTAssertEqual(channel.team, "GREEN")
@@ -440,6 +442,7 @@ final class ChannelPayload_Tests: XCTestCase {
isHidden: true,
members: [memberPayload],
memberCount: 10,
+ messageCount: 10,
team: "team-id",
cooldownDuration: 30
)
@@ -494,6 +497,7 @@ final class ChannelPayload_Tests: XCTestCase {
XCTAssertEqual(chatChannel.unreadCount, unreadCount)
XCTAssertEqual(chatChannel.watcherCount, 5)
XCTAssertEqual(chatChannel.memberCount, 10)
+ XCTAssertEqual(chatChannel.messageCount, 10)
XCTAssertEqual(chatChannel.reads.count, 1)
XCTAssertEqual(chatChannel.reads.first?.user.id, "reader-user-id")
XCTAssertEqual(chatChannel.cooldownDuration, 30)
@@ -534,6 +538,7 @@ final class ChannelPayload_Tests: XCTestCase {
isHidden: nil,
members: nil,
memberCount: 0,
+ messageCount: nil,
team: nil,
cooldownDuration: 0
)
@@ -582,6 +587,7 @@ final class ChannelPayload_Tests: XCTestCase {
XCTAssertEqual(chatChannel.unreadCount, .noUnread)
XCTAssertEqual(chatChannel.watcherCount, 0)
XCTAssertEqual(chatChannel.memberCount, 0)
+ XCTAssertEqual(chatChannel.messageCount, nil)
XCTAssertTrue(chatChannel.reads.isEmpty)
XCTAssertEqual(chatChannel.cooldownDuration, 0)
XCTAssertEqual(chatChannel.extraData, [:])
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift
index 77fa9e84d09..f8d922f315e 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift
@@ -311,6 +311,7 @@ final class IdentifiablePayload_Tests: XCTestCase {
isHidden: false,
members: users.map { MemberPayload.dummy(user: $0) },
memberCount: users.count,
+ messageCount: messageCount,
team: .unique,
cooldownDuration: 20
)
diff --git a/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift b/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift
index e97d2450958..bffbba4cb85 100644
--- a/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/StreamAttachmentUploader_Tests.swift
@@ -61,4 +61,54 @@ final class StreamAttachmentUploader_Tests: XCTestCase {
waitForExpectations(timeout: defaultTimeout)
}
+
+ func test_standaloneUpload_whenSuccessful() {
+ let expUploadComplete = expectation(description: "should complete upload attachment")
+ let expProgressCalled = expectation(description: "should call progress closure")
+ let expectedUrl = URL.localYodaImage
+ let expectedProgress: Double = 20
+
+ let data = try! Data(contentsOf: expectedUrl)
+ let mockedAttachment = StreamAttachment.mock(payload: data)
+ let mockProgress: ((Double) -> Void) = {
+ XCTAssertEqual($0, expectedProgress)
+ expProgressCalled.fulfill()
+ }
+
+ let cdnClient = CDNClient_Spy()
+ cdnClient.uploadAttachmentResult = .success(expectedUrl)
+ cdnClient.uploadAttachmentProgress = expectedProgress
+
+ let sut = StreamAttachmentUploader(cdnClient: cdnClient)
+ sut.uploadStandaloneAttachment(mockedAttachment, progress: mockProgress) { result in
+ let uploadedAttachment = try? result.get()
+ XCTAssertEqual(uploadedAttachment?.fileURL, expectedUrl)
+ expUploadComplete.fulfill()
+ }
+
+ waitForExpectations(timeout: defaultTimeout)
+ }
+
+ func test_standaloneUpload_whenError() {
+ let exp = expectation(description: "should complete upload attachment")
+
+ let expectedUrl = URL.localYodaImage
+ let data = try! Data(contentsOf: expectedUrl)
+ let mockedAttachment = StreamAttachment.mock(payload: data)
+
+ let expectedError = ClientError("Some Error")
+ let cdnClient = CDNClient_Spy()
+ cdnClient.uploadAttachmentResult = .failure(expectedError)
+
+ let sut = StreamAttachmentUploader(cdnClient: cdnClient)
+ sut.uploadStandaloneAttachment(
+ mockedAttachment,
+ progress: nil
+ ) { result in
+ XCTAssertEqual(result.error, expectedError)
+ exp.fulfill()
+ }
+
+ waitForExpectations(timeout: defaultTimeout)
+ }
}
diff --git a/Tests/StreamChatTests/APIClient/StreamCDNClient_Tests.swift b/Tests/StreamChatTests/APIClient/StreamCDNClient_Tests.swift
index 559287ed4be..3016db27094 100644
--- a/Tests/StreamChatTests/APIClient/StreamCDNClient_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/StreamCDNClient_Tests.swift
@@ -39,6 +39,39 @@ final class StreamCDNClient_Tests: XCTestCase {
// Check the encoder is called with the correct endpoint
XCTAssertEqual(builder.encoder.encodeRequest_endpoints.first, AnyEndpoint(testEndpoint))
}
+
+ func test_standaloneUploadFileEncoderIsCalledWithEndpoint() throws {
+ let builder = TestBuilder()
+ let client = builder.make()
+
+ // Setup mock encoder response (it's not actually used, we just need to return something)
+ let request = URLRequest(url: .unique())
+ builder.encoder.encodeRequest = .success(request)
+
+ let payload = try Data(contentsOf: .localYodaImage)
+
+ // Create a test endpoint
+ let testEndpoint: Endpoint = .uploadAttachment(type: .image)
+
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: .localYodaImage,
+ state: .pendingUpload,
+ file: .init(type: .png, size: 120, mimeType: "image/png")
+ )
+
+ // Simulate file uploading
+ client.uploadStandaloneAttachment(
+ .mock(
+ payload: payload,
+ uploadingState: uploadingState
+ ),
+ progress: nil,
+ completion: { (_: Result) in }
+ )
+
+ // Check the encoder is called with the correct endpoint
+ XCTAssertEqual(builder.encoder.encodeRequest_endpoints.first, AnyEndpoint(testEndpoint))
+ }
func test_uploadFileEncoderFailingToEncode() throws {
let builder = TestBuilder()
@@ -47,6 +80,36 @@ final class StreamCDNClient_Tests: XCTestCase {
let testError = TestError()
builder.encoder.encodeRequest = .failure(testError)
+ let payload = try Data(contentsOf: .localYodaImage)
+
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: .localYodaImage,
+ state: .pendingUpload,
+ file: .init(type: .png, size: 120, mimeType: "image/png")
+ )
+
+ // Create a request and assert the result is failure
+ let result: Result = try waitFor {
+ client.uploadStandaloneAttachment(
+ .mock(
+ payload: payload,
+ uploadingState: uploadingState
+ ),
+ progress: nil,
+ completion: $0
+ )
+ }
+
+ XCTAssertEqual(result.error as? TestError, testError)
+ }
+
+ func test_uploadStandaloneFileEncoderFailingToEncode() throws {
+ let builder = TestBuilder()
+ let client = builder.make()
+ // Setup mock encoder response to fail with `testError`
+ let testError = TestError()
+ builder.encoder.encodeRequest = .failure(testError)
+
// Create a request and assert the result is failure
let result: Result = try waitFor {
client.uploadAttachment(
@@ -106,6 +169,50 @@ final class StreamCDNClient_Tests: XCTestCase {
// Check the outgoing data is from the decoder
XCTAssertEqual(try result.get().fileURL, payload.fileURL)
}
+
+ func test_standaloneUploadFileSuccess() throws {
+ let builder = TestBuilder()
+ let decoder = builder.decoder
+ let client = builder.make()
+
+ // Create a test request and set it as a response from the encoder
+ let testRequest = URLRequest(url: .unique())
+ builder.encoder.encodeRequest = .success(testRequest)
+
+ // Set up a successful mock network response for the request
+ let url = URL.unique()
+ let mockResponseData = try JSONEncoder.stream.encode(["file": url])
+ URLProtocol_Mock.mockResponse(request: testRequest, statusCode: 234, responseBody: mockResponseData)
+
+ let response = FileUploadPayload(fileURL: .unique(), thumbURL: .unique())
+ decoder.decodeRequestResponse = .success(response)
+
+ let payload = try Data(contentsOf: .localYodaImage)
+
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: .localYodaImage,
+ state: .pendingUpload,
+ file: .init(type: .png, size: 120, mimeType: "image/png")
+ )
+
+ // Create a request and assert the result is failure
+ let result: Result = try waitFor {
+ client.uploadStandaloneAttachment(
+ .mock(
+ payload: payload,
+ uploadingState: uploadingState
+ ),
+ progress: nil,
+ completion: $0
+ )
+ }
+
+ // Check the incoming data to the encoder is the URLResponse and data from the network
+ XCTAssertEqual(decoder.decodeRequestResponse_data, mockResponseData)
+ XCTAssertEqual(decoder.decodeRequestResponse_response?.statusCode, 234)
+
+ XCTAssertEqual(try result.get().fileURL, response.fileURL)
+ }
func test_uploadFileFailure() throws {
let builder = TestBuilder()
@@ -151,6 +258,56 @@ final class StreamCDNClient_Tests: XCTestCase {
// Check the outgoing data is from the decoder
XCTAssertEqual(result.error as? TestError, encoderError)
}
+
+ func test_standaloneUploadFileFailure() throws {
+ let builder = TestBuilder()
+ let client = builder.make()
+ let decoder = builder.decoder
+
+ // Create a test request and set it as a response from the encoder
+ let testRequest = URLRequest(url: .unique())
+ builder.encoder.encodeRequest = .success(testRequest)
+
+ // We cannot use `TestError` since iOS14 wraps this into another error
+ let networkError = NSError(domain: "TestNetworkError", code: -1, userInfo: nil)
+ let encoderError = TestError()
+
+ // Set up a mock network response from the request
+ URLProtocol_Mock.mockResponse(request: testRequest, statusCode: 444, error: networkError)
+
+ // Set up a decoder response to return `encoderError`
+ decoder.decodeRequestResponse = .failure(encoderError)
+
+ let payload = try Data(contentsOf: .localYodaImage)
+
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: .localYodaImage,
+ state: .pendingUpload,
+ file: .init(type: .png, size: 120, mimeType: "image/png")
+ )
+
+ // Create a request and wait for the completion block
+ let result: Result = try waitFor {
+ client.uploadStandaloneAttachment(
+ .mock(
+ payload: payload,
+ uploadingState: uploadingState
+ ),
+ progress: nil,
+ completion: $0
+ )
+ }
+
+ // Check the incoming error to the encoder is the error from the response
+ XCTAssertNotNil(decoder.decodeRequestResponse_error)
+
+ // We have to compare error codes, since iOS14 wraps network errors into `NSURLError`
+ // in which we cannot retrieve the wrapper error
+ XCTAssertEqual((decoder.decodeRequestResponse_error as NSError?)?.code, networkError.code)
+
+ // Check the outgoing data is from the decoder
+ XCTAssertEqual(result.error as? TestError, encoderError)
+ }
func test_callingUploadFile_createsNetworkRequest() throws {
let builder = TestBuilder()
diff --git a/Tests/StreamChatTests/Controllers/ChannelController/LivestreamChannelController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/LivestreamChannelController_Tests.swift
index 094c2485c17..7568c73229c 100644
--- a/Tests/StreamChatTests/Controllers/ChannelController/LivestreamChannelController_Tests.swift
+++ b/Tests/StreamChatTests/Controllers/ChannelController/LivestreamChannelController_Tests.swift
@@ -2299,6 +2299,183 @@ extension LivestreamChannelController_Tests {
XCTAssertEqual(controller.skippedMessagesAmount, 1)
XCTAssertTrue(controller.messages.isEmpty) // Message not added when paused
}
+
+ func test_didReceiveEvent_userMessagesDeletedEvent_hardDeleteFalse_marksUserMessagesAsSoftDeleted() {
+ // Given
+ let bannedUserId = UserId.unique
+ let otherUserId = UserId.unique
+ let eventCreatedAt = Date()
+
+ // Add messages from both users
+ let bannedUserMessage1 = ChatMessage.mock(
+ id: "banned1",
+ cid: controller.cid!,
+ text: "Message from banned user 1",
+ author: .mock(id: bannedUserId)
+ )
+ let bannedUserMessage2 = ChatMessage.mock(
+ id: "banned2",
+ cid: controller.cid!,
+ text: "Message from banned user 2",
+ author: .mock(id: bannedUserId)
+ )
+ let otherUserMessage = ChatMessage.mock(
+ id: "other",
+ cid: controller.cid!,
+ text: "Message from other user",
+ author: .mock(id: otherUserId)
+ )
+
+ // Add messages to controller
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: MessageNewEvent(
+ user: .mock(id: bannedUserId),
+ message: bannedUserMessage1,
+ channel: .mock(cid: controller.cid!),
+ createdAt: .unique,
+ watcherCount: nil,
+ unreadCount: nil
+ )
+ )
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: MessageNewEvent(
+ user: .mock(id: bannedUserId),
+ message: bannedUserMessage2,
+ channel: .mock(cid: controller.cid!),
+ createdAt: .unique,
+ watcherCount: nil,
+ unreadCount: nil
+ )
+ )
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: MessageNewEvent(
+ user: .mock(id: otherUserId),
+ message: otherUserMessage,
+ channel: .mock(cid: controller.cid!),
+ createdAt: .unique,
+ watcherCount: nil,
+ unreadCount: nil
+ )
+ )
+
+ XCTAssertEqual(controller.messages.count, 3)
+ XCTAssertNil(controller.messages.first { $0.id == "banned1" }?.deletedAt)
+ XCTAssertNil(controller.messages.first { $0.id == "banned2" }?.deletedAt)
+ XCTAssertNil(controller.messages.first { $0.id == "other" }?.deletedAt)
+
+ // When
+ let userMessagesDeletedEvent = UserMessagesDeletedEvent(
+ user: .mock(id: bannedUserId),
+ hardDelete: false,
+ createdAt: eventCreatedAt
+ )
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: userMessagesDeletedEvent
+ )
+
+ // Then
+ XCTAssertEqual(controller.messages.count, 3) // Messages still present
+
+ // Banned user messages should be marked as deleted
+ XCTAssertEqual(controller.messages.first { $0.id == "banned1" }?.deletedAt, eventCreatedAt)
+ XCTAssertEqual(controller.messages.first { $0.id == "banned2" }?.deletedAt, eventCreatedAt)
+
+ // Other user message should not be affected
+ XCTAssertNil(controller.messages.first { $0.id == "other" }?.deletedAt)
+ }
+
+ func test_didReceiveEvent_userMessagesDeletedEvent_hardDeleteTrue_removesUserMessages() {
+ // Given
+ let bannedUserId = UserId.unique
+ let otherUserId = UserId.unique
+ let eventCreatedAt = Date()
+
+ // Add messages from both users
+ let bannedUserMessage1 = ChatMessage.mock(
+ id: "banned1",
+ cid: controller.cid!,
+ text: "Message from banned user 1",
+ author: .mock(id: bannedUserId)
+ )
+ let bannedUserMessage2 = ChatMessage.mock(
+ id: "banned2",
+ cid: controller.cid!,
+ text: "Message from banned user 2",
+ author: .mock(id: bannedUserId)
+ )
+ let otherUserMessage = ChatMessage.mock(
+ id: "other",
+ cid: controller.cid!,
+ text: "Message from other user",
+ author: .mock(id: otherUserId)
+ )
+
+ // Add messages to controller
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: MessageNewEvent(
+ user: .mock(id: bannedUserId),
+ message: bannedUserMessage1,
+ channel: .mock(cid: controller.cid!),
+ createdAt: .unique,
+ watcherCount: nil,
+ unreadCount: nil
+ )
+ )
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: MessageNewEvent(
+ user: .mock(id: bannedUserId),
+ message: bannedUserMessage2,
+ channel: .mock(cid: controller.cid!),
+ createdAt: .unique,
+ watcherCount: nil,
+ unreadCount: nil
+ )
+ )
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: MessageNewEvent(
+ user: .mock(id: otherUserId),
+ message: otherUserMessage,
+ channel: .mock(cid: controller.cid!),
+ createdAt: .unique,
+ watcherCount: nil,
+ unreadCount: nil
+ )
+ )
+
+ XCTAssertEqual(controller.messages.count, 3)
+ XCTAssertNotNil(controller.messages.first { $0.id == "banned1" })
+ XCTAssertNotNil(controller.messages.first { $0.id == "banned2" })
+ XCTAssertNotNil(controller.messages.first { $0.id == "other" })
+
+ // When
+ let userMessagesDeletedEvent = UserMessagesDeletedEvent(
+ user: .mock(id: bannedUserId),
+ hardDelete: true,
+ createdAt: eventCreatedAt
+ )
+ controller.eventsController(
+ EventsController(notificationCenter: client.eventNotificationCenter),
+ didReceiveEvent: userMessagesDeletedEvent
+ )
+
+ // Then
+ XCTAssertEqual(controller.messages.count, 1) // Only other user's message remains
+
+ // Banned user messages should be completely removed
+ XCTAssertNil(controller.messages.first { $0.id == "banned1" })
+ XCTAssertNil(controller.messages.first { $0.id == "banned2" })
+
+ // Other user message should remain unaffected
+ XCTAssertNotNil(controller.messages.first { $0.id == "other" })
+ XCTAssertNil(controller.messages.first { $0.id == "other" }?.deletedAt)
+ }
}
// MARK: - Message CRUD Tests
diff --git a/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift b/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift
index e8b6d9b7d82..7881aaaf752 100644
--- a/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift
+++ b/Tests/StreamChatTests/Database/DatabaseSession_Tests.swift
@@ -121,7 +121,8 @@ final class DatabaseSession_Tests: XCTestCase {
createdAt: nil,
isChannelHistoryCleared: false,
banReason: nil,
- banExpiredAt: nil
+ banExpiredAt: nil,
+ channelMessageCount: 5
)
// Save the event payload to DB
@@ -139,6 +140,7 @@ final class DatabaseSession_Tests: XCTestCase {
let loadedChannel: ChatChannel = try XCTUnwrap(database.viewContext.channel(cid: channelId)?.asModel())
let message = try XCTUnwrap(loadedMessage)
XCTAssert(loadedChannel.latestMessages.contains(message))
+ XCTAssertEqual(loadedChannel.messageCount, 5)
}
func test_deleteMessage() throws {
diff --git a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
index 79a9a1af8f3..b8265005626 100644
--- a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
+++ b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
@@ -343,6 +343,65 @@ final class ChannelList_Tests: XCTestCase {
cancellable.cancel()
}
+ func test_updatingChannels_whenBlockedAndUnblockedWithHiddenQuery() async throws {
+ await setUpChannelList(
+ usesMockedChannelUpdater: false,
+ filter: .and([
+ .containMembers(userIds: [memberId]),
+ .or([
+ .and([.equal(.blocked, to: false), .equal(.hidden, to: false)]),
+ .and([.equal(.blocked, to: true), .equal(.hidden, to: true)])
+ ])
+ ])
+ )
+
+ // Get initial batch of channels where 2 are blocked, 3 are not
+ let blockedOffsets = Set([1, 3])
+ let hiddenOffsets = Set([1, 3])
+ let firstChannelListPayload = makeMatchingChannelListPayload(
+ channelCount: 5,
+ createdAtOffset: 0,
+ blocked: { _, offset in blockedOffsets.contains(offset) },
+ hidden: { _, offset in hiddenOffsets.contains(offset) }
+ )
+ env.client.mockAPIClient.test_mockResponseResult(.success(firstChannelListPayload))
+ try await channelList.get()
+ await XCTAssertEqual(5, channelList.state.channels.count)
+ await XCTAssertEqual(2, channelList.state.channels.filter(\.isBlocked).count)
+ await XCTAssertEqual(2, channelList.state.channels.filter(\.isHidden).count)
+
+ // Send an event which does not include blocked and hidden states, but contains channel info
+ // Channel gets saved to DB and should not change blocked and hidden states.
+ let secondChannel = firstChannelListPayload.channels[1].channel
+ let channelPayloadWithoutBlockedAndHidden = ChannelDetailPayload.dummy(
+ cid: secondChannel.cid,
+ isBlocked: nil,
+ isHidden: nil,
+ members: secondChannel.members ?? []
+ )
+ let eventPayload = EventPayload(
+ eventType: .notificationMarkRead,
+ cid: channelPayloadWithoutBlockedAndHidden.cid,
+ user: .dummy(userId: memberId),
+ channel: channelPayloadWithoutBlockedAndHidden,
+ unreadCount: .init(channels: 0, messages: 0, threads: 0),
+ createdAt: Date()
+ )
+ let notificationMarkReadEvent = try NotificationMarkReadEventDTO(from: eventPayload)
+ let expectation = XCTestExpectation()
+ env.client.eventNotificationCenter.process(notificationMarkReadEvent, postNotification: true) {
+ expectation.fulfill()
+ }
+ await fulfillment(of: [expectation])
+
+ let secondChannelDataAfterEvent = try env.client.databaseContainer.readSynchronously { session in
+ try XCTUnwrap(session.channel(cid: secondChannel.cid)).asModel()
+ }
+ XCTAssertEqual(true, secondChannelDataAfterEvent.isBlocked, "State did not change")
+ XCTAssertEqual(true, secondChannelDataAfterEvent.isHidden, "State did not change")
+ await XCTAssertEqual(5, channelList.state.channels.count)
+ }
+
// MARK: - Linking and Unlinking Channels
func test_observingEvents_whenAddedToChannelEventReceived_thenChannelIsLinkedAndStateUpdates() async throws {
@@ -487,7 +546,9 @@ final class ChannelList_Tests: XCTestCase {
createdAtOffset: Int,
namePrefix: String = "Name",
membersCreator: ((ChannelId, Int) -> [MemberPayload])? = nil,
- messagesCreator: ((ChannelId, Int) -> [MessagePayload])? = nil
+ messagesCreator: ((ChannelId, Int) -> [MessagePayload])? = nil,
+ blocked: ((ChannelId, Int) -> Bool) = { _, _ in false },
+ hidden: ((ChannelId, Int) -> Bool) = { _, _ in false }
) -> ChannelListPayload {
let channelPayloads = (0..