diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift index c03134f41c7..0798b79c82d 100644 --- a/Sources/StreamChat/APIClient/APIClient.swift +++ b/Sources/StreamChat/APIClient/APIClient.swift @@ -82,7 +82,7 @@ class APIClient: @unchecked Sendable { /// - Parameters: /// - endpoint: The `Endpoint` used to create the network request. /// - completion: Called when the networking request is finished. - func request( + func request( endpoint: Endpoint, completion: @escaping @Sendable (Result) -> Void ) { @@ -95,7 +95,7 @@ class APIClient: @unchecked Sendable { /// - Parameters: /// - endpoint: The `Endpoint` used to create the network request. /// - completion: Called when the networking request is finished. - func recoveryRequest( + func recoveryRequest( endpoint: Endpoint, completion: @escaping @Sendable (Result) -> Void ) { @@ -113,7 +113,7 @@ class APIClient: @unchecked Sendable { /// - Parameters: /// - endpoint: The `Endpoint` used to create the network request. /// - completion: Called when the networking request is finished. - func unmanagedRequest( + func unmanagedRequest( endpoint: Endpoint, completion: @escaping @Sendable (Result) -> Void ) { @@ -122,7 +122,7 @@ class APIClient: @unchecked Sendable { ) } - private func operation( + private func operation( endpoint: Endpoint, isRecoveryOperation: Bool, completion: @escaping @Sendable (Result) -> Void @@ -192,7 +192,7 @@ class APIClient: @unchecked Sendable { } } - private func unmanagedOperation( + private func unmanagedOperation( endpoint: Endpoint, completion: @escaping @Sendable (Result) -> Void ) -> AsyncOperation { @@ -222,7 +222,7 @@ class APIClient: @unchecked Sendable { /// - Parameters: /// - endpoint: The `Endpoint` used to create the network request. /// - completion: Called when the networking request is finished. - private func executeRequest( + private func executeRequest( endpoint: Endpoint, completion: @escaping @Sendable (Result) -> Void ) { diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiablePayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiablePayload.swift index 9b10cb09546..874e5f03575 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiablePayload.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiablePayload.swift @@ -58,18 +58,16 @@ extension IdentifiablePayload { guard let modelClass = modelClass, let keyPath = modelClass.idKeyPath else { continue } let values = Array(identifiableValues) - var results: [NSManagedObject]? + nonisolated(unsafe) var modelMapping: [DatabaseId: NSManagedObjectID] = [:] context.performAndWait { - results = modelClass.batchFetch(keyPath: keyPath, equalTo: values, context: context) - } - guard let results = results else { continue } - - var modelMapping: [DatabaseId: NSManagedObjectID] = [:] - results.forEach { - if let id = modelClass.id(for: $0) { - modelMapping[id] = $0.objectID + let results = modelClass.batchFetch(keyPath: keyPath, equalTo: values, context: context) + results.forEach { + if let id = modelClass.id(for: $0) { + modelMapping[id] = $0.objectID + } } } + cache[modelClass.className] = modelMapping } diff --git a/Sources/StreamChat/APIClient/RequestEncoder.swift b/Sources/StreamChat/APIClient/RequestEncoder.swift index 8464b0d3285..d14bb5695be 100644 --- a/Sources/StreamChat/APIClient/RequestEncoder.swift +++ b/Sources/StreamChat/APIClient/RequestEncoder.swift @@ -85,7 +85,7 @@ class DefaultRequestEncoder: RequestEncoder, @unchecked Sendable { private let waiterTimeout: TimeInterval = 10 weak var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate? - func encodeRequest( + func encodeRequest( for endpoint: Endpoint, completion: @escaping @Sendable (Result) -> Void ) { diff --git a/Sources/StreamChat/Audio/AudioSessionConfiguring.swift b/Sources/StreamChat/Audio/AudioSessionConfiguring.swift index 67644c3b11f..35e9ead3783 100644 --- a/Sources/StreamChat/Audio/AudioSessionConfiguring.swift +++ b/Sources/StreamChat/Audio/AudioSessionConfiguring.swift @@ -74,9 +74,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring, @unchecked S mode: .spokenAudio, policy: .default, options: [ - // It is deprecated, but for now we need to use it, - // since the newer ones are not available in Xcode 15. - .allowBluetooth + .allowBluetoothDevice ] ) try activateSession() @@ -100,9 +98,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring, @unchecked S policy: .default, options: [ .defaultToSpeaker, - // It is deprecated, but for now we need to use it, - // since the newer ones are not available in Xcode 15. - .allowBluetooth + .allowBluetoothDevice ] ) try activateSession() @@ -177,3 +173,13 @@ final class AudioSessionConfiguratorError: ClientError, @unchecked Sendable { .init("No available audio inputs found.", file, line) } } + +// MARK: - + +extension AVAudioSession.CategoryOptions { + #if compiler(>=6.2) + static let allowBluetoothDevice: AVAudioSession.CategoryOptions = .allowBluetoothHFP + #else + static let allowBluetoothDevice: AVAudioSession.CategoryOptions = .allowBluetooth + #endif +} diff --git a/Sources/StreamChat/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift index 4ae8959aa69..ab03cf8e388 100644 --- a/Sources/StreamChat/ChatClient+Environment.swift +++ b/Sources/StreamChat/ChatClient+Environment.swift @@ -110,7 +110,7 @@ extension ChatClient { } } - var timerType: TimerScheduling.Type = DefaultTimer.self + nonisolated(unsafe) var timerType: TimerScheduling.Type = DefaultTimer.self var tokenExpirationRetryStrategy: RetryStrategy = DefaultRetryStrategy() diff --git a/Sources/StreamChat/Config/ChatClientConfig.swift b/Sources/StreamChat/Config/ChatClientConfig.swift index eebb7059f69..19907b559f0 100644 --- a/Sources/StreamChat/Config/ChatClientConfig.swift +++ b/Sources/StreamChat/Config/ChatClientConfig.swift @@ -115,30 +115,6 @@ public struct ChatClientConfig: Sendable { /// A component that can be used to change an attachment which was successfully uploaded. public var uploadedAttachmentPostProcessor: UploadedAttachmentPostProcessor? - /// Returns max possible attachment size in bytes. - /// By default the value is taken from `CDNClient.maxAttachmentSize` type. - /// But it can be overridden by setting a value here. - @available(*, deprecated, message: "The max attachment size can now be set from the Stream's Dashboard App Settings. It supports setting a size limit per attachment type.") - public var maxAttachmentSize: Int64 { - // TODO: For v5 the maxAttachmentSize should be responsibility of the UI SDK. - // Since this is not even used in the StreamChat LLC SDK. - get { - if let overrideMaxAttachmentSize = self.overrideMaxAttachmentSize { - return overrideMaxAttachmentSize - } else if let customCDNClient = customCDNClient { - return type(of: customCDNClient).maxAttachmentSize - } else { - return StreamCDNClient.maxAttachmentSize - } - } - set { - overrideMaxAttachmentSize = newValue - } - } - - /// Used to override the maxAttachmentSize, by setting the value in the config instead of relying on `CDNClient`. - private var overrideMaxAttachmentSize: Int64? - /// Returns max number of attachments that can be attached to a message. /// /// The current limit on the backend is `30`. You can only configure a value below `30`. diff --git a/Sources/StreamChat/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift index 162bdaa8195..f512e77d257 100644 --- a/Sources/StreamChat/Database/DatabaseContainer.swift +++ b/Sources/StreamChat/Database/DatabaseContainer.swift @@ -270,10 +270,10 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } } - func readAndWait(_ actions: (DatabaseSession) throws -> T) throws -> T { + func readAndWait(_ actions: @Sendable (DatabaseSession) throws -> T) throws -> T where T: Sendable { let context = backgroundReadOnlyContext - var result: T? - var readError: Error? + nonisolated(unsafe) var result: T? + nonisolated(unsafe) var readError: Error? context.performAndWait { do { result = try actions(context) @@ -289,7 +289,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable { } /// Removes all data from the local storage. - func removeAllData(completion: ((Error?) -> Void)? = nil) { + func removeAllData(completion: (@Sendable (Error?) -> Void)? = nil) { let entityNames = managedObjectModel.entities.compactMap(\.name) writableContext.perform { [weak self] in let requests = entityNames @@ -487,8 +487,9 @@ extension NSManagedObjectContext { queue: nil ) { [weak self] notification in guard let self else { return } + nonisolated(unsafe) let unsafeNotification = notification self.performAndWait { - self.mergeChanges(fromContextDidSave: notification) + self.mergeChanges(fromContextDidSave: unsafeNotification) // Keep the state clean after merging changes guard self.hasChanges else { return } self.perform { diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift index 83dddbe65e5..cd627a18231 100644 --- a/Sources/StreamChat/Models/ChatMessage.swift +++ b/Sources/StreamChat/Models/ChatMessage.swift @@ -595,7 +595,6 @@ public extension ChatMessage { } extension ChatMessage: Hashable { - // swiftlint:disable cyclomatic_complexity public static func == (lhs: Self, rhs: Self) -> Bool { guard lhs.id == rhs.id else { return false } guard lhs.localState == rhs.localState else { return false } @@ -629,8 +628,6 @@ extension ChatMessage: Hashable { return true } - // swiftlint:enable cyclomatic_complexity - public func hash(into hasher: inout Hasher) { hasher.combine(id) } diff --git a/Sources/StreamChat/Models/Thread.swift b/Sources/StreamChat/Models/Thread.swift index 2338051e0b2..a245ae58cdb 100644 --- a/Sources/StreamChat/Models/Thread.swift +++ b/Sources/StreamChat/Models/Thread.swift @@ -38,3 +38,23 @@ public struct ChatThread: Identifiable, Sendable { /// The custom data of the thread. public let extraData: [String: RawJSON] } + +extension ChatThread: Hashable { + public static func == (lhs: ChatThread, rhs: ChatThread) -> Bool { + lhs.parentMessageId == rhs.parentMessageId && + lhs.updatedAt == rhs.updatedAt && + lhs.title == rhs.title && + lhs.reads == rhs.reads && + lhs.latestReplies == rhs.latestReplies && + lhs.lastMessageAt == rhs.lastMessageAt && + lhs.channel == rhs.channel && + lhs.participantCount == rhs.participantCount && + lhs.replyCount == rhs.replyCount && + lhs.threadParticipants == rhs.threadParticipants && + lhs.extraData == rhs.extraData + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(parentMessageId) + } +} diff --git a/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift b/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift index f8bd590b171..4a1e12d2762 100644 --- a/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift +++ b/Sources/StreamChat/StateLayer/DatabaseObserver/StateLayerDatabaseObserver.swift @@ -16,7 +16,7 @@ class ListResult: DatabaseObserverType {} /// A CoreData store observer which immediately reports changes as soon as the store has been changed. /// /// - Note: Requires the ``DatabaseContainer/stateLayerContext`` which is immediately synchronized. -final class StateLayerDatabaseObserver { +final class StateLayerDatabaseObserver: @unchecked Sendable { private let changeAggregator: ListChangeAggregator private let frc: NSFetchedResultsController let itemCreator: (DTO) throws -> Item @@ -67,7 +67,7 @@ extension StateLayerDatabaseObserver where ResultType == EntityResult { } var item: Item? { - var item: Item? + nonisolated(unsafe) var item: Item? context.performAndWait { item = Self.makeEntity( frc: frc, @@ -154,7 +154,7 @@ extension StateLayerDatabaseObserver where ResultType == ListResult { } var items: [Item] { - var collection: [Item]! + nonisolated(unsafe) var collection: [Item]! context.performAndWait { // When we already have loaded items, reuse them, otherwise refetch all let items = reuseItems ?? updateItems(nil) diff --git a/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift b/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift index 97898f92893..3ba55866986 100644 --- a/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift +++ b/Sources/StreamChat/Workers/Background/AttachmentQueueUploader.swift @@ -22,7 +22,7 @@ import Foundation class AttachmentQueueUploader: Worker, @unchecked Sendable { @Atomic private var pendingAttachmentIDs: Set = [] - private let observer: StateLayerDatabaseObserver + private let observer: StateLayerDatabaseObserver private let attachmentPostProcessor: UploadedAttachmentPostProcessor? private let attachmentUpdater = AnyAttachmentUpdater() private let attachmentStorage = AttachmentStorage() @@ -35,8 +35,10 @@ class AttachmentQueueUploader: Worker, @unchecked Sendable { init(database: DatabaseContainer, apiClient: APIClient, attachmentPostProcessor: UploadedAttachmentPostProcessor?) { observer = StateLayerDatabaseObserver( - context: database.backgroundReadOnlyContext, - fetchRequest: AttachmentDTO.pendingUploadFetchRequest() + database: database, + fetchRequest: AttachmentDTO.pendingUploadFetchRequest(), + itemCreator: { $0.attachmentID ?? AttachmentId(cid: ChannelId(type: .messaging, id: ""), messageId: "", index: -1) }, + itemReuseKeyPaths: nil ) self.attachmentPostProcessor = attachmentPostProcessor @@ -67,7 +69,7 @@ class AttachmentQueueUploader: Worker, @unchecked Sendable { } } - private func handleChanges(changes: [ListChange]) { + private func handleChanges(changes: [ListChange]) { guard !changes.isEmpty else { return } // Only start uploading attachment when inserted and it is present in pendingAttachmentIds @@ -282,12 +284,12 @@ class AttachmentQueueUploader: Worker, @unchecked Sendable { } } -private extension Array where Element == ListChange { +private extension Array where Element == ListChange { var attachmentIDs: [AttachmentId] { compactMap { switch $0 { - case let .insert(dto, _), let .update(dto, _): - return dto.attachmentID + case let .insert(id, _), let .update(id, _): + return id.messageId.isEmpty ? nil : id case .move, .remove: return nil } diff --git a/Sources/StreamChat/Workers/Background/MessageSender.swift b/Sources/StreamChat/Workers/Background/MessageSender.swift index 3e41aab0fb3..1ce9704f259 100644 --- a/Sources/StreamChat/Workers/Background/MessageSender.swift +++ b/Sources/StreamChat/Workers/Background/MessageSender.swift @@ -22,10 +22,23 @@ class MessageSender: Worker, @unchecked Sendable { @Atomic private var sendingQueueByCid: [ChannelId: MessageSendingQueue] = [:] private var continuations = [MessageId: CheckedContinuation]() - private lazy var observer = StateLayerDatabaseObserver( - context: self.database.backgroundReadOnlyContext, - fetchRequest: MessageDTO - .messagesPendingSendFetchRequest() + private lazy var observer = StateLayerDatabaseObserver( + database: database, + fetchRequest: MessageDTO.messagesPendingSendFetchRequest(), + itemCreator: { dto in + let cid: ChannelId = { + if let rawValue = dto.channel?.cid, let cid = try? ChannelId(cid: rawValue) { + return cid + } + return ChannelId(type: .messaging, id: "") + }() + return .init( + messageId: dto.id, + cid: cid, + createdLocallyAt: (dto.locallyCreatedAt ?? dto.createdAt).bridgeDate + ) + }, + itemReuseKeyPaths: nil ) private let sendingDispatchQueue: DispatchQueue = .init( @@ -71,26 +84,17 @@ class MessageSender: Worker, @unchecked Sendable { } } - func handleChanges(changes: [ListChange]) { + private func handleChanges(changes: [ListChange]) { // Convert changes to a dictionary of requests by their cid nonisolated(unsafe) var newRequests: [ChannelId: [MessageSendingQueue.SendRequest]] = [:] changes.forEach { change in switch change { - case .insert(let dto, index: _), .update(let dto, index: _): - database.backgroundReadOnlyContext.performAndWait { - guard let cid = dto.channel.map({ try? ChannelId(cid: $0.cid) }) else { - log.error("Skipping sending of the message \(dto.id) because the channel info is missing.") - return - } - // Create the array if it didn't exist - guard let cid = cid else { return } - newRequests[cid] = newRequests[cid] ?? [] - newRequests[cid]!.append(.init( - messageId: dto.id, - cid: cid, - createdLocallyAt: (dto.locallyCreatedAt ?? dto.createdAt).bridgeDate - )) - } + case .insert(let request, index: _), .update(let request, index: _): + // Create the array if it didn't exist + let cid = request.cid + guard !cid.id.isEmpty else { return } + newRequests[cid] = newRequests[cid] ?? [] + newRequests[cid]!.append(request) case .move, .remove: break } diff --git a/Sources/StreamChat/Workers/ChannelListLinker.swift b/Sources/StreamChat/Workers/ChannelListLinker.swift index 061482e6497..01ca514c738 100644 --- a/Sources/StreamChat/Workers/ChannelListLinker.swift +++ b/Sources/StreamChat/Workers/ChannelListLinker.swift @@ -63,7 +63,7 @@ final class ChannelListLinker: Sendable { transform: { $0 as? ChannelVisibleEvent }, callback: { [weak self, databaseContainer] event in let context = databaseContainer.backgroundReadOnlyContext - context.perform { + context.perform { [self] in guard let channel = try? context.channel(cid: event.cid)?.asModel() else { return } self?.linkChannelIfNeeded(channel) } @@ -74,7 +74,7 @@ final class ChannelListLinker: Sendable { private func isInChannelList( _ channel: ChatChannel, - completion: @escaping (_ isPresent: Bool, _ belongsToOtherQuery: Bool) -> Void + completion: @escaping @Sendable (_ isPresent: Bool, _ belongsToOtherQuery: Bool) -> Void ) { let context = databaseContainer.backgroundReadOnlyContext context.performAndWait { [weak self] in diff --git a/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift b/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift index cd1f961bcd0..9bad1442dd3 100644 --- a/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelMemberListUpdater.swift @@ -89,7 +89,7 @@ private extension ChannelMemberListUpdater { } } - func checkChannelExistsLocally(with cid: ChannelId, completion: @escaping (Bool) -> Void) { + func checkChannelExistsLocally(with cid: ChannelId, completion: @escaping @Sendable (Bool) -> Void) { let context = database.backgroundReadOnlyContext context.perform { let exists = context.channel(cid: cid) != nil diff --git a/Sources/StreamChat/Workers/MessageUpdater.swift b/Sources/StreamChat/Workers/MessageUpdater.swift index 6e442987fd4..fd5b441bb45 100644 --- a/Sources/StreamChat/Workers/MessageUpdater.swift +++ b/Sources/StreamChat/Workers/MessageUpdater.swift @@ -1115,7 +1115,7 @@ private extension MessageUpdater { } } - func checkMessageExistsLocally(_ messageId: MessageId, completion: @escaping (Bool) -> Void) { + func checkMessageExistsLocally(_ messageId: MessageId, completion: @escaping @Sendable (Bool) -> Void) { let context = database.backgroundReadOnlyContext context.perform { let exists = context.message(id: messageId) != nil diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift index c8516067eb9..8b798866d96 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift @@ -7,7 +7,7 @@ import Foundation /// A typealias of `Set` to make the API similar of an `OptionSet`. public typealias ChatMessageLayoutOptions = Set -extension ChatMessageLayoutOptions: Identifiable { +extension ChatMessageLayoutOptions: @retroactive Identifiable { /// The id is composed by the raw values of each option joined by "-". /// This id is then used to compute the reuse identifier of each message cell. public var id: String { diff --git a/Sources/StreamChatUI/ChatThreadList/ChatThreadListVC.swift b/Sources/StreamChatUI/ChatThreadList/ChatThreadListVC.swift index 6088b89c1fa..de33767dd12 100644 --- a/Sources/StreamChatUI/ChatThreadList/ChatThreadListVC.swift +++ b/Sources/StreamChatUI/ChatThreadList/ChatThreadListVC.swift @@ -364,31 +364,12 @@ open class ChatThreadListVC: } } -extension ChatThread: Differentiable, Equatable, Hashable { - public static func == (lhs: ChatThread, rhs: ChatThread) -> Bool { - lhs.parentMessageId == rhs.parentMessageId && - lhs.updatedAt == rhs.updatedAt && - lhs.parentMessage.isContentEqual(to: rhs.parentMessage) && - lhs.title == rhs.title && - lhs.reads == rhs.reads && - lhs.latestReplies == rhs.latestReplies && - lhs.lastMessageAt == rhs.lastMessageAt && - lhs.channel == rhs.channel && - lhs.participantCount == rhs.participantCount && - lhs.replyCount == rhs.replyCount && - lhs.threadParticipants == rhs.threadParticipants && - lhs.extraData == rhs.extraData - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(parentMessageId) - } - +extension ChatThread: Differentiable { public var differenceIdentifier: Int { hashValue } public func isContentEqual(to source: ChatThread) -> Bool { - self == source + self == source && parentMessage.isContentEqual(to: source.parentMessage) } } diff --git a/Sources/StreamChatUI/CommonViews/ListCollectionViewLayout/ListCollectionViewLayout.swift b/Sources/StreamChatUI/CommonViews/ListCollectionViewLayout/ListCollectionViewLayout.swift index 4fd5429a120..12d313f1620 100644 --- a/Sources/StreamChatUI/CommonViews/ListCollectionViewLayout/ListCollectionViewLayout.swift +++ b/Sources/StreamChatUI/CommonViews/ListCollectionViewLayout/ListCollectionViewLayout.swift @@ -89,8 +89,7 @@ open class ListCollectionViewLayout: UICollectionViewFlowLayout { private func separatorLayoutAttributes( forCellLayoutAttributes cellAttributes: [UICollectionViewLayoutAttributes] ) -> [UICollectionViewLayoutAttributes] { - guard let collectionView = collectionView else { return [] } - let delegate = collectionView.delegate as? ListCollectionViewLayoutDelegate + guard collectionView != nil else { return [] } return cellAttributes.compactMap { cellAttribute in guard cellAttribute.representedElementCategory == .cell else { return nil } return separatorLayoutAttributes(forCellFrame: cellAttribute.frame, indexPath: cellAttribute.indexPath) diff --git a/Sources/StreamChatUI/Composer/ComposerVC.swift b/Sources/StreamChatUI/Composer/ComposerVC.swift index e7425721a8d..482d49bc43e 100644 --- a/Sources/StreamChatUI/Composer/ComposerVC.swift +++ b/Sources/StreamChatUI/Composer/ComposerVC.swift @@ -1506,9 +1506,13 @@ open class ComposerVC: _ViewController, } // If no value is set in the dashboard, the size_limit will be nil or zero, - // so in this case we fallback to the deprecated value. + // so in this case we fallback to the default value. guard let maxSize = maxAttachmentSize, maxSize > 0 else { - return client.config.maxAttachmentSize + if let customCDNClient = client.config.customCDNClient { + return type(of: customCDNClient).maxAttachmentSize + } else { + return AttachmentValidationError.fileSizeMaxLimitFallback + } } return maxSize diff --git a/Sources/StreamChatUI/Gallery/ZoomAnimator.swift b/Sources/StreamChatUI/Gallery/ZoomAnimator.swift index 1b8caef2d96..b811db5d257 100644 --- a/Sources/StreamChatUI/Gallery/ZoomAnimator.swift +++ b/Sources/StreamChatUI/Gallery/ZoomAnimator.swift @@ -71,12 +71,14 @@ open class ZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning { toVC.view.alpha = 1 }) }, completion: { _ in - fromImageView.isHidden = false - self.transitionImageView?.removeFromSuperview() - self.transitionImageView = nil - backgroundColorView.removeFromSuperview() - - transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + StreamConcurrency.onMain { + fromImageView.isHidden = false + self.transitionImageView?.removeFromSuperview() + self.transitionImageView = nil + backgroundColorView.removeFromSuperview() + + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } }) } diff --git a/Sources/StreamChatUI/StreamNuke/Nuke/Caching/Cache.swift b/Sources/StreamChatUI/StreamNuke/Nuke/Caching/Cache.swift index 27365a7bf29..7da85af29ca 100644 --- a/Sources/StreamChatUI/StreamNuke/Nuke/Caching/Cache.swift +++ b/Sources/StreamChatUI/StreamNuke/Nuke/Caching/Cache.swift @@ -9,7 +9,7 @@ import UIKit.UIApplication #endif // Internal memory-cache implementation. -final class NukeCache: @unchecked Sendable { +final class NukeCache: @unchecked Sendable { // Can't use `NSCache` because it is not LRU struct Configuration { diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index fcccf7e9345..78285dd82ef 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -431,10 +431,10 @@ 4FC7445B2D8D9A2600E314EB /* ImageProcessors+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC743D92D8D9A2600E314EB /* ImageProcessors+Resize.swift */; }; 4FC7445C2D8D9A2600E314EB /* ImageDecompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC743D02D8D9A2600E314EB /* ImageDecompression.swift */; }; 4FC7445D2D8D9A2600E314EB /* Graphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC743BC2D8D9A2600E314EB /* Graphics.swift */; }; - 4FCB7DF52EB229BB00908631 /* StreamCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB7DF42EB229B400908631 /* StreamCore+Extensions.swift */; }; - 4FCB7DF62EB229BB00908631 /* StreamCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB7DF42EB229B400908631 /* StreamCore+Extensions.swift */; }; 4FC7B3F02ED86E3000246903 /* MarkUnreadPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC7B3EF2ED86E1E00246903 /* MarkUnreadPayload.swift */; }; 4FC7B3F12ED86E3000246903 /* MarkUnreadPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC7B3EF2ED86E1E00246903 /* MarkUnreadPayload.swift */; }; + 4FCB7DF52EB229BB00908631 /* StreamCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB7DF42EB229B400908631 /* StreamCore+Extensions.swift */; }; + 4FCB7DF62EB229BB00908631 /* StreamCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB7DF42EB229B400908631 /* StreamCore+Extensions.swift */; }; 4FCCACE42BC939EB009D23E1 /* MemberList_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCCACE32BC939EB009D23E1 /* MemberList_Tests.swift */; }; 4FD2BE502B99F68300FFC6F2 /* ReadStateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD2BE4F2B99F68300FFC6F2 /* ReadStateHandler.swift */; }; 4FD2BE512B99F68300FFC6F2 /* ReadStateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD2BE4F2B99F68300FFC6F2 /* ReadStateHandler.swift */; }; @@ -912,7 +912,6 @@ 84F373EC280D803E0081E8BA /* TestChannelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F373EB280D803E0081E8BA /* TestChannelObserver.swift */; }; 84F373EE280D95690081E8BA /* ChatMessageDeliveryStatusCheckmarkView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F373ED280D95690081E8BA /* ChatMessageDeliveryStatusCheckmarkView_Tests.swift */; }; 84F373F0280D95990081E8BA /* ChatMessageDeliveryStatusView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F373EF280D95990081E8BA /* ChatMessageDeliveryStatusView_Tests.swift */; }; - 84F61270268B415C00DDF6EE /* ChatClientConfig_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6126F268B415C00DDF6EE /* ChatClientConfig_Tests.swift */; }; 84FD350827FD8BE300D68D85 /* ChatChannel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FD350727FD8BE300D68D85 /* ChatChannel_Tests.swift */; }; 8800A26F258A04D5006D64C4 /* ChatMessageAttachmentPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8800A26E258A04D5006D64C4 /* ChatMessageAttachmentPreviewVC.swift */; }; 8800A28C258A1924006D64C4 /* ChatMessageFileAttachmentListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8800A28B258A1924006D64C4 /* ChatMessageFileAttachmentListView.swift */; }; @@ -3347,8 +3346,8 @@ 4FC743E72D8D9A2600E314EB /* ImageTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTask.swift; sourceTree = ""; }; 4FC743E92D8D9A2600E314EB /* ImageLoadingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoadingOptions.swift; sourceTree = ""; }; 4FC743EA2D8D9A2600E314EB /* ImageViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewExtensions.swift; sourceTree = ""; }; - 4FCB7DF42EB229B400908631 /* StreamCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StreamCore+Extensions.swift"; sourceTree = ""; }; 4FC7B3EF2ED86E1E00246903 /* MarkUnreadPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkUnreadPayload.swift; sourceTree = ""; }; + 4FCB7DF42EB229B400908631 /* StreamCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StreamCore+Extensions.swift"; sourceTree = ""; }; 4FCCACE32BC939EB009D23E1 /* MemberList_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberList_Tests.swift; sourceTree = ""; }; 4FD2BE4F2B99F68300FFC6F2 /* ReadStateHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadStateHandler.swift; sourceTree = ""; }; 4FD2BE552B9AF8A300FFC6F2 /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = ""; }; @@ -3821,7 +3820,6 @@ 84F373EB280D803E0081E8BA /* TestChannelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestChannelObserver.swift; sourceTree = ""; }; 84F373ED280D95690081E8BA /* ChatMessageDeliveryStatusCheckmarkView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageDeliveryStatusCheckmarkView_Tests.swift; sourceTree = ""; }; 84F373EF280D95990081E8BA /* ChatMessageDeliveryStatusView_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageDeliveryStatusView_Tests.swift; sourceTree = ""; }; - 84F6126F268B415C00DDF6EE /* ChatClientConfig_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatClientConfig_Tests.swift; sourceTree = ""; }; 84FD350727FD8BE300D68D85 /* ChatChannel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannel_Tests.swift; sourceTree = ""; }; 8800A26E258A04D5006D64C4 /* ChatMessageAttachmentPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageAttachmentPreviewVC.swift; sourceTree = ""; }; 8800A28B258A1924006D64C4 /* ChatMessageFileAttachmentListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageFileAttachmentListView.swift; sourceTree = ""; }; @@ -7154,7 +7152,6 @@ isa = PBXGroup; children = ( AD7977B92936D9450008B5FB /* Token_Tests.swift */, - 84F6126F268B415C00DDF6EE /* ChatClientConfig_Tests.swift */, ); path = Config; sourceTree = ""; @@ -12159,7 +12156,6 @@ 7931818E24FD4275002F8C84 /* ChannelListController+Combine_Tests.swift in Sources */, 88EA9B0625472430007EE76B /* MessageReactionPayload_Tests.swift in Sources */, 79B8B649285B5ADD0059FB2D /* ChannelListSortingKey_Tests.swift in Sources */, - 84F61270268B415C00DDF6EE /* ChatClientConfig_Tests.swift in Sources */, 79877A292498E51500015F8B /* ChannelDTO_Tests.swift in Sources */, 79D6CE1725F7C02400BE2EEC /* ChannelWatcherListQuery_Tests.swift in Sources */, 40789D4229F6B3250018C2BB /* VoiceRecordingAttachmentPayload_Tests.swift in Sources */, @@ -14129,9 +14125,9 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; + SWIFT_OPTIMIZATION_LEVEL = "-Osize"; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 6.0; - SWIFT_OPTIMIZATION_LEVEL = "-Osize"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/ChannelDeliveryTracker_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/ChannelDeliveryTracker_Mock.swift index 3576ee8d102..d0a90a2470c 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/ChannelDeliveryTracker_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/WebSocketClient/ChannelDeliveryTracker_Mock.swift @@ -6,7 +6,7 @@ import Foundation @testable import StreamChat /// A mock implementation of `ChannelDeliveryTracker` for testing purposes. -final class ChannelDeliveryTracker_Mock: ChannelDeliveryTracker { +final class ChannelDeliveryTracker_Mock: ChannelDeliveryTracker, @unchecked Sendable { init() { super.init( currentUserUpdater: CurrentUserUpdater_Mock( diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ManualEventHandler_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ManualEventHandler_Mock.swift index 963900b4991..1258249f62b 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ManualEventHandler_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ManualEventHandler_Mock.swift @@ -3,7 +3,6 @@ // @testable import StreamChat -@testable import StreamChatTestTools import XCTest final class ManualEventHandler_Mock: ManualEventHandler, @unchecked Sendable { diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift index 36d4e614d78..cccb99aa460 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift @@ -105,7 +105,7 @@ public final class DatabaseContainer_Spy: DatabaseContainer, Spy, @unchecked Sen } } - override public func removeAllData(completion: ((Error?) -> Void)? = nil) { + override public func removeAllData(completion: (@Sendable (Error?) -> Void)? = nil) { removeAllData_called = true if let error = removeAllData_errorResponse { diff --git a/Tests/StreamChatTests/Audio/StreamAudioSessionConfigurator_Tests.swift b/Tests/StreamChatTests/Audio/StreamAudioSessionConfigurator_Tests.swift index 79c4f78bae6..5cf16d595f6 100644 --- a/Tests/StreamChatTests/Audio/StreamAudioSessionConfigurator_Tests.swift +++ b/Tests/StreamChatTests/Audio/StreamAudioSessionConfigurator_Tests.swift @@ -36,7 +36,7 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase { XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithCategory, .playAndRecord) XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithMode, .spokenAudio) XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithPolicy, .default) - XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [.allowBluetooth]) + XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [.allowBluetoothDevice]) } func test_activateRecordingSession_setActiveFailed() { @@ -105,7 +105,7 @@ final class StreamAudioSessionConfigurator_Tests: XCTestCase { XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithCategory, .playAndRecord) XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithMode, .default) XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithPolicy, .default) - XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [.defaultToSpeaker, .allowBluetooth]) + XCTAssertEqual(stubAudioSession.setCategoryWasCalledWithOptions, [.defaultToSpeaker, .allowBluetoothDevice]) } func test_activatePlaybackSession_setActiveFailed() { diff --git a/Tests/StreamChatTests/Config/ChatClientConfig_Tests.swift b/Tests/StreamChatTests/Config/ChatClientConfig_Tests.swift deleted file mode 100644 index 0f9f1d17cd2..00000000000 --- a/Tests/StreamChatTests/Config/ChatClientConfig_Tests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -@testable import StreamChat -@testable import StreamChatTestTools -import XCTest - -final class ChatClientConfig_Tests: XCTestCase { - func test_maxAttachmentSize_whenNoCustomCDNClient_takesMaxSizeFromDefaultCDNClient() { - // Create a config without custom CDN client. - let config = ChatClientConfig(apiKey: .init(.unique)) - - // Assert max size from default CDN client is used. - XCTAssertEqual(config.maxAttachmentSize, StreamCDNClient.maxAttachmentSize) - } - - func test_maxAttachmentSize_whenCustomCDNClientSet_takesMaxSizeFromCustomCDNClient() { - // Create a config. - var config = ChatClientConfig(apiKey: .init(.unique)) - - // Set custom CDN client. - config.customCDNClient = CustomCDNClient() - - // Assert max size from custom CDN client is used. - XCTAssertEqual(config.maxAttachmentSize, CustomCDNClient.maxAttachmentSize) - } -} diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift index ab6e86c2326..d74040c2510 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserSearchController/UserSearchController_Tests.swift @@ -653,7 +653,7 @@ private class TestDelegate: QueueAwareDelegate, ChatUserSearchControllerDelegate } } -extension UserListQuery: Equatable { +extension UserListQuery: @retroactive Equatable { public static func == (lhs: UserListQuery, rhs: UserListQuery) -> Bool { lhs.filter == rhs.filter && lhs.sort == rhs.sort diff --git a/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift b/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift index cffde4d1718..e43611e4288 100644 --- a/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/AuthenticationRepository_Tests.swift @@ -1179,7 +1179,7 @@ private class AuthenticationRepositoryDelegateMock: AuthenticationRepositoryDele } } -extension UserInfo: Equatable {} +extension UserInfo: @retroactive Equatable {} public func == (lhs: UserInfo, rhs: UserInfo) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name diff --git a/Tests/StreamChatTests/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandler_Tests.swift b/Tests/StreamChatTests/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandler_Tests.swift index 1d01150ab5c..cd225cbf04e 100644 --- a/Tests/StreamChatTests/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandler_Tests.swift +++ b/Tests/StreamChatTests/Utils/MessagesPaginationStateHandling/MessagesPaginationStateHandler_Tests.swift @@ -394,7 +394,7 @@ class MessagesPaginationStateHandlerTests: XCTestCase { } } -extension MessagesPaginationState: Equatable { +extension MessagesPaginationState: @retroactive Equatable { public static func == (lhs: MessagesPaginationState, rhs: MessagesPaginationState) -> Bool { lhs.hasLoadedAllNextMessages == rhs.hasLoadedAllNextMessages && lhs.hasLoadedAllPreviousMessages == rhs.hasLoadedAllPreviousMessages && diff --git a/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift index 71a5cbf629a..5460669b905 100644 --- a/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift @@ -992,15 +992,6 @@ import XCTest XCTAssertEqual(composerVC.maxAttachmentSize(for: .video), expectedValue) } - func test_maxAttachmentSize_whenSizeLimitNotDefined_thenReturnsLimitFromChatClientConfig() { - let expectedValue: Int64 = 50 * 1024 * 1024 - var config = ChatClientConfig(apiKeyString: "sadsad") - config.maxAttachmentSize = expectedValue - composerVC.channelController = ChatChannelController_Mock.mock(chatClientConfig: config) - - XCTAssertEqual(composerVC.maxAttachmentSize(for: .image), expectedValue) - } - // MARK: - audioPlayer func test_audioPlayer_voiceRecordingAndAttachmentsVCGetTheSameInstance() {