diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index 2aa2f045474..89164b238ac 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -3,18 +3,13 @@ description: 'Download and Install requested iOS Runtime' runs: using: "composite" steps: - - name: Cache iOS Simulator Runtime - uses: actions/cache@v4 - id: runtime-cache - with: - path: ./*.dmg - key: ipsw-runtime-ios-${{ inputs.version }} - restore-keys: ipsw-runtime-ios-${{ inputs.version }} - name: Setup iOS Simulator Runtime shell: bash run: | + sudo rm -rfv ~/Library/Developer/CoreSimulator/* || true brew install blacktop/tap/ipsw bundle exec fastlane install_runtime ios:${{ inputs.version }} + sudo rm -rfv *.dmg || true xcrun simctl list runtimes - name: Create Custom iOS Simulator shell: bash diff --git a/.github/workflows/release-merge.yml b/.github/workflows/release-merge.yml index a100effec18..c8c29253c30 100644 --- a/.github/workflows/release-merge.yml +++ b/.github/workflows/release-merge.yml @@ -24,7 +24,7 @@ jobs: - uses: ./.github/actions/ruby-cache - name: Merge - run: bundle exec fastlane merge_release_to_main author:"$USER_LOGIN" --verbose + run: bundle exec fastlane merge_release author:"$USER_LOGIN" --verbose env: GITHUB_TOKEN: ${{ secrets.ADMIN_API_TOKEN }} # A token with the "admin:org" scope to get the list of the team members on GitHub GITHUB_PR_NUM: ${{ github.event.issue.number }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c73861b7668..e7dfbe83d92 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1,24 +1,16 @@ name: "Publish new release" on: - pull_request: + push: branches: - main - types: - - closed workflow_dispatch: - inputs: - version: - description: 'Release version' - type: string - required: true jobs: release: name: Publish new release runs-on: macos-12 - if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true }} # only merged pull requests must trigger this job steps: - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 @@ -26,31 +18,15 @@ jobs: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 - uses: ./.github/actions/ruby-cache - - name: Extract version from input (for workflow dispatch) - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) - if [ "$BRANCH_NAME" != "main" ]; then - echo "This workflow can only be run on the main branch." - exit 1 - fi - echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - - - name: Extract version from branch name (for release branches) - if: ${{ github.event_name == 'pull_request' && startsWith(github.event.pull_request.head.ref, 'release/') }} - run: | - BRANCH_NAME="${{ github.event.pull_request.head.ref }}" - VERSION=${BRANCH_NAME#release/} - echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV - - name: "Fastlane - Publish Release" - if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.pull_request.head.ref, 'release/') }} env: GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} - run: bundle exec fastlane publish_release version:${{ env.RELEASE_VERSION }} --verbose + run: bundle exec fastlane publish_release --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md index b91bcc2c828..d49bee01f8b 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.63.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.63.0) +_September 05, 2024_ + +## StreamChat +### ✅ Added +- Local attachment downloads ([docs](https://getstream.io/chat/docs/sdk/ios/client/attachment-downloads)) [#3393](https://github.com/GetStream/stream-chat-swift/pull/3393) + - Add `downloadAttachment(_:)` and `deleteLocalAttachmentDownload(for:)` to `Chat` and `MessageController` + - Add `deleteAllLocalAttachmentDownloads()` to `ConnectedUser` and `CurrentUserController` +- Add `unset` argument to `CurrentChatUserController.updateUserData` and `ConnectedUser.update` for clearing user data fields [#3404](https://github.com/GetStream/stream-chat-swift/pull/3404) +### 🐞 Fixed +- Fix Logger printing the incorrect thread name [#3382](https://github.com/GetStream/stream-chat-swift/pull/3382) +- Channel watching did not resume on web-socket reconnection [#3409](https://github.com/GetStream/stream-chat-swift/pull/3409) +### 🔄 Changed +- Discard offline state changes when saving database changes fails [#3399](https://github.com/GetStream/stream-chat-swift/pull/3399) + +## StreamChatUI +### ✅ Added +- Downloading and sharing file attachments in the message list [#3393](https://github.com/GetStream/stream-chat-swift/pull/3393) + - Feature toggle for download and share buttons: `Components.default.isDownloadFileAttachmentsEnabled` (default is `false`) + # [4.62.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.62.0) _August 15, 2024_ diff --git a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift index 16d14ab5a76..1611c9761ca 100644 --- a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift +++ b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift @@ -187,6 +187,7 @@ class AppConfigViewController: UITableViewController { case mentionAllAppUsers case isBlockingUsersEnabled case isMessageListAnimationsEnabled + case isDownloadFileAttachmentsEnabled } enum ChatClientConfigOption: String, CaseIterable { @@ -477,6 +478,10 @@ class AppConfigViewController: UITableViewController { cell.accessoryView = makeSwitchButton(Components.default.isMessageListAnimationsEnabled) { newValue in Components.default.isMessageListAnimationsEnabled = newValue } + case .isDownloadFileAttachmentsEnabled: + cell.accessoryView = makeSwitchButton(Components.default.isDownloadFileAttachmentsEnabled) { newValue in + Components.default.isDownloadFileAttachmentsEnabled = newValue + } } } diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift index 03e57727025..44ea4864fee 100644 --- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift +++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift @@ -518,6 +518,34 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { self?.showChannel(for: cid, at: message?.id) } } + }), + .init(title: "Delete Downloaded Attachments", handler: { [unowned self] _ in + do { + let connectedUser = try self.rootViewController.controller.client.makeConnectedUser() + Task { + do { + try await connectedUser.deleteAllLocalAttachmentDownloads() + } catch { + self.rootViewController.presentAlert(title: error.localizedDescription) + } + } + } catch { + self.rootViewController.presentAlert(title: error.localizedDescription) + } + }), + .init(title: "Reset User Image", handler: { [unowned self] _ in + do { + let connectedUser = try self.rootViewController.controller.client.makeConnectedUser() + Task { + do { + try await connectedUser.update(unset: ["image"]) + } catch { + self.rootViewController.presentAlert(title: error.localizedDescription) + } + } + } catch { + self.rootViewController.presentAlert(title: error.localizedDescription) + } }) ]) } diff --git a/Gemfile.lock b/Gemfile.lock index 3849c5a49a5..824883faeca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -199,7 +199,7 @@ GEM fastlane pry fastlane-plugin-sonarcloud_metric_kit (0.2.1) - fastlane-plugin-stream_actions (0.3.60) + fastlane-plugin-stream_actions (0.3.63) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.5.2) ffi (1.17.0) @@ -330,7 +330,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.3) + rexml (3.3.6) strscan rouge (2.0.7) rubocop (1.38.0) @@ -437,7 +437,7 @@ DEPENDENCIES fastlane-plugin-create_xcframework fastlane-plugin-lizard fastlane-plugin-sonarcloud_metric_kit - fastlane-plugin-stream_actions (= 0.3.60) + fastlane-plugin-stream_actions (= 0.3.63) fastlane-plugin-versioning jazzy json diff --git a/README.md b/README.md index 481ac8107b8..45d332c6227 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@

- StreamChat - StreamChatUI + StreamChat + StreamChatUI

This is the official iOS SDK for [Stream Chat](https://getstream.io/chat/sdk/ios/), a service for building chat and messaging applications. This library includes both a low-level SDK and a set of reusable UI components. diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift index d753ae7b531..2e54f808f32 100644 --- a/Sources/StreamChat/APIClient/APIClient.swift +++ b/Sources/StreamChat/APIClient/APIClient.swift @@ -21,6 +21,9 @@ class APIClient { /// Used to queue requests that happen while we are offline var queueOfflineRequest: QueueOfflineRequestBlock? + /// The attachment downloader. + let attachmentDownloader: AttachmentDownloader + /// The attachment uploader. let attachmentUploader: AttachmentUploader @@ -59,11 +62,13 @@ class APIClient { sessionConfiguration: URLSessionConfiguration, requestEncoder: RequestEncoder, requestDecoder: RequestDecoder, + attachmentDownloader: AttachmentDownloader, attachmentUploader: AttachmentUploader ) { encoder = requestEncoder decoder = requestDecoder session = URLSession(configuration: sessionConfiguration) + self.attachmentDownloader = attachmentDownloader self.attachmentUploader = attachmentUploader } @@ -226,19 +231,12 @@ class APIClient { return } - log.debug( - "Making URL request: \(endpoint.method.rawValue.uppercased()) \(endpoint.path)\n" - + "Headers:\n\(String(describing: urlRequest.allHTTPHeaderFields))\n" - + "Body:\n\(urlRequest.httpBody?.debugPrettyPrintedJSON ?? "")\n" - + "Query items:\n\(urlRequest.queryItems.prettyPrinted)", - subsystems: .httpRequests - ) - guard let self = self else { log.warning("Callback called while self is nil", subsystems: .httpRequests) completion(.failure(ClientError("APIClient was deallocated"))) return } + log.debug(urlRequest.cURLRepresentation(for: self.session), subsystems: .httpRequests) let task = self.session.dataTask(with: urlRequest) { [decoder = self.decoder] (data, response, error) in do { @@ -288,7 +286,32 @@ class APIClient { // We only retry transient errors like connectivity stuff or HTTP 5xx errors ClientError.isEphemeral(error: error) } - + + func downloadFile( + from remoteURL: URL, + to localURL: URL, + progress: ((Double) -> Void)?, + completion: @escaping (Error?) -> Void + ) { + let downloadOperation = AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in + self?.attachmentDownloader.download(from: remoteURL, to: localURL, progress: progress) { error in + if let error, self?.isConnectionError(error) == true { + // Do not retry unless its a connection problem and we still have retries left + if operation.canRetry { + done(.retry) + } else { + completion(error) + done(.continue) + } + } else { + completion(error) + done(.continue) + } + } + } + operationQueue.addOperation(downloadOperation) + } + func uploadAttachment( _ attachment: AnyChatMessageAttachment, progress: ((Double) -> Void)?, diff --git a/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift b/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift new file mode 100644 index 00000000000..8db4b990385 --- /dev/null +++ b/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift @@ -0,0 +1,71 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// The component responsible for downloading files. +protocol AttachmentDownloader { + /// Downloads a file attachment to the specified local URL. + /// + /// - Parameters: + /// - remoteURL: A remote URL of the file. + /// - localURL: The destination URL of the download. + /// - progress: The progress of the download. + /// - completion: The callback with an error if a failure occured. + func download( + from remoteURL: URL, + to localURL: URL, + progress: ((Double) -> Void)?, + completion: @escaping (Error?) -> Void + ) +} + +final class StreamAttachmentDownloader: AttachmentDownloader { + private let session: URLSession + @Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:] + + init(sessionConfiguration: URLSessionConfiguration) { + session = URLSession(configuration: sessionConfiguration) + } + + func download( + from remoteURL: URL, + to localURL: URL, + progress: ((Double) -> Void)?, + completion: @escaping (Error?) -> Void + ) { + let request = URLRequest(url: remoteURL) + let task = session.downloadTask(with: request) { temporaryURL, _, downloadError in + if let downloadError { + completion(downloadError) + } else if let temporaryURL { + do { + try FileManager.default.createDirectory(at: localURL.deletingLastPathComponent(), withIntermediateDirectories: true) + if FileManager.default.fileExists(atPath: localURL.path) { + try FileManager.default.removeItem(at: localURL) + } + try FileManager.default.moveItem(at: temporaryURL, to: localURL) + completion(nil) + } catch { + completion(error) + } + } + } + if let progressHandler = progress { + let taskID = task.taskIdentifier + _taskProgressObservers.mutate { observers in + observers[taskID] = task.progress.observe(\.fractionCompleted, options: [.initial]) { [weak self] progress, _ in + progressHandler(progress.fractionCompleted) + if progress.isFinished || progress.isCancelled { + self?._taskProgressObservers.mutate { observers in + observers[taskID]?.invalidate() + observers[taskID] = nil + } + } + } + } + } + task.resume() + } +} diff --git a/Sources/StreamChat/APIClient/Endpoints/UserEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/UserEndpoints.swift index a751d542cb1..040979ce5a7 100644 --- a/Sources/StreamChat/APIClient/Endpoints/UserEndpoints.swift +++ b/Sources/StreamChat/APIClient/Endpoints/UserEndpoints.swift @@ -18,11 +18,13 @@ extension Endpoint { static func updateUser( id: UserId, - payload: UserUpdateRequestBody + payload: UserUpdateRequestBody, + unset: [String] ) -> Endpoint { let users: [String: AnyEncodable] = [ "id": AnyEncodable(id), - "set": AnyEncodable(payload) + "set": AnyEncodable(payload), + "unset": AnyEncodable(unset) ] let body: [String: AnyEncodable] = [ "users": AnyEncodable([users]) diff --git a/Sources/StreamChat/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift index df00cd15d87..9d86085105a 100644 --- a/Sources/StreamChat/ChatClient+Environment.swift +++ b/Sources/StreamChat/ChatClient+Environment.swift @@ -11,15 +11,9 @@ extension ChatClient { _ sessionConfiguration: URLSessionConfiguration, _ requestEncoder: RequestEncoder, _ requestDecoder: RequestDecoder, + _ attachmentDownloader: AttachmentDownloader, _ attachmentUploader: AttachmentUploader - ) -> APIClient = { - APIClient( - sessionConfiguration: $0, - requestEncoder: $1, - requestDecoder: $2, - attachmentUploader: $3 - ) - } + ) -> APIClient = APIClient.init var webSocketClientBuilder: (( _ sessionConfiguration: URLSessionConfiguration, diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift index 626b465ef32..d5a303f1a65 100644 --- a/Sources/StreamChat/ChatClient.swift +++ b/Sources/StreamChat/ChatClient.swift @@ -10,6 +10,8 @@ import Foundation /// /// Typically, an app contains just one instance of `ChatClient`. However, it's possible to have multiple instances if your use /// case requires it (i.e. more than one window with different workspaces in a Slack-like app). +/// +/// - Important: When using multiple instances of `ChatClient` at the same time, it is required to use a different ``ChatClientConfig/localStorageFolderURL`` for each instance. For example, adding an additional path component to the default URL. public class ChatClient { /// The `UserId` of the currently logged in user. public var currentUserId: UserId? { @@ -99,6 +101,8 @@ public class ChatClient { /// The environment object containing all dependencies of this `Client` instance. private let environment: Environment + + @Atomic static var activeLocalStorageURLs = Set() /// The default configuration of URLSession to be used for both the `APIClient` and `WebSocketClient`. It contains all /// required header auth parameters to make a successful request. @@ -217,9 +221,11 @@ public class ChatClient { setupTokenRefresher() setupOfflineRequestQueue() setupConnectionRecoveryHandler(with: environment) + validateIntegrity() } deinit { + Self._activeLocalStorageURLs.mutate { $0.subtract(databaseContainer.persistentStoreDescriptions.compactMap(\.url)) } completeConnectionIdWaiters(connectionId: nil) completeTokenWaiters(token: nil) } @@ -255,6 +261,20 @@ public class ChatClient { config.reconnectionTimeout.map { ScheduledStreamTimer(interval: $0, fireOnStart: false, repeats: false) } ) } + + private func validateIntegrity() { + Self._activeLocalStorageURLs.mutate { urls in + let existingCount = urls.count + urls.formUnion(databaseContainer.persistentStoreDescriptions.compactMap(\.url).filter { $0.path != "/dev/null" }) + guard existingCount == urls.count, !urls.isEmpty else { return } + log.error( + """ + There are multiple ChatClient instances using the same `ChatClientConfig.localStorageFolderURL` - this is disallowed. + Either create a shared instance or make sure the previous instance of `ChatClient` is deallocated. + """ + ) + } + } /// Register a custom attachment payload. /// diff --git a/Sources/StreamChat/ChatClientFactory.swift b/Sources/StreamChat/ChatClientFactory.swift index dc7834c5d3d..39fd01a0baa 100644 --- a/Sources/StreamChat/ChatClientFactory.swift +++ b/Sources/StreamChat/ChatClientFactory.swift @@ -45,6 +45,7 @@ class ChatClientFactory { encoder: RequestEncoder, urlSessionConfiguration: URLSessionConfiguration ) -> APIClient { + let attachmentDownloader = StreamAttachmentDownloader(sessionConfiguration: urlSessionConfiguration) let decoder = environment.requestDecoderBuilder() let attachmentUploader = config.customAttachmentUploader ?? StreamAttachmentUploader( cdnClient: config.customCDNClient ?? StreamCDNClient( @@ -57,6 +58,7 @@ class ChatClientFactory { urlSessionConfiguration, encoder, decoder, + attachmentDownloader, attachmentUploader ) return apiClient diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift index d85e5f0efb6..218bb20d240 100644 --- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift +++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift @@ -155,7 +155,7 @@ public extension CurrentChatUserController { /// /// By default all data is `nil`, and it won't be updated unless a value is provided. /// - /// - Note: This operation does a partial user update which keeps existing data if not modified. + /// - Note: This operation does a partial user update which keeps existing data if not modified. Use ``unset`` for clearing the existing state. /// /// - Parameters: /// - name: Optionally provide a new name to be updated. @@ -163,6 +163,7 @@ public extension CurrentChatUserController { /// - privacySettings: The privacy settings of the user. Example: If the user does not want to expose typing events or read events. /// - role: The role for the user. /// - userExtraData: Optionally provide new user extra data to be updated. + /// - unset: Existing values for specified properties are removed. For example, `image` or `name`. /// - completion: Called when user is successfuly updated, or with error. func updateUserData( name: String? = nil, @@ -170,6 +171,7 @@ public extension CurrentChatUserController { privacySettings: UserPrivacySettings? = nil, role: UserRole? = nil, userExtraData: [String: RawJSON] = [:], + unsetProperties: Set = [], completion: ((Error?) -> Void)? = nil ) { guard let currentUserId = client.currentUserId else { @@ -256,6 +258,18 @@ public extension CurrentChatUserController { } } } + + /// Deletes all the local downloads of file attachments. + /// + /// - Parameter completion: Called when files have been deleted or when an error occured. + func deleteAllLocalAttachmentDownloads(completion: ((Error?) -> Void)? = nil) { + currentUserUpdater.deleteAllLocalAttachmentDownloads { error in + guard let completion else { return } + self.callback { + completion(error) + } + } + } /// Fetches all the unread information from the current user. /// diff --git a/Sources/StreamChat/Controllers/MessageController/MessageController.swift b/Sources/StreamChat/Controllers/MessageController/MessageController.swift index 558bdccb789..54ae0d23dff 100644 --- a/Sources/StreamChat/Controllers/MessageController/MessageController.swift +++ b/Sources/StreamChat/Controllers/MessageController/MessageController.swift @@ -664,7 +664,40 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP } } } - + + /// Downloads the specified attachment and stores it locally on the device. + /// + /// - Parameters: + /// - attachment: The attachment to download. + /// - completion: A completion block with the attachment containing the downloading state. + /// + /// - Note: The local storage URL (`attachment.downloadingState?.localFileURL`) can change between app launches. + public func downloadAttachment( + _ attachment: ChatMessageAttachment, + completion: @escaping (Result, Error>) -> Void + ) where Payload: DownloadableAttachmentPayload { + messageUpdater.downloadAttachment(attachment) { result in + self.callback { + completion(result) + } + } + } + + /// Deletes the locally downloaded file. + /// + /// - SeeAlso: Deleting all the local downloads: ``CurrentChatUserController/deleteAllLocalAttachmentDownloads(completion:)`` + /// + /// - Parameters: + /// - attachmentId: The id of the attachment. + /// - completion: A completion block with an error if the deletion failed. + public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: ((Error?) -> Void)? = nil) { + messageUpdater.deleteLocalAttachmentDownload(for: attachmentId) { error in + self.callback { + completion?(error) + } + } + } + /// Updates local state of attachment with provided `id` to be enqueued by attachment uploader. /// - Parameters: /// - id: The attachment identifier. diff --git a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift index a127f611503..44674e3528d 100644 --- a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift +++ b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift @@ -24,7 +24,7 @@ class AttachmentDTO: NSManagedObject { set { type = newValue.rawValue } } - /// An attachment local state. + /// An attachment local upload state. @NSManaged private var localStateRaw: String? @NSManaged private var localProgress: Double var localState: LocalAttachmentState? { @@ -37,12 +37,36 @@ class AttachmentDTO: NSManagedObject { localProgress = newValue?.progress ?? 0 } } + + /// An attachment local download state. + @NSManaged private var localDownloadStateRaw: String? + var localDownloadState: LocalAttachmentDownloadState? { + get { + guard let localDownloadStateRaw else { return nil } + return LocalAttachmentDownloadState(rawValue: localDownloadStateRaw, progress: localProgress) + } + set { + localDownloadStateRaw = newValue?.rawValue + localProgress = newValue?.progress ?? 0 + } + } /// An attachment local url. @NSManaged var localURL: URL? + + /// An attachment local relative path used for storing downloaded attachments. + @NSManaged var localRelativePath: String? + /// An attachment raw `Data`. @NSManaged var data: Data + func clearLocalState() { + localDownloadState = nil + localRelativePath = nil + localState = nil + localURL = nil + } + // MARK: - Relationships @NSManaged var message: MessageDTO @@ -76,6 +100,13 @@ class AttachmentDTO: NSManagedObject { return new } + static func downloadedFetchRequest() -> NSFetchRequest { + let request = NSFetchRequest(entityName: AttachmentDTO.entityName) + request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)] + request.predicate = NSPredicate(format: "localDownloadStateRaw == %@", LocalAttachmentDownloadState.downloaded.rawValue) + return request + } + static func pendingUploadFetchRequest() -> NSFetchRequest { let request = NSFetchRequest(entityName: AttachmentDTO.entityName) request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)] @@ -89,6 +120,24 @@ class AttachmentDTO: NSManagedObject { request.predicate = NSPredicate(format: "localStateRaw == %@", LocalAttachmentState.uploading(progress: 0).rawValue) return load(by: request, context: context) } + + static func loadAllDownloadedAttachments(context: NSManagedObjectContext) -> [AttachmentDTO] { + let request = NSFetchRequest(entityName: AttachmentDTO.entityName) + request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)] + request.predicate = NSPredicate(format: "localDownloadStateRaw == %@", LocalAttachmentDownloadState.downloaded.rawValue) + return load(by: request, context: context) + } +} + +extension AttachmentDTO: EphemeralValuesContainer { + func resetEphemeralValues() { + switch localDownloadState { + case .downloading, .downloadingFailed: + clearLocalState() + default: + break + } + } } extension NSManagedObjectContext: AttachmentDatabaseSession { @@ -110,8 +159,10 @@ extension NSManagedObjectContext: AttachmentDatabaseSession { dto.data = try JSONEncoder.default.encode(payload.payload) dto.message = messageDTO - dto.localURL = nil - dto.localState = nil + // Keep local state for downloaded attachments + if dto.localDownloadState == nil { + dto.clearLocalState() + } return dto } @@ -140,9 +191,37 @@ extension NSManagedObjectContext: AttachmentDatabaseSession { func delete(attachment: AttachmentDTO) { delete(attachment) } + + func allLocallyDownloadedAttachments() -> [AttachmentDTO] { + AttachmentDTO.loadAllDownloadedAttachments(context: self) + } } private extension AttachmentDTO { + var downloadingState: AttachmentDownloadingState? { + guard let localDownloadState else { return nil } + let localFileURL: URL? = { + guard let localRelativePath, !localRelativePath.isEmpty else { return nil } + return URL.streamAttachmentLocalStorageURL(forRelativePath: localRelativePath) + }() + let file: AttachmentFile? = { + // Most attachments contain the attachment file information + if let file = try? JSONDecoder.stream.decode(AttachmentFile.self, from: data) { + return file + } + // Try extracting it from the downloaded file + if let localFileURL { + return try? AttachmentFile(url: localFileURL) + } + return nil + }() + return AttachmentDownloadingState( + localFileURL: localFileURL, + state: localDownloadState, + file: file + ) + } + var uploadingState: AttachmentUploadingState? { guard let localURL = localURL, @@ -177,6 +256,7 @@ extension AttachmentDTO { id: id, type: attachmentType, payload: data, + downloadingState: downloadingState, uploadingState: uploadingState ) } @@ -239,6 +319,41 @@ extension LocalAttachmentState { } } +extension LocalAttachmentDownloadState { + var rawValue: String { + switch self { + case .downloading: + return "downloading" + case .downloadingFailed: + return "downloadingFailed" + case .downloaded: + return "downloaded" + } + } + + var progress: Double { + switch self { + case let .downloading(progress): + return progress + default: + return 0 + } + } + + init?(rawValue: String, progress: Double) { + switch rawValue { + case LocalAttachmentDownloadState.downloaded.rawValue: + self = .downloaded + case LocalAttachmentDownloadState.downloadingFailed.rawValue: + self = .downloadingFailed + case LocalAttachmentDownloadState.downloading(progress: 0).rawValue: + self = .downloading(progress: progress) + default: + return nil + } + } +} + extension ClientError { final class AttachmentDoesNotExist: ClientError { init(id: AttachmentId) { @@ -254,6 +369,14 @@ extension ClientError { final class AttachmentDecoding: ClientError {} + final class AttachmentDownloading: ClientError { + init(id: AttachmentId, reason: String) { + super.init( + "Failed to download attachment with id: \(id): \(reason)" + ) + } + } + final class AttachmentUploading: ClientError { init(id: AttachmentId) { super.init( diff --git a/Sources/StreamChat/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift index 4fb3ec1fe10..d93cfadc1ec 100644 --- a/Sources/StreamChat/Database/DatabaseContainer.swift +++ b/Sources/StreamChat/Database/DatabaseContainer.swift @@ -77,7 +77,6 @@ class DatabaseContainer: NSPersistentContainer { return context }() - private var canWriteData = true private var stateLayerContextRefreshObservers = [NSObjectProtocol]() private var loggerNotificationObserver: NSObjectProtocol? private let localCachingSettings: ChatClientConfig.LocalCaching? @@ -218,12 +217,6 @@ class DatabaseContainer: NSPersistentContainer { func write(_ actions: @escaping (DatabaseSession) throws -> Void, completion: @escaping (Error?) -> Void) { writableContext.perform { log.debug("Starting a database session.", subsystems: .database) - guard self.canWriteData else { - log.debug("Discarding write attempt.", subsystems: .database) - completion(nil) - return - } - do { FetchCache.clear() try actions(self.writableContext) @@ -244,9 +237,9 @@ class DatabaseContainer: NSPersistentContainer { log.debug("Database session succesfully saved.", subsystems: .database) completion(nil) - } catch { log.error("Failed to save data to DB. Error: \(error)", subsystems: .database) + self.writableContext.reset() FetchCache.clear() completion(error) } @@ -300,7 +293,6 @@ class DatabaseContainer: NSPersistentContainer { func removeAllData(completion: ((Error?) -> Void)? = nil) { let entityNames = managedObjectModel.entities.compactMap(\.name) writableContext.perform { [weak self] in - self?.canWriteData = false let requests = entityNames .map { NSFetchRequest(entityName: $0) } .map { fetchRequest in @@ -323,7 +315,7 @@ class DatabaseContainer: NSPersistentContainer { } if !deletedObjectIds.isEmpty, let contexts = self?.allContext { log.debug("Merging \(deletedObjectIds.count) deletions to contexts", subsystems: .database) - // Merging changes triggers DB observers to react to deletions + // Merging changes triggers DB observers to react to deletions which clears the state NSManagedObjectContext.mergeChanges( fromRemoteContextSave: [NSDeletedObjectsKey: deletedObjectIds], into: contexts @@ -340,8 +332,15 @@ class DatabaseContainer: NSPersistentContainer { context.reset() } } + + if FileManager.default.fileExists(atPath: URL.streamAttachmentDownloadsDirectory.path) { + do { + try FileManager.default.removeItem(at: .streamAttachmentDownloadsDirectory) + } catch { + log.debug("Failed to remove local downloads", subsystems: .database) + } + } } - self?.canWriteData = true completion?(lastEncounteredError) } } diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift index fb9c26b08f4..900ffbdbdc9 100644 --- a/Sources/StreamChat/Database/DatabaseSession.swift +++ b/Sources/StreamChat/Database/DatabaseSession.swift @@ -404,6 +404,9 @@ protocol AttachmentDatabaseSession { /// Deletes the provided dto from a database /// - Parameter attachment: The DTO to be deleted func delete(attachment: AttachmentDTO) + + /// All the attachments with the local status being downloaded. + func allLocallyDownloadedAttachments() -> [AttachmentDTO] } protocol QueuedRequestDatabaseSession { diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents index dd35fc9f1f1..86a53ad6075 100644 --- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents +++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents @@ -1,9 +1,11 @@ - + + + diff --git a/Sources/StreamChat/Extensions/URLRequest+cURL.swift b/Sources/StreamChat/Extensions/URLRequest+cURL.swift new file mode 100644 index 00000000000..f6f23eaea4f --- /dev/null +++ b/Sources/StreamChat/Extensions/URLRequest+cURL.swift @@ -0,0 +1,38 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +extension URLRequest { + /// Gives cURL representation of the request for an easy API request reproducibility in Terminal. + /// - Parameter urlSession: The URLSession handling the request. + /// - Returns: cURL representation of the URLRequest. + func cURLRepresentation(for urlSession: URLSession) -> String { + guard let url, let httpMethod else { return "$ curl failed to create" } + var cURL = [String]() + cURL.append("$ curl -v") + cURL.append("-X \(httpMethod)") + + var allHeaders = [String: String]() + if let additionalHeaders = urlSession.configuration.httpAdditionalHeaders as? [String: String] { + allHeaders.merge(additionalHeaders, uniquingKeysWith: { _, new in new }) + } + if let allHTTPHeaderFields { + allHeaders.merge(allHTTPHeaderFields, uniquingKeysWith: { _, new in new }) + } + cURL.append(contentsOf: allHeaders + .mapValues { $0.replacingOccurrences(of: "\"", with: "\\\"") } + .map { "-H \"\($0.key): \($0.value)\"" } + ) + if let httpBody { + let httpBodyString = String(decoding: httpBody, as: UTF8.self) + let escapedBody = httpBodyString + .replacingOccurrences(of: "\\\"", with: "\\\\\"") + .replacingOccurrences(of: "\"", with: "\\\"") + cURL.append("-d \"\(escapedBody)\"") + } + cURL.append("\"\(url.absoluteString)\"") + return cURL.joined(separator: " \\\n\t") + } +} diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift index 32fd2eebeab..d32e783add7 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.62.0" + public static let version: String = "4.63.0" } diff --git a/Sources/StreamChat/Info.plist b/Sources/StreamChat/Info.plist index 4f55c7aea35..24ce17378e1 100644 --- a/Sources/StreamChat/Info.plist +++ b/Sources/StreamChat/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.62.0 + 4.63.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift index 9eef5709906..0093a4bff33 100644 --- a/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift +++ b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift @@ -36,6 +36,16 @@ public enum LocalAttachmentState: Hashable { case uploaded } +/// A local download state of the attachment. +public enum LocalAttachmentDownloadState: Hashable { + /// The attachment is being downloaded. + case downloading(progress: Double) + /// The attachment download failed. + case downloadingFailed + /// The attachment has been downloaded. + case downloaded +} + /// An attachment action, e.g. send, shuffle. public struct AttachmentAction: Codable, Hashable { /// A name. diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift index f0a3d33ca1f..de7e45ec927 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift @@ -17,6 +17,11 @@ public struct ChatMessageAttachment { /// 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. @@ -29,11 +34,13 @@ public struct ChatMessageAttachment { id: AttachmentId, type: AttachmentType, payload: Payload, + downloadingState: AttachmentDownloadingState?, uploadingState: AttachmentUploadingState? ) { self.id = id self.type = type self.payload = payload + self.downloadingState = downloadingState self.uploadingState = uploadingState } } @@ -47,6 +54,23 @@ public extension ChatMessageAttachment { extension ChatMessageAttachment: Equatable where Payload: Equatable {} extension ChatMessageAttachment: Hashable where Payload: Hashable {} +/// A type represeting the downloading state for attachments. +public struct AttachmentDownloadingState: Hashable { + /// The local file URL of the downloaded attachment. + /// + /// - Note: The local file URL is available when the state is `.downloaded`. + public let localFileURL: URL? + + /// The local download state of the attachment. + public let state: LocalAttachmentDownloadState + + /// The information about file size/mimeType. + /// + /// - Returns: The file information if it is part of the attachment payload, + /// otherwise it is extracted from the downloaded file. + public let file: AttachmentFile? +} + /// A type representing the uploading state for attachments that require prior uploading. public struct AttachmentUploadingState: Hashable { /// The local file URL that is being uploaded. @@ -83,6 +107,7 @@ public extension AnyChatMessageAttachment { id: id, type: type, payload: concretePayload, + downloadingState: downloadingState, uploadingState: uploadingState ) } @@ -96,6 +121,7 @@ public extension ChatMessageAttachment where Payload: AttachmentPayload { id: id, type: type, payload: try! JSONEncoder.stream.encode(payload), + downloadingState: downloadingState, uploadingState: uploadingState ) } @@ -118,7 +144,53 @@ public extension ChatMessageAttachment where Payload: AttachmentPayload { id: id, type: .file, payload: concretePayload, + downloadingState: downloadingState, uploadingState: uploadingState ) } } + +// MARK: - Local Downloads + +/// The attachment payload which can be downloaded. +public typealias DownloadableAttachmentPayload = AttachmentPayloadDownloading & AttachmentPayload + +/// A capability of downloading attachment payload data to the local storage. +public protocol AttachmentPayloadDownloading { + /// The file name used for storing the attachment file locally. + /// + /// Example: `myfile.txt` + /// + /// - Note: Does not need to be unique. + var localStorageFileName: String { get } + + /// The remote URL of the attachment what can be downloaded and stored locally. + /// + /// For example, an image for image attachments. + var remoteURL: URL { get } +} + +extension AttachmentFile { + func defaultLocalStorageFileName(for attachmentType: AttachmentType) -> String { + "\(attachmentType.rawValue.localizedCapitalized).\(type.rawValue)" // image.jpeg + } +} + +extension URL { + /// The directory URL for attachment downloads. + static var streamAttachmentDownloadsDirectory: URL { + (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory) + .appendingPathComponent("StreamAttachmentDownloads", isDirectory: true) + } + + static func streamAttachmentLocalStorageURL(forRelativePath path: String) -> URL { + URL(fileURLWithPath: path, isDirectory: false, relativeTo: .streamAttachmentDownloadsDirectory).standardizedFileURL + } +} + +extension ChatMessageAttachment where Payload: AttachmentPayloadDownloading { + /// A local and unique file path for the attachment. + var relativeStoragePath: String { + "\(id.messageId)-\(id.index)/\(payload.localStorageFileName)" + } +} diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageAudioAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageAudioAttachment.swift index 57c2339cd27..029b960c5e2 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageAudioAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageAudioAttachment.swift @@ -47,6 +47,18 @@ public struct AudioAttachmentPayload: AttachmentPayload { extension AudioAttachmentPayload: Hashable {} +// MARK: - Local Downloads + +extension AudioAttachmentPayload: AttachmentPayloadDownloading { + public var localStorageFileName: String { + title ?? file.defaultLocalStorageFileName(for: Self.type) + } + + public var remoteURL: URL { + audioURL + } +} + // MARK: - Encodable extension AudioAttachmentPayload: Encodable { diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageFileAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageFileAttachment.swift index 3feb03b163b..65d3e4090d7 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageFileAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageFileAttachment.swift @@ -47,6 +47,18 @@ public struct FileAttachmentPayload: AttachmentPayload { extension FileAttachmentPayload: Hashable {} +// MARK: - Local Downloads + +extension FileAttachmentPayload: AttachmentPayloadDownloading { + public var localStorageFileName: String { + title ?? file.defaultLocalStorageFileName(for: Self.type) + } + + public var remoteURL: URL { + assetURL + } +} + // MARK: - Encodable extension FileAttachmentPayload: Encodable { diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageGiphyAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageGiphyAttachment.swift index 69e26e304eb..45cb5fde477 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageGiphyAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageGiphyAttachment.swift @@ -15,7 +15,7 @@ public struct GiphyAttachmentPayload: AttachmentPayload { /// An attachment type all `GiphyAttachmentPayload` instances conform to. Is set to `.giphy`. public static let type: AttachmentType = .giphy - /// A title, usually the search request used to find the gif. + /// A title, usually the search request used to find the gif. public var title: String? /// A link to gif file. public var previewURL: URL diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift index 979ccadaa16..cc47f895fe0 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageImageAttachment.swift @@ -84,6 +84,18 @@ public struct ImageAttachmentPayload: AttachmentPayload { extension ImageAttachmentPayload: Hashable {} +// MARK: - Local Downloads + +extension ImageAttachmentPayload: AttachmentPayloadDownloading { + public var localStorageFileName: String { + title ?? imageURL.lastPathComponent + } + + public var remoteURL: URL { + imageURL + } +} + // MARK: - Encodable extension ImageAttachmentPayload: Encodable { diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageVideoAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageVideoAttachment.swift index 49fd4c76153..028145c2d5e 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageVideoAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageVideoAttachment.swift @@ -50,6 +50,18 @@ public struct VideoAttachmentPayload: AttachmentPayload { extension VideoAttachmentPayload: Hashable {} +// MARK: - Local Downloads + +extension VideoAttachmentPayload: AttachmentPayloadDownloading { + public var localStorageFileName: String { + title ?? file.defaultLocalStorageFileName(for: Self.type) + } + + public var remoteURL: URL { + videoURL + } +} + // MARK: - Encodable extension VideoAttachmentPayload: Encodable { diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageVoiceRecordingAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageVoiceRecordingAttachment.swift index d77159542f6..700b94ed238 100644 --- a/Sources/StreamChat/Models/Attachments/ChatMessageVoiceRecordingAttachment.swift +++ b/Sources/StreamChat/Models/Attachments/ChatMessageVoiceRecordingAttachment.swift @@ -69,6 +69,18 @@ extension VoiceRecordingAttachmentPayload { } } +// MARK: - Local Downloads + +extension VoiceRecordingAttachmentPayload: AttachmentPayloadDownloading { + public var localStorageFileName: String { + title ?? file.defaultLocalStorageFileName(for: Self.type) + } + + public var remoteURL: URL { + voiceRecordingURL + } +} + // MARK: - Encodable extension VoiceRecordingAttachmentPayload: Encodable { diff --git a/Sources/StreamChat/Repositories/SyncOperations.swift b/Sources/StreamChat/Repositories/SyncOperations.swift index 1220b70e962..6a9a11174f0 100644 --- a/Sources/StreamChat/Repositories/SyncOperations.swift +++ b/Sources/StreamChat/Repositories/SyncOperations.swift @@ -161,7 +161,7 @@ final class WatchChannelOperation: AsyncOperation { } let cidString = (controller.cid?.rawValue ?? "unknown") - log.info("2. Watching active channel \(cidString)", subsystems: .offlineSupport) + log.info("Watching active channel \(cidString)", subsystems: .offlineSupport) controller.recoverWatchedChannel { error in if let cid = controller.cid, error == nil { log.info("Successfully watched active channel \(cidString)", subsystems: .offlineSupport) @@ -175,6 +175,32 @@ final class WatchChannelOperation: AsyncOperation { } } } + + init(chat: Chat, context: SyncContext) { + super.init(maxRetries: syncOperationsMaximumRetries) { [weak chat] _, done in + guard let chat else { + done(.continue) + return + } + Task { + guard await chat.state.channelQuery.options.contains(.watch) else { + done(.continue) + return + } + do { + let cid = try await chat.cid + log.info("Watching active chat \(cid.rawValue)", subsystems: .offlineSupport) + try await chat.watch() + context.watchedAndSynchedChannelIds.insert(cid) + log.info("Successfully watched active chat \(cid.rawValue)", subsystems: .offlineSupport) + done(.continue) + } catch { + log.error("Failed watching active chat with error \(error.localizedDescription)", subsystems: .offlineSupport) + done(.retry) + } + } + } + } } final class RefetchChannelListQueryOperation: AsyncOperation { diff --git a/Sources/StreamChat/Repositories/SyncRepository.swift b/Sources/StreamChat/Repositories/SyncRepository.swift index 35b237d7d5f..65563b5d81d 100644 --- a/Sources/StreamChat/Repositories/SyncRepository.swift +++ b/Sources/StreamChat/Repositories/SyncRepository.swift @@ -158,6 +158,7 @@ class SyncRepository { /// 1. Collect all the **active** channel ids (from instances of `Chat`, `ChannelList`, `ChatChannelController`, `ChatChannelListController`) /// 2. Apply updates from the /sync endpoint for these channels /// 3. Refresh channel lists (channels for current pages in `ChannelList`, `ChatChannelListController`) + /// 4. Re-watch channels what we were watching before disconnect private func syncLocalStateV2(lastSyncAt: Date, completion: @escaping () -> Void) { let context = SyncContext(lastSyncAt: lastSyncAt) var operations: [Operation] = [] @@ -189,6 +190,11 @@ class SyncRepository { operations.append(contentsOf: activeChannelLists.allObjects.map { RefreshChannelListOperation(channelList: $0, context: context) }) operations.append(contentsOf: activeChannelListControllers.allObjects.map { RefreshChannelListOperation(controller: $0, context: context) }) + // 4. Re-watch channels what we were watching before disconnect + // Needs to be done explicitly after reconnection, otherwise SDK users need to handle connection changes + operations.append(contentsOf: activeChannelControllers.allObjects.map { WatchChannelOperation(controller: $0, context: context) }) + operations.append(contentsOf: activeChats.allObjects.map { WatchChannelOperation(chat: $0, context: context) }) + operations.append(BlockOperation(block: { let duration = CFAbsoluteTimeGetCurrent() - start log.info("Finished refreshing offline state (\(context.synchedChannelIds.count) channels in \(String(format: "%.1f", duration)) seconds)", subsystems: .offlineSupport) diff --git a/Sources/StreamChat/StateLayer/Chat.swift b/Sources/StreamChat/StateLayer/Chat.swift index 4476f6dcfea..93d60b871be 100644 --- a/Sources/StreamChat/StateLayer/Chat.swift +++ b/Sources/StreamChat/StateLayer/Chat.swift @@ -87,6 +87,9 @@ public class Chat { channelQuery: query, memberSorting: state.memberSorting ) + // Store the watch state + await state.setChannelQuery(query) + client.syncRepository.startTrackingChat(self) // cid is retrieved from the server when we are creating new channels or there is no local state present guard query.cid != payload.channel.cid else { return } @@ -352,16 +355,47 @@ public class Chat { return try await messageSender.waitForAPIRequest(messageId: messageId) } + /// Downloads the specified attachment and stores it locally on the device. + /// + /// The local URL of the downloaded file: + /// ```swift + /// let downloadedAttachment = try await chat.downloadAttachment(attachment) + /// let localURL = downloadedAttachment.downloadingState?.localFileURL + /// ``` + /// + /// - Parameter attachment: The attachment to download. + /// + /// - Note: The local storage URL can change between app launches. + /// + /// - Throws: An error while downloading the attachment. + /// - Returns: An instance of the downloaded attachment which includes the local URL. + @discardableResult public func downloadAttachment( + _ attachment: ChatMessageAttachment + ) async throws -> ChatMessageAttachment where Payload: DownloadableAttachmentPayload { + try await messageUpdater.downloadAttachment(attachment) + } + + /// Deletes the locally downloaded file. + /// + /// - Parameter attachmentId: The id of the attachment. + /// + /// - SeeAlso: Deleting all the local downloads: ``ConnectedUser/deleteAllLocalAttachmentDownloads()`` + /// + /// - Throws: An error while deleting a downloaded file. + public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId) async throws { + try await messageUpdater.deleteLocalAttachmentDownload(for: attachmentId) + } + /// Resends a failed attachment. /// - /// - Parameter attachment: The id of the attachment. + /// - Parameter attachmentId: The id of the attachment. /// /// - Throws: An error while sending a message to the Stream API. /// - Returns: The uploaded attachment with additional information like remote and thumbnail URLs. - @discardableResult public func resendAttachment(_ attachment: AttachmentId) async throws -> UploadedAttachment { + @discardableResult public func resendAttachment(_ attachmentId: AttachmentId) async throws -> UploadedAttachment { let attachmentQueueUploader = try client.backgroundWorker(of: AttachmentQueueUploader.self) - try await messageUpdater.resendAttachment(with: attachment) - return try await attachmentQueueUploader.waitForAPIRequest(attachmentId: attachment) + try await messageUpdater.resendAttachment(with: attachmentId) + return try await attachmentQueueUploader.waitForAPIRequest(attachmentId: attachmentId) } /// Invokes the ephemeral action specified by the attachment. diff --git a/Sources/StreamChat/StateLayer/ChatState.swift b/Sources/StreamChat/StateLayer/ChatState.swift index 7320f80d82d..b199a74b9e6 100644 --- a/Sources/StreamChat/StateLayer/ChatState.swift +++ b/Sources/StreamChat/StateLayer/ChatState.swift @@ -167,8 +167,12 @@ import Foundation // MARK: - Internal extension ChatState { + func setChannelQuery(_ query: ChannelQuery) { + channelQuery = query + } + func setChannelId(_ channelId: ChannelId) { - channelQuery = ChannelQuery(cid: channelId, channelQuery: channelQuery) + setChannelQuery(ChannelQuery(cid: channelId, channelQuery: channelQuery)) observe(channelId) } diff --git a/Sources/StreamChat/StateLayer/ConnectedUser.swift b/Sources/StreamChat/StateLayer/ConnectedUser.swift index 06d42a00284..7e77f537aa7 100644 --- a/Sources/StreamChat/StateLayer/ConnectedUser.swift +++ b/Sources/StreamChat/StateLayer/ConnectedUser.swift @@ -38,7 +38,7 @@ public final class ConnectedUser { /// Updates the currently logged-in user's data. /// - /// - Note: This does partial update and only updates existing data when a non-nil value is specified. + /// - Note: This does partial update and only updates existing data when a non-nil value is specified. Use ``unset`` for clearing the existing state. /// /// - Parameters: /// - name: The name to be set to the user. @@ -46,6 +46,7 @@ public final class ConnectedUser { /// - privacySettings: The privacy settings of the user. Example: If the user does not want to expose typing events or read events. /// - role: The role for the user. /// - extraData: Additional data associated with the user. + /// - unset: Existing values for specified fields are removed. For example, `image` or `name`. /// /// - Throws: An error while communicating with the Stream API or when user is not logged in. public func update( @@ -53,7 +54,8 @@ public final class ConnectedUser { imageURL: URL? = nil, privacySettings: UserPrivacySettings? = nil, role: UserRole? = nil, - extraData: [String: RawJSON] = [:] + extraData: [String: RawJSON] = [:], + unset: Set = [] ) async throws { try await currentUserUpdater.updateUserData( currentUserId: try currentUserId(), @@ -61,7 +63,8 @@ public final class ConnectedUser { imageURL: imageURL, privacySettings: privacySettings, role: role, - userExtraData: extraData + userExtraData: extraData, + unset: unset ) } @@ -176,6 +179,17 @@ public final class ConnectedUser { try await userUpdater.unflag(userId) } + // MARK: Managing Local Attachment Downloads + + /// Deletes all the local downloads of file attachments. + /// + /// - Parameter completion: Called when files have been deleted or when an error occured. + /// + /// - Throws: An error while deleting local downloads. + public func deleteAllLocalAttachmentDownloads() async throws { + try await currentUserUpdater.deleteAllLocalAttachmentDownloads() + } + // MARK: - Private private func currentUserId() throws -> UserId { diff --git a/Sources/StreamChat/Utils/Logger/Logger.swift b/Sources/StreamChat/Utils/Logger/Logger.swift index e5f463a92f8..d3daeb552b5 100644 --- a/Sources/StreamChat/Utils/Logger/Logger.swift +++ b/Sources/StreamChat/Utils/Logger/Logger.swift @@ -290,6 +290,10 @@ public class Logger { // it is important the closure is performed in the managedObjectContext's thread. let messageString = String(describing: message()) + // Read the thread name before dispatching the log to the desired destinations, + // so that we have the name of the thread that actually initiated the log. + let threadName = threadName + loggerQueue.async { [weak self] in guard let self = self else { return } @@ -298,7 +302,7 @@ public class Logger { level: level, date: Date(), message: messageString, - threadName: self.threadName, + threadName: threadName, functionName: functionName, fileName: fileName, lineNumber: lineNumber diff --git a/Sources/StreamChat/Workers/ChannelUpdater.swift b/Sources/StreamChat/Workers/ChannelUpdater.swift index 64175552713..72c7c3f6769 100644 --- a/Sources/StreamChat/Workers/ChannelUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelUpdater.swift @@ -589,6 +589,7 @@ class ChannelUpdater: Worker { id: .init(cid: cid, messageId: "", index: 0), // messageId and index won't be used for uploading type: type, payload: .init(), // payload won't be used for uploading + downloadingState: nil, uploadingState: .init( localFileURL: localFileURL, state: .pendingUpload, // will not be used diff --git a/Sources/StreamChat/Workers/CurrentUserUpdater.swift b/Sources/StreamChat/Workers/CurrentUserUpdater.swift index e442a24c7d4..ec5d37f1c15 100644 --- a/Sources/StreamChat/Workers/CurrentUserUpdater.swift +++ b/Sources/StreamChat/Workers/CurrentUserUpdater.swift @@ -17,6 +17,7 @@ class CurrentUserUpdater: Worker { /// - imageURL: Optionally provide a new image to be updated. /// - privacySettings: The privacy settings of the user. Example: If the user does not want to expose typing events or read events. /// - userExtraData: Optionally provide new user extra data to be updated. + /// - unset: Existing values for specified fields are removed. For example, `image` or `name`. /// - completion: Called when user is successfuly updated, or with error. func updateUserData( currentUserId: UserId, @@ -25,10 +26,11 @@ class CurrentUserUpdater: Worker { privacySettings: UserPrivacySettings?, role: UserRole?, userExtraData: [String: RawJSON]?, + unset: Set = [], completion: ((Error?) -> Void)? = nil ) { let params: [Any?] = [name, imageURL, userExtraData] - guard !params.allSatisfy({ $0 == nil }) else { + guard !params.allSatisfy({ $0 == nil }) || !unset.isEmpty else { log.warning("Update user request not performed. All provided data was nil.") completion?(nil) return @@ -43,7 +45,7 @@ class CurrentUserUpdater: Worker { ) apiClient - .request(endpoint: .updateUser(id: currentUserId, payload: payload)) { [weak self] in + .request(endpoint: .updateUser(id: currentUserId, payload: payload, unset: Array(unset))) { [weak self] in switch $0 { case let .success(response): self?.database.write({ (session) in @@ -155,6 +157,30 @@ class CurrentUserUpdater: Worker { } } } + + func deleteAllLocalAttachmentDownloads(completion: @escaping (Error?) -> Void) { + database.write({ session in + // Try to delete all the local files even when one of them happens to fail. + var latestError: Error? + let attachments = session.allLocallyDownloadedAttachments() + for attachment in attachments { + if let localRelativePath = attachment.localRelativePath { + let localURL = URL.streamAttachmentLocalStorageURL(forRelativePath: localRelativePath) + if FileManager.default.fileExists(atPath: localURL.path) { + do { + try FileManager.default.removeItem(at: localURL) + } catch { + latestError = error + } + } + } + attachment.clearLocalState() + } + log.info("Deleted local downloads for number of attachments: \(attachments.count)", subsystems: .database) + guard let latestError else { return } + throw latestError + }, completion: completion) + } /// Marks all channels for a user as read. /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. @@ -224,6 +250,14 @@ extension CurrentUserUpdater { } } + func deleteAllLocalAttachmentDownloads() async throws { + try await withCheckedThrowingContinuation { continuation in + deleteAllLocalAttachmentDownloads { error in + continuation.resume(with: error) + } + } + } + func fetchDevices(currentUserId: UserId) async throws -> [Device] { try await withCheckedThrowingContinuation { continuation in fetchDevices(currentUserId: currentUserId) { result in @@ -254,7 +288,8 @@ extension CurrentUserUpdater { imageURL: URL?, privacySettings: UserPrivacySettings?, role: UserRole?, - userExtraData: [String: RawJSON]? + userExtraData: [String: RawJSON]?, + unset: Set ) async throws { try await withCheckedThrowingContinuation { continuation in updateUserData( @@ -263,7 +298,8 @@ extension CurrentUserUpdater { imageURL: imageURL, privacySettings: privacySettings, role: role, - userExtraData: userExtraData + userExtraData: userExtraData, + unset: unset ) { error in continuation.resume(with: error) } diff --git a/Sources/StreamChat/Workers/MessageUpdater.swift b/Sources/StreamChat/Workers/MessageUpdater.swift index fe4276b9ab0..8f88d44decd 100644 --- a/Sources/StreamChat/Workers/MessageUpdater.swift +++ b/Sources/StreamChat/Workers/MessageUpdater.swift @@ -573,7 +573,100 @@ class MessageUpdater: Worker { } } } - + + static let minSignificantDownloadingProgressChange: Double = 0.01 + + func downloadAttachment( + _ attachment: ChatMessageAttachment, + completion: @escaping (Result, Error>) -> Void + ) where Payload: DownloadableAttachmentPayload { + let attachmentId = attachment.id + let localURL = URL.streamAttachmentLocalStorageURL(forRelativePath: attachment.relativeStoragePath) + apiClient.downloadFile( + from: attachment.remoteURL, + to: localURL, + progress: { [weak self] progress in + self?.updateDownloadProgress( + for: attachmentId, + payloadType: Payload.self, + newState: .downloading(progress: progress), + localURL: localURL + ) + }, + completion: { [weak self] error in + self?.updateDownloadProgress( + for: attachmentId, + payloadType: Payload.self, + newState: error == nil ? .downloaded : .downloadingFailed, + localURL: localURL, + completion: { result in + if let downloadError = error { + completion(.failure(downloadError)) + } else { + completion(result) + } + } + ) + } + ) + } + + func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: @escaping (Error?) -> Void) { + database.write({ session in + let dto = session.attachment(id: attachmentId) + guard let attachment = dto?.asAnyModel() else { + throw ClientError.AttachmentDoesNotExist(id: attachmentId) + } + guard attachment.downloadingState?.state == .downloaded else { return } + guard let localURL = attachment.downloadingState?.localFileURL else { return } + guard FileManager.default.fileExists(atPath: localURL.path) else { return } + try FileManager.default.removeItem(at: localURL) + dto?.clearLocalState() + }, completion: completion) + } + + private func updateDownloadProgress( + for attachmentId: AttachmentId, + payloadType: Payload.Type, + newState: LocalAttachmentDownloadState, + localURL: URL, + completion: ((Result, Error>) -> Void)? = nil + ) where Payload: DownloadableAttachmentPayload { + var model: ChatMessageAttachment? + database.write({ session in + guard let attachmentDTO = session.attachment(id: attachmentId) else { + throw ClientError.AttachmentDoesNotExist(id: attachmentId) + } + let needsUpdate: Bool = { + if case let .downloading(lastProgress) = attachmentDTO.localDownloadState, + case let .downloading(currentProgress) = newState { + return abs(currentProgress - lastProgress) >= Self.minSignificantDownloadingProgressChange + } else { + return attachmentDTO.localDownloadState != newState + } + }() + guard needsUpdate else { return } + attachmentDTO.localDownloadState = newState + // Store only the relative path because sandboxed base URL can change between app launchs + attachmentDTO.localRelativePath = localURL.relativePath + + guard completion != nil else { return } + guard let attachmentAnyModel = attachmentDTO.asAnyModel() else { + throw ClientError.AttachmentDoesNotExist(id: attachmentId) + } + guard let result = attachmentAnyModel.attachment(payloadType: Payload.self) else { + throw ClientError.AttachmentDownloading(id: attachmentId, reason: "Invalid payload type: \(Payload.self)") + } + model = result + }, completion: { error in + if let error { + completion?(.failure(error)) + } else if let model { + completion?(.success(model)) + } + }) + } + /// Updates local state of attachment with provided `id` to be enqueued by attachment uploader. /// - Parameters: /// - id: The attachment identifier. @@ -942,6 +1035,14 @@ extension MessageUpdater { } } + func deleteLocalAttachmentDownload(for attachmentId: AttachmentId) async throws { + try await withCheckedThrowingContinuation { continuation in + deleteLocalAttachmentDownload(for: attachmentId) { error in + continuation.resume(with: error) + } + } + } + func deleteMessage(messageId: MessageId, hard: Bool) async throws { try await withCheckedThrowingContinuation { continuation in deleteMessage(messageId: messageId, hard: hard) { error in @@ -974,6 +1075,16 @@ extension MessageUpdater { } } + func downloadAttachment( + _ attachment: ChatMessageAttachment + ) async throws -> ChatMessageAttachment where Payload: DownloadableAttachmentPayload { + try await withCheckedThrowingContinuation { continuation in + downloadAttachment(attachment) { result in + continuation.resume(with: result) + } + } + } + func editMessage( messageId: MessageId, text: String, diff --git a/Sources/StreamChatUI/Appearance+Images.swift b/Sources/StreamChatUI/Appearance+Images.swift index b935e9a212c..1d0e5ea6c42 100644 --- a/Sources/StreamChatUI/Appearance+Images.swift +++ b/Sources/StreamChatUI/Appearance+Images.swift @@ -234,8 +234,6 @@ public extension Appearance { public var fileAttachmentActionIcons: [LocalAttachmentState?: UIImage] { get { _fileAttachmentActionIcons ?? [ - // Uncomment when download feature is done - // .uploaded: download, .uploadingFailed: restart, nil: folder ] @@ -243,6 +241,18 @@ public extension Appearance { set { _fileAttachmentActionIcons = newValue } } + private var _fileAttachmentDownloadActionIcons: [LocalAttachmentDownloadState?: UIImage]? + public var fileAttachmentDownloadActionIcons: [LocalAttachmentDownloadState?: UIImage] { + get { _fileAttachmentDownloadActionIcons ?? + [ + .downloaded: share, + .downloadingFailed: download, + nil: download + ] + } + set { _fileAttachmentDownloadActionIcons = newValue } + } + public var camera: UIImage = loadImageSafely(with: "camera") public var bigPlay: UIImage = loadImageSafely(with: "play_big") diff --git a/Sources/StreamChatUI/ChatMessageList/Attachments/File/ChatFileAttachmentListView+ItemView.swift b/Sources/StreamChatUI/ChatMessageList/Attachments/File/ChatFileAttachmentListView+ItemView.swift index 170753ba8f9..8a6f3a3784d 100644 --- a/Sources/StreamChatUI/ChatMessageList/Attachments/File/ChatFileAttachmentListView+ItemView.swift +++ b/Sources/StreamChatUI/ChatMessageList/Attachments/File/ChatFileAttachmentListView+ItemView.swift @@ -114,27 +114,53 @@ extension ChatMessageFileAttachmentListView { // If we cannot fetch filename, let's use only content type. fileNameLabel.text = content?.payload.title ?? content?.type.rawValue - switch content?.uploadingState?.state { - case .uploaded, .none: - fileSizeLabel.text = content?.payload.file.sizeString - case .uploadingFailed: - fileSizeLabel.text = L10n.Message.Sending.attachmentUploadingFailed - default: - fileSizeLabel.text = content?.uploadingState?.fileUploadingProgress - } - - if let state = content?.uploadingState?.state { - actionIconImageView.image = appearance.fileAttachmentActionIcon(for: state) + let downloadState = content?.downloadingState?.state + let uploadState = content?.uploadingState?.state + + if let downloadState { + switch downloadState { + case .downloading: + fileSizeLabel.text = content?.downloadingState?.fileProgress + case .downloaded, .downloadingFailed: + fileSizeLabel.text = content?.payload.file.sizeString + } + } else if let uploadState { + switch uploadState { + case .uploading: + fileSizeLabel.text = content?.uploadingState?.fileProgress + case .uploadingFailed: + fileSizeLabel.text = L10n.Message.Sending.attachmentUploadingFailed + case .pendingUpload, .uploaded, .unknown: + fileSizeLabel.text = content?.payload.file.sizeString + } } else { - actionIconImageView.image = nil - } - - switch content?.uploadingState?.state { - case .pendingUpload, .uploading: - loadingIndicator.isVisible = true - default: - loadingIndicator.isVisible = false + fileSizeLabel.text = content?.payload.file.sizeString } + + actionIconImageView.image = { + guard let fileSize = content?.file.size, fileSize > 0 else { return nil } + guard content?.file.type != .unknown else { return nil } + return appearance.fileAttachmentActionIcon( + uploadState: uploadState, + downloadState: downloadState, + downloadingEnabled: Components.default.isDownloadFileAttachmentsEnabled + ) + }() + + loadingIndicator.isVisible = { + if let downloadState, case .downloading = downloadState { + return true + } + if let uploadState { + switch uploadState { + case .pendingUpload, .uploading: + return true + default: + return false + } + } + return false + }() if content?.file.type == .unknown { fileNameLabel.text = L10n.Message.unsupportedAttachment diff --git a/Sources/StreamChatUI/ChatMessageList/Attachments/Unsupported/UnsupportedAttachmentViewInjector.swift b/Sources/StreamChatUI/ChatMessageList/Attachments/Unsupported/UnsupportedAttachmentViewInjector.swift index b163b25f881..9623510c893 100644 --- a/Sources/StreamChatUI/ChatMessageList/Attachments/Unsupported/UnsupportedAttachmentViewInjector.swift +++ b/Sources/StreamChatUI/ChatMessageList/Attachments/Unsupported/UnsupportedAttachmentViewInjector.swift @@ -44,6 +44,7 @@ public class UnsupportedAttachmentViewInjector: AttachmentViewInjector { file: .init(type: .unknown, size: 0, mimeType: nil), extraData: nil ), + downloadingState: $0.downloadingState, uploadingState: $0.uploadingState ) } diff --git a/Sources/StreamChatUI/ChatMessageList/Attachments/UploadingOverlayView.swift b/Sources/StreamChatUI/ChatMessageList/Attachments/UploadingOverlayView.swift index 230ea176edf..a6d59fa41e6 100644 --- a/Sources/StreamChatUI/ChatMessageList/Attachments/UploadingOverlayView.swift +++ b/Sources/StreamChatUI/ChatMessageList/Attachments/UploadingOverlayView.swift @@ -154,15 +154,21 @@ open class UploadingOverlayView: _View, ThemeProvider { } extension Appearance { - func fileAttachmentActionIcon(for state: LocalAttachmentState) -> UIImage? { - images.fileAttachmentActionIcons[state] + func fileAttachmentActionIcon(uploadState: LocalAttachmentState?, downloadState: LocalAttachmentDownloadState?, downloadingEnabled: Bool) -> UIImage? { + if let uploadState { + return images.fileAttachmentActionIcons[uploadState] + } + if downloadingEnabled { + return images.fileAttachmentDownloadActionIcons[downloadState] + } + return nil } } -extension AttachmentUploadingState { - var fileUploadingProgress: String { - switch state { - case let .uploading(progress): +extension LocalAttachmentState { + func progressDescription(for file: AttachmentFile) -> String { + switch self { + case .uploading(let progress): let uploadedByteCount = Int64(Double(file.size) * progress) let uploadedSize = AttachmentFile.sizeFormatter.string(fromByteCount: uploadedByteCount) return "\(uploadedSize)/\(file.sizeString)" @@ -173,3 +179,35 @@ extension AttachmentUploadingState { } } } + +private extension AttachmentFile { + func progressDescription(for progress: Double) -> String { + let uploadedByteCount = Int64(Double(size) * progress) + let uploadedSize = AttachmentFile.sizeFormatter.string(fromByteCount: uploadedByteCount) + return "\(uploadedSize) / \(sizeString)" + } +} + +extension AttachmentDownloadingState { + var fileProgress: String { + switch state { + case .downloading(let progress): + return file?.progressDescription(for: progress) ?? "" + case .downloaded, .downloadingFailed: + return file?.sizeString ?? "" + } + } +} + +extension AttachmentUploadingState { + var fileProgress: String { + switch state { + case .uploading(let progress): + return file.progressDescription(for: progress) + case .pendingUpload: + return "0 / \(file.sizeString)" + case .uploaded, .uploadingFailed, .unknown: + return file.sizeString + } + } +} diff --git a/Sources/StreamChatUI/ChatMessageList/Attachments/VoiceRecording/ChatMessageVoiceRecordingAttachmentListView+ItemView.swift b/Sources/StreamChatUI/ChatMessageList/Attachments/VoiceRecording/ChatMessageVoiceRecordingAttachmentListView+ItemView.swift index 8e733085d0e..40e4a3a5425 100644 --- a/Sources/StreamChatUI/ChatMessageList/Attachments/VoiceRecording/ChatMessageVoiceRecordingAttachmentListView+ItemView.swift +++ b/Sources/StreamChatUI/ChatMessageList/Attachments/VoiceRecording/ChatMessageVoiceRecordingAttachmentListView+ItemView.swift @@ -194,7 +194,7 @@ extension ChatMessageVoiceRecordingAttachmentListView { case .uploadingFailed: fileSizeLabel.text = L10n.Message.Sending.attachmentUploadingFailed default: - fileSizeLabel.text = content?.uploadingState?.fileUploadingProgress + fileSizeLabel.text = content?.uploadingState?.fileProgress } switch content?.uploadingState?.state { @@ -206,7 +206,7 @@ extension ChatMessageVoiceRecordingAttachmentListView { switch content?.uploadingState?.state { case .uploadingFailed: - fileIconImageView.image = appearance.fileAttachmentActionIcon(for: .uploadingFailed) + fileIconImageView.image = appearance.fileAttachmentActionIcon(uploadState: .uploadingFailed, downloadState: nil, downloadingEnabled: false) default: fileIconImageView.image = appearance.images.fileAac } diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift index 4247f7b71e9..3eec30f1290 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift @@ -979,13 +979,31 @@ open class ChatMessageListVC: _ViewController, } open func didTapActionOnAttachment(_ attachment: ChatMessageFileAttachment, at indexPath: IndexPath?) { - switch attachment.uploadingState?.state { - case .uploadingFailed: - client - .messageController(cid: attachment.id.cid, messageId: attachment.id.messageId) - .restartFailedAttachmentUploading(with: attachment.id) - default: - break + if let uploadingState = attachment.uploadingState { + switch uploadingState.state { + case .uploadingFailed: + client + .messageController(cid: attachment.id.cid, messageId: attachment.id.messageId) + .restartFailedAttachmentUploading(with: attachment.id) + default: + break + } + } else if Components.default.isDownloadFileAttachmentsEnabled { + if let downloadingState = attachment.downloadingState, downloadingState.state == .downloaded, let localFileURL = downloadingState.localFileURL { + guard let indexPath, let cell = listView.cellForRow(at: indexPath) else { return } + let activityViewController = UIActivityViewController(activityItems: [localFileURL], applicationActivities: nil) + activityViewController.popoverPresentationController?.sourceView = cell + present(activityViewController, animated: true) + } else { + let chat = client.makeChat(for: attachment.id.cid) + _Concurrency.Task { + do { + try await chat.downloadAttachment(attachment) + } catch { + log.debug("Downloaded attachment for id \(attachment.id)") + } + } + } } } diff --git a/Sources/StreamChatUI/Components.swift b/Sources/StreamChatUI/Components.swift index 694c5187e7c..82e5bf03975 100644 --- a/Sources/StreamChatUI/Components.swift +++ b/Sources/StreamChatUI/Components.swift @@ -311,6 +311,9 @@ public struct Components { /// The view that displays the number of unread messages in the chat. public var messageHeaderDecorationView: ChatChannelMessageHeaderDecoratorView.Type = ChatChannelMessageHeaderDecoratorView.self + + /// A flag which determines if download action is shown for file attachments. + public var isDownloadFileAttachmentsEnabled = false // MARK: - Reactions diff --git a/Sources/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist index 4f55c7aea35..24ce17378e1 100644 --- a/Sources/StreamChatUI/Info.plist +++ b/Sources/StreamChatUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.62.0 + 4.63.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec index 626cd324897..6d4646f6b17 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.62.0" + spec.version = "4.63.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 11158ae6d34..c297940e5eb 100644 --- a/StreamChat.podspec +++ b/StreamChat.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "StreamChat" - spec.version = "4.62.0" + spec.version = "4.63.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 68f3a47d2b1..6915fd4433a 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -238,6 +238,8 @@ 43F4750C26F4E4FF0009487D /* ChatMessageReactionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F4750B26F4E4FF0009487D /* ChatMessageReactionItemView.swift */; }; 43F4750E26FB247C0009487D /* ChatReactionPickerReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F4750D26FB247C0009487D /* ChatReactionPickerReactionsView.swift */; }; 4A4E184728D06F260062378D /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 4A4E184528D06CA30062378D /* Documentation.docc */; }; + 4F05C0712C8832C40085B4B7 /* URLRequest+cURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05C0702C8832C40085B4B7 /* URLRequest+cURL.swift */; }; + 4F05C0722C8832C40085B4B7 /* URLRequest+cURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05C0702C8832C40085B4B7 /* URLRequest+cURL.swift */; }; 4F05ECB82B6CCA4900641820 /* DifferenceKit+Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05ECB72B6CCA4900641820 /* DifferenceKit+Stream.swift */; }; 4F05ECB92B6CCA4900641820 /* DifferenceKit+Stream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05ECB72B6CCA4900641820 /* DifferenceKit+Stream.swift */; }; 4F072F032BC008D9006A66CA /* StateLayerDatabaseObserver_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F072F022BC008D9006A66CA /* StateLayerDatabaseObserver_Tests.swift */; }; @@ -255,6 +257,8 @@ 4F1BEE7C2BE3851200B6685C /* ReactionListState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1BEE7B2BE3851200B6685C /* ReactionListState+Observer.swift */; }; 4F1BEE7D2BE3851200B6685C /* ReactionListState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1BEE7B2BE3851200B6685C /* ReactionListState+Observer.swift */; }; 4F1BEE7F2BE38B5500B6685C /* ReactionList_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1BEE7E2BE38B5500B6685C /* ReactionList_Tests.swift */; }; + 4F1FB7D62C7DE22D00C47C2A /* ChatMessageAudioAttachment_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1FB7D52C7DE22D00C47C2A /* ChatMessageAudioAttachment_Mock.swift */; }; + 4F1FB7D82C7DEC6600C47C2A /* ChatMessageVideoAttachment_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1FB7D72C7DEC6600C47C2A /* ChatMessageVideoAttachment_Mock.swift */; }; 4F427F662BA2F43200D92238 /* ConnectedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F427F652BA2F43200D92238 /* ConnectedUser.swift */; }; 4F427F672BA2F43200D92238 /* ConnectedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F427F652BA2F43200D92238 /* ConnectedUser.swift */; }; 4F427F692BA2F52100D92238 /* ConnectedUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F427F682BA2F52100D92238 /* ConnectedUserState.swift */; }; @@ -303,6 +307,7 @@ 4F97F27A2BA88936001C4D66 /* MessageSearchState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F97F2792BA88936001C4D66 /* MessageSearchState+Observer.swift */; }; 4F97F27B2BA88936001C4D66 /* MessageSearchState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F97F2792BA88936001C4D66 /* MessageSearchState+Observer.swift */; }; 4FB4AB9F2BAD6DBD00712C4E /* Chat_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB4AB9E2BAD6DBD00712C4E /* Chat_Tests.swift */; }; + 4FBD840B2C774E5C00B1E680 /* AttachmentDownloader_Spy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBD840A2C774E5C00B1E680 /* AttachmentDownloader_Spy.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 */; }; @@ -323,6 +328,8 @@ 4FE6E1AE2BAC7A1B00C80AF1 /* UserListState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6E1AC2BAC7A1B00C80AF1 /* UserListState+Observer.swift */; }; 4FF2A80D2B8E011000941A64 /* ChatState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF2A80C2B8E011000941A64 /* ChatState+Observer.swift */; }; 4FF2A80E2B8E011000941A64 /* ChatState+Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF2A80C2B8E011000941A64 /* ChatState+Observer.swift */; }; + 4FF9B2682C6F697300A3B711 /* AttachmentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */; }; + 4FF9B2692C6F697300A3B711 /* AttachmentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */; }; 4FFB5EA02BA0507900F0454F /* Collection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFB5E9F2BA0507900F0454F /* Collection+Extensions.swift */; }; 4FFB5EA12BA0507900F0454F /* Collection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFB5E9F2BA0507900F0454F /* Collection+Extensions.swift */; }; 6428DD5526201DCC0065DA1D /* BannerShowingConnectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6428DD5426201DCC0065DA1D /* BannerShowingConnectionDelegate.swift */; }; @@ -3079,6 +3086,7 @@ 43F4750D26FB247C0009487D /* ChatReactionPickerReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReactionPickerReactionsView.swift; sourceTree = ""; }; 4A4E184528D06CA30062378D /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = ""; }; 4A51230029D3170C005CEA9B /* docusaurus */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docusaurus; sourceTree = ""; }; + 4F05C0702C8832C40085B4B7 /* URLRequest+cURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+cURL.swift"; sourceTree = ""; }; 4F05ECB72B6CCA4900641820 /* DifferenceKit+Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Stream.swift"; sourceTree = ""; }; 4F072F022BC008D9006A66CA /* StateLayerDatabaseObserver_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateLayerDatabaseObserver_Tests.swift; sourceTree = ""; }; 4F12DC8A2B70DE4C009E48CC /* DifferenceKit+Stream_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Stream_Tests.swift"; sourceTree = ""; }; @@ -3090,6 +3098,8 @@ 4F1BEE782BE384FE00B6685C /* ReactionListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListState.swift; sourceTree = ""; }; 4F1BEE7B2BE3851200B6685C /* ReactionListState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReactionListState+Observer.swift"; sourceTree = ""; }; 4F1BEE7E2BE38B5500B6685C /* ReactionList_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionList_Tests.swift; sourceTree = ""; }; + 4F1FB7D52C7DE22D00C47C2A /* ChatMessageAudioAttachment_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageAudioAttachment_Mock.swift; sourceTree = ""; }; + 4F1FB7D72C7DEC6600C47C2A /* ChatMessageVideoAttachment_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageVideoAttachment_Mock.swift; sourceTree = ""; }; 4F427F652BA2F43200D92238 /* ConnectedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedUser.swift; sourceTree = ""; }; 4F427F682BA2F52100D92238 /* ConnectedUserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedUserState.swift; sourceTree = ""; }; 4F427F6B2BA2F53200D92238 /* ConnectedUserState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectedUserState+Observer.swift"; sourceTree = ""; }; @@ -3118,6 +3128,7 @@ 4F97F2762BA87E30001C4D66 /* MessageSearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSearchState.swift; sourceTree = ""; }; 4F97F2792BA88936001C4D66 /* MessageSearchState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSearchState+Observer.swift"; sourceTree = ""; }; 4FB4AB9E2BAD6DBD00712C4E /* Chat_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chat_Tests.swift; sourceTree = ""; }; + 4FBD840A2C774E5C00B1E680 /* AttachmentDownloader_Spy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDownloader_Spy.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 = ""; }; 4FD2BE522B9AEE3500FFC6F2 /* StreamCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamCollection.swift; sourceTree = ""; }; @@ -3129,6 +3140,7 @@ 4FE6E1A92BAC79F400C80AF1 /* MemberListState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MemberListState+Observer.swift"; sourceTree = ""; }; 4FE6E1AC2BAC7A1B00C80AF1 /* UserListState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserListState+Observer.swift"; sourceTree = ""; }; 4FF2A80C2B8E011000941A64 /* ChatState+Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatState+Observer.swift"; sourceTree = ""; }; + 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDownloader.swift; sourceTree = ""; }; 4FFB5E9F2BA0507900F0454F /* Collection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extensions.swift"; sourceTree = ""; }; 6428DD5426201DCC0065DA1D /* BannerShowingConnectionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerShowingConnectionDelegate.swift; sourceTree = ""; }; 647F66D4261E22C200111B19 /* DemoConnectionBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoConnectionBannerView.swift; sourceTree = ""; }; @@ -5178,6 +5190,14 @@ path = StateLayer; sourceTree = ""; }; + 4FF9B2662C6F695C00A3B711 /* AttachmentDownloader */ = { + isa = PBXGroup; + children = ( + 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */, + ); + path = AttachmentDownloader; + sourceTree = ""; + }; 790881AB254327C800896F03 /* StreamChat */ = { isa = PBXGroup; children = ( @@ -5775,6 +5795,7 @@ 79DDF80D249CB920002F4412 /* RequestDecoder.swift */, 7964F3BB249A5E60002A09EC /* RequestEncoder.swift */, ADB951A3291BD7F700800554 /* CDNClient */, + 4FF9B2662C6F695C00A3B711 /* AttachmentDownloader */, ADB951A7291BD85300800554 /* AttachmentUploader */, 79877A122498E4EE00015F8B /* Endpoints */, ); @@ -6658,10 +6679,12 @@ children = ( A344075C27D753530044F150 /* AnyAttachmentPayload_Mock.swift */, A344075A27D753530044F150 /* AttachmentUploadingState_Mock.swift */, + 4F1FB7D52C7DE22D00C47C2A /* ChatMessageAudioAttachment_Mock.swift */, A344075727D753530044F150 /* ChatMessageFileAttachment_Mock.swift */, - 40A2961929F8244500E0C186 /* ChatMessageVoiceRecordingAttachment_Mock.swift */, A344075927D753530044F150 /* ChatMessageImageAttachment_Mock.swift */, A344075B27D753530044F150 /* ChatMessageLinkAttachment_Mock.swift */, + 4F1FB7D72C7DEC6600C47C2A /* ChatMessageVideoAttachment_Mock.swift */, + 40A2961929F8244500E0C186 /* ChatMessageVoiceRecordingAttachment_Mock.swift */, ); path = Attachments; sourceTree = ""; @@ -7436,8 +7459,9 @@ isa = PBXGroup; children = ( 4FFB5E9F2BA0507900F0454F /* Collection+Extensions.swift */, - A36C39F42860680A0004EB7E /* URL+EnrichedURL.swift */, 4F51519B2BC66FBE001B7152 /* Task+Extensions.swift */, + A36C39F42860680A0004EB7E /* URL+EnrichedURL.swift */, + 4F05C0702C8832C40085B4B7 /* URLRequest+cURL.swift */, ); path = Extensions; sourceTree = ""; @@ -8126,6 +8150,7 @@ children = ( 792921C624C047DD00116BBB /* APIClient_Spy.swift */, 649968D8264E6A71000515AB /* CDNClient_Spy.swift */, + 4FBD840A2C774E5C00B1E680 /* AttachmentDownloader_Spy.swift */, ADB951A8291C1DDC00800554 /* AttachmentUploader_Spy.swift */, 79896D602507B0DD00BA8F1C /* ChannelListUpdater_Spy.swift */, C186BFB127AAF7E00099CCA6 /* ChatChannelController_Spy.swift */, @@ -10819,6 +10844,7 @@ A3C3BC7527E8AA7000224761 /* Endpoint+Mock.swift in Sources */, A344077527D753530044F150 /* ChatChannel_Mock.swift in Sources */, A344078527D753530044F150 /* UnreadCount.swift in Sources */, + 4F1FB7D62C7DE22D00C47C2A /* ChatMessageAudioAttachment_Mock.swift in Sources */, A3D15D8827E9D4B5006B34D7 /* VirtualTime.swift in Sources */, 40A2961A29F8244500E0C186 /* ChatMessageVoiceRecordingAttachment_Mock.swift in Sources */, A3C3BC6927E8AA4300224761 /* TestBuilder.swift in Sources */, @@ -10892,6 +10918,7 @@ A3C3BC2327E87F1800224761 /* ChatChannelWatcherListController_Mock.swift in Sources */, A3C3BC3627E87F3200224761 /* InternetConnection_Mock.swift in Sources */, A3C3BC4627E87F5C00224761 /* CurrentUserUpdater_Mock.swift in Sources */, + 4F1FB7D82C7DEC6600C47C2A /* ChatMessageVideoAttachment_Mock.swift in Sources */, A3C3BC5E27E8AA0A00224761 /* String+Unique.swift in Sources */, CF5DCBC42837F11000CCA48C /* ScheduledStreamTimer_Mock.swift in Sources */, A3C3BC4527E87F5C00224761 /* EventSender_Mock.swift in Sources */, @@ -10973,6 +11000,7 @@ A3C3BC2E27E87F2900224761 /* RequestRecorderURLProtocol_Mock.swift in Sources */, A3C3BC3D27E87F5100224761 /* WebSocketEngine_Mock.swift in Sources */, A3C3BC6627E8AA0A00224761 /* URL+Unique.swift in Sources */, + 4FBD840B2C774E5C00B1E680 /* AttachmentDownloader_Spy.swift in Sources */, A34ECB5B27F5D0BF00A804C1 /* TestDataModel.xcdatamodeld in Sources */, A3C3BC5D27E8AA0A00224761 /* ChatUser+Unique.swift in Sources */, A3C3BC6E27E8AA4300224761 /* PhotoMetaData.swift in Sources */, @@ -11035,6 +11063,7 @@ 841BAA0A2BCE9B57000C73E4 /* CreatePollOptionRequestBody.swift in Sources */, 40789D2B29F6AC500018C2BB /* AudioRecordingContext.swift in Sources */, 84DCB853269F569A006CDF32 /* EventsController+SwiftUI.swift in Sources */, + 4FF9B2692C6F697300A3B711 /* AttachmentDownloader.swift in Sources */, 40789D3129F6AC500018C2BB /* AudioRecording.swift in Sources */, 841BAA4B2BD1CCC0000C73E4 /* PollVoteDTO.swift in Sources */, 88381E65258258C20047A6A3 /* FileUploadPayload.swift in Sources */, @@ -11148,6 +11177,7 @@ 88E26D6E2580F34B00F55AB5 /* AttachmentQueueUploader.swift in Sources */, 88A00DD02525F08000259AB4 /* ModerationEndpoints.swift in Sources */, 79A0E9B02498C09900E9BD50 /* ConnectionStatus.swift in Sources */, + 4F05C0712C8832C40085B4B7 /* URLRequest+cURL.swift in Sources */, 799C9439247D2FB9001F1104 /* ChannelDTO.swift in Sources */, 799C9443247D3DA7001F1104 /* APIClient.swift in Sources */, 4F14F1262BBBDD7400B1074E /* StateLayerDatabaseObserver.swift in Sources */, @@ -12078,6 +12108,7 @@ C121E872274544AF00023E4C /* AttachmentDTO.swift in Sources */, C121E874274544AF00023E4C /* UserDTO.swift in Sources */, 8413D2F32BDDAAEE005ADA4E /* PollVoteListController+Combine.swift in Sources */, + 4FF9B2682C6F697300A3B711 /* AttachmentDownloader.swift in Sources */, 841BAA0E2BCE9F44000C73E4 /* UpdatePollOptionRequestBody.swift in Sources */, C186BFB027AADB410099CCA6 /* SyncOperations.swift in Sources */, AD78568D298B268F00C2FEAD /* ChannelControllerDelegate.swift in Sources */, @@ -12233,6 +12264,7 @@ C121E8C5274544B100023E4C /* ChannelMemberListQuery.swift in Sources */, 40789D3029F6AC500018C2BB /* AudioRecordingDelegate.swift in Sources */, C121E8C6274544B100023E4C /* ChannelWatcherListQuery.swift in Sources */, + 4F05C0722C8832C40085B4B7 /* URLRequest+cURL.swift in Sources */, AD37D7CB2BC98A5300800D8C /* ThreadReadDTO.swift in Sources */, C121E8C7274544B100023E4C /* ChannelListQuery.swift in Sources */, 841BAA0B2BCE9B57000C73E4 /* CreatePollOptionRequestBody.swift in Sources */, diff --git a/StreamChatArtifacts.json b/StreamChatArtifacts.json index 32af7ff6b45..bc00e4ff596 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"} \ 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"} \ No newline at end of file diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec index 9ebdec6ed6f..764ee488be2 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.62.0" + spec.version = "4.63.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 6fa4b8f5228..0c329663a4d 100644 --- a/StreamChatUI.podspec +++ b/StreamChatUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "StreamChatUI" - spec.version = "4.62.0" + spec.version = "4.63.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/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json index 25036c30440..e80dc5ffda8 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json @@ -1,6 +1,6 @@ { "channel": { - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "config": { "automod": "AI", "automod_behavior": "block", @@ -49,25 +49,29 @@ "uploads": true, "url_enrichment": true }, - "created_at": "2024-08-02T10:15:59.830311Z", + "created_at": "2024-09-01T00:16:07.852577Z", "created_by": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "disabled": false, "frozen": false, "hidden": false, - "id": "36d17a7e-50b8-11ef-a447-1eebe6661088", - "last_message_at": "2024-08-02T10:16:01.297535Z", + "id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "last_message_at": "2024-09-01T00:16:11.276243Z", "member_count": 4, "name": "Sync Mock Server", "own_capabilities": [ @@ -106,29 +110,32 @@ "upload-file" ], "type": "messaging", - "updated_at": "2024-08-02T10:15:59.830312Z" + "updated_at": "2024-09-01T00:16:07.852577Z" }, - "duration": "25.14ms", + "duration": "24.12ms", "members": [ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Serenno", + "blocked_user_ids": [], "created_at": "2024-04-22T06:42:08.562992Z", "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", - "last_active": "2024-07-18T22:22:54.993937Z", + "language": "", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", + "teams": [], "updated_at": "2024-07-11T05:45:57.296628Z" }, "user_id": "count_dooku" @@ -136,22 +143,25 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Corellia", + "blocked_user_ids": [], "created_at": "2024-04-04T09:18:11.060737Z", "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", - "last_active": "2024-07-31T22:17:14.021725Z", + "language": "", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, "role": "user", + "teams": [], "updated_at": "2024-06-25T14:17:08.295189Z" }, "user_id": "han_solo" @@ -159,44 +169,49 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" }, { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:16:01.569225Z", + "created_at": "2024-09-01T00:16:11.760869Z", "notifications_muted": false, "role": "admin", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:16:01.569225Z", + "updated_at": "2024-09-01T00:16:11.760869Z", "user": { "banned": false, "birthland": "Polis Massa", + "blocked_user_ids": [], "created_at": "2024-04-04T09:42:00.68335Z", "id": "leia_organa", "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png", - "language": "de", - "last_active": "2024-08-02T06:35:06.594322Z", + "language": "en", + "last_active": "2024-08-30T14:16:49.393869Z", "name": "Leia Organa", "online": false, "private_settings": { @@ -208,7 +223,8 @@ } }, "role": "admin", - "updated_at": "2024-07-15T12:34:51.215666Z" + "teams": [], + "updated_at": "2024-08-29T18:09:28.42841Z" }, "user_id": "leia_organa" } diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json index 00303c2dc1f..126cd3936d1 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json @@ -1,4 +1,4 @@ { - "duration": "285.55ms", - "file": "https://frankfurt.stream-io-cdn.com/102399/images/7a8e9b16-cf20-47bb-bfb1-60f763dbf126.yoda.jpg?Key-Pair-Id=APKAIHG36VEWPDULE23Q&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9mcmFua2Z1cnQuc3RyZWFtLWlvLWNkbi5jb20vMTAyMzk5L2ltYWdlcy83YThlOWIxNi1jZjIwLTQ3YmItYmZiMS02MGY3NjNkYmYxMjYueW9kYS5qcGc~Km9oPTUxNSpvdz01MTUqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzIzODAzMzYxfX19XX0_&Signature=ViVkW27dg8QYMyhVV0i5FWOwb-gWQyALlmET7L2yABzH~SbCLq~Lc6zgrw7iXXdOUgpFbeoV7h1ky3wN37kWG5IJBgT9O19K0bpph5~KqKfnwv~1V7dScVZy433qFFdn-C0D5NOJebLK3tLIItBj~4ApBLycegpRBvUl1w9ECSIDvercya39OWGYiAT3mK1~ZtHn2Rx2gvqUaqyAL-~S1sNm0hKANz7MNvEFNXgzxKp8VBR31exwh7Bdp8oCjfBtqWZ~89aZFPvsZs-SXNtJ-LsLMeaFqOBY~hBJqJzPGFJ4kLjHlcYJ5qrRxudGGLuDjGMEp618At4DNYHeVaDyNw__&oh=515&ow=515" + "duration": "283.24ms", + "file": "https://frankfurt.stream-io-cdn.com/102399/images/897394c8-9979-4f1b-8b11-49b12ab78294.yoda.jpg?Key-Pair-Id=APKAIHG36VEWPDULE23Q&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9mcmFua2Z1cnQuc3RyZWFtLWlvLWNkbi5jb20vMTAyMzk5L2ltYWdlcy84OTczOTRjOC05OTc5LTRmMWItOGIxMS00OWIxMmFiNzgyOTQueW9kYS5qcGc~Km9oPTUxNSpvdz01MTUqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzI2MzU5MzcyfX19XX0_&Signature=ORg6Cw~Hhrry7D4D6wHWBhU4etIQBXBwnMb3uqfe3s1IEsqo6xhBmGy6V1ukaMDHaF4xaomzfWeim6h6zaaACnQzCmM8rA6xW8nfJxuIpOKSuJXcHdvli8vnKqQHnDiAyZ7~OeRSwjRr-oJLuyqvMm8TgL~hQEVle1sPO0kI0ttVLi9bvDIjpVW6MOw6OfYBdZ9KUVbCk0IYIHjxQJiZWScNAqwMyDVnjx6PDyHjqx7VFETbTgqXVw6MGwIp4Vk3pWNte0GAAAwlK1DOdrZBZwoVEoJ0rsndoLBUHRDzAH~HaETRNT6wX0agVTxvelFlOsFMpOb56J3cr7g2q0zGkw__&oh=515&ow=515" } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_creation.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_creation.json index 5c62f58a40c..feadbe44d2a 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_creation.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_creation.json @@ -1,7 +1,7 @@ { "channel": { "blocked": false, - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "config": { "automod": "AI", "automod_behavior": "block", @@ -50,24 +50,28 @@ "uploads": true, "url_enrichment": true }, - "created_at": "2024-08-02T10:15:59.830311Z", + "created_at": "2024-09-01T00:16:07.852577Z", "created_by": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "disabled": false, "frozen": false, "hidden": false, - "id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "member_count": 3, "name": "Sync Mock Server", "own_capabilities": [ @@ -106,29 +110,32 @@ "upload-file" ], "type": "messaging", - "updated_at": "2024-08-02T10:15:59.830312Z" + "updated_at": "2024-09-01T00:16:07.852577Z" }, - "duration": "71.47ms", + "duration": "225.28ms", "members": [ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Serenno", + "blocked_user_ids": [], "created_at": "2024-04-22T06:42:08.562992Z", "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", - "last_active": "2024-07-18T22:22:54.993937Z", + "language": "", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", + "teams": [], "updated_at": "2024-07-11T05:45:57.296628Z" }, "user_id": "count_dooku" @@ -136,22 +143,25 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Corellia", + "blocked_user_ids": [], "created_at": "2024-04-04T09:18:11.060737Z", "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", - "last_active": "2024-07-31T22:17:14.021725Z", + "language": "", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, "role": "user", + "teams": [], "updated_at": "2024-06-25T14:17:08.295189Z" }, "user_id": "han_solo" @@ -159,24 +169,28 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" } @@ -184,31 +198,35 @@ "membership": { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } }, "messages": [], "pinned_messages": [], "read": [ { - "last_read": "2024-08-02T10:15:59.849616704Z", + "last_read": "2024-09-01T00:16:07.916440838Z", "unread_messages": 0, "user": { "banned": false, @@ -218,7 +236,7 @@ "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", "language": "", - "last_active": "2024-07-18T22:22:54.993937Z", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", @@ -227,7 +245,7 @@ } }, { - "last_read": "2024-08-02T10:15:59.849616704Z", + "last_read": "2024-09-01T00:16:07.916440838Z", "unread_messages": 0, "user": { "banned": false, @@ -237,24 +255,16 @@ "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", "language": "", - "last_active": "2024-07-31T22:17:14.021725Z", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "user", "teams": [], "updated_at": "2024-06-25T14:17:08.295189Z" } }, { - "last_read": "2024-08-02T10:15:59.849616704Z", + "last_read": "2024-09-01T00:16:07.916440838Z", "unread_messages": 0, "user": { "banned": false, @@ -264,20 +274,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } ], diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json index 663ab6ea33e..1eb3d3e344f 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json @@ -1,6 +1,6 @@ { "channel": { - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "config": { "automod": "AI", "automod_behavior": "block", @@ -49,29 +49,29 @@ "uploads": true, "url_enrichment": true }, - "created_at": "2024-08-02T10:15:59.830311Z", + "created_at": "2024-09-01T00:16:07.852577Z", "created_by": null, - "deleted_at": "2024-08-02T10:16:02.912715Z", + "deleted_at": "2024-09-01T00:16:14.809395Z", "disabled": false, "frozen": false, - "id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "members": [ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Serenno", "created_at": "2024-04-22T06:42:08.562992Z", "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", - "last_active": "2024-07-18T22:22:54.993937Z", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", @@ -82,19 +82,19 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Corellia", "created_at": "2024-04-04T09:18:11.060737Z", "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", - "last_active": "2024-07-31T22:17:14.021725Z", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, "role": "user", @@ -105,12 +105,12 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", @@ -118,31 +118,33 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" }, { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:16:01.569225Z", + "created_at": "2024-09-01T00:16:11.760869Z", "notifications_muted": false, "role": "admin", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:16:01.569225Z", + "updated_at": "2024-09-01T00:16:11.760869Z", "user": { "banned": false, "birthland": "Polis Massa", "created_at": "2024-04-04T09:42:00.68335Z", "id": "leia_organa", "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png", - "language": "de", - "last_active": "2024-08-02T06:35:06.594322Z", + "language": "en", + "last_active": "2024-08-30T14:16:49.393869Z", "name": "Leia Organa", "online": false, "private_settings": { @@ -154,12 +156,12 @@ } }, "role": "admin", - "updated_at": "2024-07-15T12:34:51.215666Z" + "updated_at": "2024-08-29T18:09:28.42841Z" }, "user_id": "leia_organa" } ], - "truncated_at": "2024-08-02T10:16:02.912715Z", + "truncated_at": "2024-09-01T00:16:14.809395Z", "truncated_by": { "banned": false, "birthland": "Tatooine", @@ -167,14 +169,16 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "type": "messaging", - "updated_at": "2024-08-02T10:16:02.799276Z" + "updated_at": "2024-09-01T00:16:14.540443Z" }, - "duration": "17.95ms" + "duration": "33.01ms" } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json index 40a858b96ed..d8e4157000d 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json @@ -3,7 +3,7 @@ { "channel": { "blocked": false, - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "config": { "automod": "AI", "automod_behavior": "block", @@ -52,24 +52,28 @@ "uploads": true, "url_enrichment": true }, - "created_at": "2024-08-02T10:15:59.830311Z", + "created_at": "2024-09-01T00:16:07.852577Z", "created_by": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "disabled": false, "frozen": false, "hidden": false, - "id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "member_count": 3, "name": "Sync Mock Server", "own_capabilities": [ @@ -108,28 +112,31 @@ "upload-file" ], "type": "messaging", - "updated_at": "2024-08-02T10:15:59.830312Z" + "updated_at": "2024-09-01T00:16:07.852577Z" }, "members": [ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Serenno", + "blocked_user_ids": [], "created_at": "2024-04-22T06:42:08.562992Z", "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", - "last_active": "2024-07-18T22:22:54.993937Z", + "language": "", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", + "teams": [], "updated_at": "2024-07-11T05:45:57.296628Z" }, "user_id": "count_dooku" @@ -137,22 +144,25 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Corellia", + "blocked_user_ids": [], "created_at": "2024-04-04T09:18:11.060737Z", "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", - "last_active": "2024-07-31T22:17:14.021725Z", + "language": "", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, "role": "user", + "teams": [], "updated_at": "2024-06-25T14:17:08.295189Z" }, "user_id": "han_solo" @@ -160,24 +170,28 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" } @@ -185,30 +199,34 @@ "membership": { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } }, "messages": [], "pinned_messages": [], "read": [ { - "last_read": "2024-08-02T10:15:59.849616704Z", + "last_read": "2024-09-01T00:16:07.916440838Z", "unread_messages": 0, "user": { "banned": false, @@ -218,7 +236,7 @@ "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", "language": "", - "last_active": "2024-07-18T22:22:54.993937Z", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", @@ -227,7 +245,7 @@ } }, { - "last_read": "2024-08-02T10:15:59.849616704Z", + "last_read": "2024-09-01T00:16:07.916440838Z", "unread_messages": 0, "user": { "banned": false, @@ -237,24 +255,16 @@ "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", "language": "", - "last_active": "2024-07-31T22:17:14.021725Z", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "user", "teams": [], "updated_at": "2024-06-25T14:17:08.295189Z" } }, { - "last_read": "2024-08-02T10:15:59.849616704Z", + "last_read": "2024-09-01T00:16:07.916440838Z", "unread_messages": 0, "user": { "banned": false, @@ -264,20 +274,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } ], @@ -285,5 +289,5 @@ "watcher_count": 1 } ], - "duration": "261.11ms" + "duration": "166.89ms" } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_events.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_events.json index 19486a81045..eed2437f6c4 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_events.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_events.json @@ -1,10 +1,10 @@ { - "duration": "4.08ms", + "duration": "5.53ms", "event": { - "channel_id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "channel_id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "channel_type": "messaging", - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:00.431491005Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:09.559637224Z", "type": "typing.start", "user": { "banned": false, @@ -13,11 +13,13 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json index 63fb2b5480b..a86c2c36efd 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json @@ -1,12 +1,12 @@ { - "duration": "862.00ms", + "duration": "1578.86ms", "message": { "attachments": [], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.297535Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.276243Z", "deleted_reply_count": 0, "html": "

Test

\n", - "id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [], "mentioned_users": [], "own_reactions": [], @@ -21,7 +21,7 @@ "silent": false, "text": "Test", "type": "regular", - "updated_at": "2024-08-02T10:16:01.297535Z", + "updated_at": "2024-09-01T00:16:11.276243Z", "user": { "banned": false, "birthland": "Tatooine", @@ -30,20 +30,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message_ephemeral.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message_ephemeral.json index e9cb7edee0e..9ba99267231 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message_ephemeral.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message_ephemeral.json @@ -1,5 +1,5 @@ { - "duration": "96.30ms", + "duration": "289.97ms", "message": { "args": "Test", "attachments": [ @@ -31,73 +31,73 @@ "fixed_height": { "frames": "", "height": "200", - "size": "225374", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/200.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=200.gif&ct=g", - "width": "235" + "size": "1876174", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/200.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=200.gif&ct=g", + "width": "356" }, "fixed_height_downsampled": { "frames": "", "height": "200", - "size": "56022", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/200_d.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=200_d.gif&ct=g", - "width": "235" + "size": "93981", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/200_d.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=200_d.gif&ct=g", + "width": "356" }, "fixed_height_still": { "frames": "", "height": "200", - "size": "9474", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/200_s.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=200_s.gif&ct=g", - "width": "235" + "size": "14511", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/200_s.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=200_s.gif&ct=g", + "width": "356" }, "fixed_width": { "frames": "", - "height": "170", - "size": "168665", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/200w.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=200w.gif&ct=g", + "height": "112", + "size": "737477", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/200w.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=200w.gif&ct=g", "width": "200" }, "fixed_width_downsampled": { "frames": "", - "height": "170", - "size": "43314", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/200w_d.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=200w_d.gif&ct=g", + "height": "112", + "size": "40221", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/200w_d.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=200w_d.gif&ct=g", "width": "200" }, "fixed_width_still": { "frames": "", - "height": "170", - "size": "7388", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/200w_s.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=200w_s.gif&ct=g", + "height": "112", + "size": "6180", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/200w_s.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=200w_s.gif&ct=g", "width": "200" }, "original": { - "frames": "25", - "height": "310", - "size": "491084", - "url": "https://media2.giphy.com/media/c5RxnGpPUav60/giphy.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=giphy.gif&ct=g", - "width": "364" + "frames": "117", + "height": "270", + "size": "3688892", + "url": "https://media0.giphy.com/media/TUwmmQHmofOda/giphy.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=giphy.gif&ct=g", + "width": "480" } }, - "thumb_url": "https://media2.giphy.com/media/c5RxnGpPUav60/giphy.gif?cid=c4b03675pk5gbhjyh729dwp8w8u7wzmzhjlpdoo8bqnp24rr&ep=v1_gifs_search&rid=giphy.gif&ct=g", + "thumb_url": "https://media0.giphy.com/media/TUwmmQHmofOda/giphy.gif?cid=c4b03675i8h3ibpabdi6sehggkmhp61agf9mm7poih9la21d&ep=v1_gifs_search&rid=giphy.gif&ct=g", "title": "Test", - "title_link": "https://giphy.com/gifs/test-dupa-c5RxnGpPUav60", + "title_link": "https://giphy.com/gifs/toilet-carrots-TUwmmQHmofOda", "type": "giphy" } ], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "command": "giphy", "command_info": { "name": "Giphy" }, - "created_at": "2024-08-02T10:16:02.05817Z", + "created_at": "2024-09-01T00:16:12.725395Z", "deleted_reply_count": 0, "html": "

/giphy Test

\n", "i18n": { - "de_text": "/Giphy-Test", + "en_text": "/Giphy Test", "fr_text": "/giphy Test", "language": "fr" }, - "id": "382f18ea-50b8-11ef-a447-1eebe6661088", + "id": "6528d524-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [], "mentioned_users": [], "own_reactions": [], @@ -112,7 +112,7 @@ "silent": false, "text": "/giphy Test", "type": "ephemeral", - "updated_at": "2024-08-02T10:16:02.05817Z", + "updated_at": "2024-09-01T00:16:12.725395Z", "user": { "banned": false, "birthland": "Tatooine", @@ -121,20 +121,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json index 054f83e624b..41a628245a6 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json @@ -1,19 +1,19 @@ { - "duration": "28.78ms", + "duration": "27.75ms", "message": { "attachments": [], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.297535Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.276243Z", "deleted_reply_count": 0, "html": "

Test

\n", - "id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [ { - "created_at": "2024-08-02T10:16:01.481963Z", - "message_id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "created_at": "2024-09-01T00:16:11.531837Z", + "message_id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "score": 1, "type": "like", - "updated_at": "2024-08-02T10:16:01.481963Z", + "updated_at": "2024-09-01T00:16:11.531837Z", "user": { "banned": false, "birthland": "Tatooine", @@ -22,20 +22,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" } @@ -43,11 +37,11 @@ "mentioned_users": [], "own_reactions": [ { - "created_at": "2024-08-02T10:16:01.481963Z", - "message_id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "created_at": "2024-09-01T00:16:11.531837Z", + "message_id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "score": 1, "type": "like", - "updated_at": "2024-08-02T10:16:01.481963Z", + "updated_at": "2024-09-01T00:16:11.531837Z", "user": { "banned": false, "birthland": "Tatooine", @@ -56,20 +50,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" } @@ -84,8 +72,8 @@ "reaction_groups": { "like": { "count": 1, - "first_reaction_at": "2024-08-02T10:16:01.481963Z", - "last_reaction_at": "2024-08-02T10:16:01.481963Z", + "first_reaction_at": "2024-09-01T00:16:11.531837Z", + "last_reaction_at": "2024-09-01T00:16:11.531837Z", "sum_scores": 1 } }, @@ -97,7 +85,7 @@ "silent": false, "text": "Test", "type": "regular", - "updated_at": "2024-08-02T10:16:01.488725Z", + "updated_at": "2024-09-01T00:16:11.543157Z", "user": { "banned": false, "birthland": "Tatooine", @@ -106,28 +94,22 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } }, "reaction": { - "created_at": "2024-08-02T10:16:01.481963Z", - "message_id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "created_at": "2024-09-01T00:16:11.531837Z", + "message_id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "score": 1, "type": "like", - "updated_at": "2024-08-02T10:16:01.481963Z", + "updated_at": "2024-09-01T00:16:11.531837Z", "user": { "banned": false, "birthland": "Tatooine", @@ -136,20 +118,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:08.037217752Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" } diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json index b9fc2abed48..7e2dc2a7079 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json @@ -1,6 +1,6 @@ { "channel": { - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "config": { "automod": "AI", "automod_behavior": "block", @@ -49,45 +49,52 @@ "uploads": true, "url_enrichment": true }, - "created_at": "2024-08-02T10:15:59.830311Z", + "created_at": "2024-09-01T00:16:07.852577Z", "created_by": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:12.728250396Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "disabled": false, "frozen": false, - "id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "last_message_at": "0001-01-01T00:00:00Z", "member_count": 4, "members": [ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Serenno", + "blocked_user_ids": [], "created_at": "2024-04-22T06:42:08.562992Z", "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", - "last_active": "2024-07-18T22:22:54.993937Z", + "language": "", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", + "teams": [], "updated_at": "2024-07-11T05:45:57.296628Z" }, "user_id": "count_dooku" @@ -95,22 +102,25 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Corellia", + "blocked_user_ids": [], "created_at": "2024-04-04T09:18:11.060737Z", "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", - "last_active": "2024-07-31T22:17:14.021725Z", + "language": "", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, "role": "user", + "teams": [], "updated_at": "2024-06-25T14:17:08.295189Z" }, "user_id": "han_solo" @@ -118,44 +128,49 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:12.728250396Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" }, { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:16:01.569225Z", + "created_at": "2024-09-01T00:16:11.760869Z", "notifications_muted": false, "role": "admin", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:16:01.569225Z", + "updated_at": "2024-09-01T00:16:11.760869Z", "user": { "banned": false, "birthland": "Polis Massa", + "blocked_user_ids": [], "created_at": "2024-04-04T09:42:00.68335Z", "id": "leia_organa", "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png", - "language": "de", - "last_active": "2024-08-02T06:35:06.594322Z", + "language": "en", + "last_active": "2024-08-30T14:16:49.393869Z", "name": "Leia Organa", "online": false, "private_settings": { @@ -167,37 +182,42 @@ } }, "role": "admin", - "updated_at": "2024-07-15T12:34:51.215666Z" + "teams": [], + "updated_at": "2024-08-29T18:09:28.42841Z" }, "user_id": "leia_organa" } ], "name": "Sync Mock Server", - "truncated_at": "2024-08-02T10:16:02.78193Z", + "truncated_at": "2024-09-01T00:16:14.534309Z", "truncated_by": { "banned": false, "birthland": "Tatooine", + "blocked_user_ids": [], "created_at": "2024-04-04T09:26:11.805899Z", "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:12.728250396Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "teams": [], + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "type": "messaging", - "updated_at": "2024-08-02T10:16:02.799276Z" + "updated_at": "2024-09-01T00:16:14.540443Z" }, - "duration": "80.12ms", + "duration": "78.60ms", "message": { "attachments": [], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:02.781931Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:14.53431Z", "deleted_reply_count": 0, "html": "

Channel truncated

\n", - "id": "38a88b1c-50b8-11ef-a447-1eebe6661088", + "id": "666791f0-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [], "mentioned_users": [], "own_reactions": [], @@ -213,7 +233,7 @@ "silent": false, "text": "Channel truncated", "type": "system", - "updated_at": "2024-08-02T10:16:02.781931Z", + "updated_at": "2024-09-01T00:16:14.53431Z", "user": { "banned": false, "birthland": "Tatooine", @@ -221,11 +241,13 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json index dcb69c2f832..da8519ae776 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json @@ -1,27 +1,27 @@ { - "duration": "231.36ms", + "duration": "899.86ms", "message": { "attachments": [ { - "image_url": "https://images.unsplash.com/photo-1568574728383-06fca083883d?blend=000000&blend-alpha=10&blend-mode=normal&blend-w=1&crop=faces%2Cedges&h=630&mark=https%3A%2F%2Fimages.unsplash.com%2Fopengraph%2Flogo.png&mark-align=top%2Cleft&mark-pad=50&mark-w=64&w=1200&auto=format&fit=crop&q=60&ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzIyNTkyNTQ5fA&ixlib=rb-4.0.3", + "image_url": "https://images.unsplash.com/photo-1568574728383-06fca083883d?mark=https%3A%2F%2Fimages.unsplash.com%2Fopengraph%2Flogo.png&mark-w=64&mark-align=top%2Cleft&mark-pad=50&h=630&w=1200&crop=faces%2Cedges&blend-w=1&blend=000000&blend-mode=normal&blend-alpha=10&auto=format&fit=crop&q=60&ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzI1MTQ5NzczfA&ixlib=rb-4.0.3", "og_scrape_url": "https://unsplash.com/photos/1_2d3MRbI9c", "text": "Download this photo by Joao Branco on Unsplash", - "thumb_url": "https://images.unsplash.com/photo-1568574728383-06fca083883d?blend=000000&blend-alpha=10&blend-mode=normal&blend-w=1&crop=faces%2Cedges&h=630&mark=https%3A%2F%2Fimages.unsplash.com%2Fopengraph%2Flogo.png&mark-align=top%2Cleft&mark-pad=50&mark-w=64&w=1200&auto=format&fit=crop&q=60&ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzIyNTkyNTQ5fA&ixlib=rb-4.0.3", + "thumb_url": "https://images.unsplash.com/photo-1568574728383-06fca083883d?mark=https%3A%2F%2Fimages.unsplash.com%2Fopengraph%2Flogo.png&mark-w=64&mark-align=top%2Cleft&mark-pad=50&h=630&w=1200&crop=faces%2Cedges&blend-w=1&blend=000000&blend-mode=normal&blend-alpha=10&auto=format&fit=crop&q=60&ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzI1MTQ5NzczfA&ixlib=rb-4.0.3", "title": "Photo by Joao Branco on Unsplash", "title_link": "https://unsplash.com/photos/green-pine-tree-mountain-slope-scenery-1_2d3MRbI9c", "type": "image" } ], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:02.706829Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:14.315238Z", "deleted_reply_count": 0, "html": "

https://unsplash.com/photos/1_2d3MRbI9c

\n", "i18n": { - "de_text": "https://unsplash.com/photos/1_2d3MRbI9c", + "en_text": "https://unsplash.com/photos/1_2d3MRbI9c", "fr_text": "https://unsplash.com/photos/1_2d3MRbI9c", "language": "fr" }, - "id": "387dd2a0-50b8-11ef-a447-1eebe6661088", + "id": "65c12cd4-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [], "mentioned_users": [], "own_reactions": [], @@ -36,7 +36,7 @@ "silent": false, "text": "https://unsplash.com/photos/1_2d3MRbI9c", "type": "regular", - "updated_at": "2024-08-02T10:16:02.706829Z", + "updated_at": "2024-09-01T00:16:14.315238Z", "user": { "banned": false, "birthland": "Tatooine", @@ -45,20 +45,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:12.728250396Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_youtube_link.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_youtube_link.json index 8b01563c914..55b63e14011 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_youtube_link.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_youtube_link.json @@ -1,5 +1,5 @@ { - "duration": "317.71ms", + "duration": "319.95ms", "message": { "attachments": [ { @@ -14,16 +14,16 @@ "type": "video" } ], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:02.419412Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:13.222384Z", "deleted_reply_count": 0, "html": "

https://youtube.com/watch?v=xOX7MsrbaPY

\n", "i18n": { - "de_text": "https://youtube.com/watch?v=xOX7MsrbaPY", + "en_text": "https://youtube.com/watch?v=xOX7MsrbaPY", "fr_text": "https://youtube.com/watch?v=xOX7MsrbaPY", "language": "fr" }, - "id": "384584ae-50b8-11ef-a447-1eebe6661088", + "id": "6572eb50-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [], "mentioned_users": [], "own_reactions": [], @@ -38,7 +38,7 @@ "silent": false, "text": "https://youtube.com/watch?v=xOX7MsrbaPY", "type": "regular", - "updated_at": "2024-08-02T10:16:02.419412Z", + "updated_at": "2024-09-01T00:16:13.222384Z", "user": { "banned": false, "birthland": "Tatooine", @@ -47,20 +47,14 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.863243042Z", + "last_active": "2024-09-01T00:16:12.728250396Z", "name": "Luke Skywalker", "online": true, - "privacy_settings": { - "read_receipts": { - "enabled": false - }, - "typing_indicators": { - "enabled": false - } - }, "role": "admin", + "team": "test", "teams": [], - "updated_at": "2024-07-31T12:39:19.150927Z" + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events.json index 1c01f0c1eec..e1cf5830022 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events.json @@ -1,8 +1,8 @@ { - "channel_id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "channel_id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "channel_type": "messaging", - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:00.431491005Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:09.559637224Z", "type": "typing.start", "user": { "banned": false, @@ -11,10 +11,12 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json index 7aa9b5ee639..0fd8f170297 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json @@ -1,6 +1,6 @@ { "channel": { - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", "config": { "automod": "AI", "automod_behavior": "block", @@ -49,7 +49,7 @@ "uploads": true, "url_enrichment": true }, - "created_at": "2024-08-02T10:15:59.830311Z", + "created_at": "2024-09-01T00:16:07.852577Z", "created_by": { "banned": false, "birthland": "Tatooine", @@ -57,34 +57,36 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "disabled": false, "frozen": false, - "id": "36d17a7e-50b8-11ef-a447-1eebe6661088", - "last_message_at": "2024-08-02T10:16:01.297535Z", + "id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "last_message_at": "2024-09-01T00:16:11.276243Z", "member_count": 4, "members": [ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Serenno", "created_at": "2024-04-22T06:42:08.562992Z", "id": "count_dooku", "image": "https://vignette.wikia.nocookie.net/starwars/images/b/b8/Dooku_Headshot.jpg", - "last_active": "2024-07-18T22:22:54.993937Z", + "last_active": "2024-08-08T21:34:35.577641Z", "name": "Count Dooku", "online": false, "role": "user", @@ -95,19 +97,19 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "member", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Corellia", "created_at": "2024-04-04T09:18:11.060737Z", "id": "han_solo", "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png", - "last_active": "2024-07-31T22:17:14.021725Z", + "last_active": "2024-08-31T15:48:09.744514Z", "name": "Han Solo", "online": false, "role": "user", @@ -118,12 +120,12 @@ { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:15:59.836883Z", + "created_at": "2024-09-01T00:16:07.874942Z", "notifications_muted": false, "role": "owner", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:15:59.836883Z", + "updated_at": "2024-09-01T00:16:07.874942Z", "user": { "banned": false, "birthland": "Tatooine", @@ -131,31 +133,33 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" }, { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:16:01.569225Z", + "created_at": "2024-09-01T00:16:11.760869Z", "notifications_muted": false, "role": "admin", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:16:01.569225Z", + "updated_at": "2024-09-01T00:16:11.760869Z", "user": { "banned": false, "birthland": "Polis Massa", "created_at": "2024-04-04T09:42:00.68335Z", "id": "leia_organa", "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png", - "language": "de", - "last_active": "2024-08-02T06:35:06.594322Z", + "language": "en", + "last_active": "2024-08-30T14:16:49.393869Z", "name": "Leia Organa", "online": false, "private_settings": { @@ -167,19 +171,19 @@ } }, "role": "admin", - "updated_at": "2024-07-15T12:34:51.215666Z" + "updated_at": "2024-08-29T18:09:28.42841Z" }, "user_id": "leia_organa" } ], "name": "Sync Mock Server", "type": "messaging", - "updated_at": "2024-08-02T10:15:59.830312Z" + "updated_at": "2024-09-01T00:16:07.852577Z" }, - "channel_id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "channel_id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "channel_type": "messaging", - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.585913498Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.772933273Z", "type": "channel.updated", "user": { "banned": false, @@ -188,10 +192,12 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_member.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_member.json index 832e0e0d26a..414f75abd1f 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_member.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_member.json @@ -1,25 +1,25 @@ { - "channel_id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "channel_id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "channel_type": "messaging", - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.580203708Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.768406088Z", "member": { "banned": false, "channel_role": "channel_member", - "created_at": "2024-08-02T10:16:01.569225Z", + "created_at": "2024-09-01T00:16:11.760869Z", "notifications_muted": false, "role": "admin", "shadow_banned": false, "status": "member", - "updated_at": "2024-08-02T10:16:01.569225Z", + "updated_at": "2024-09-01T00:16:11.760869Z", "user": { "banned": false, "birthland": "Polis Massa", "created_at": "2024-04-04T09:42:00.68335Z", "id": "leia_organa", "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png", - "language": "de", - "last_active": "2024-08-02T06:35:06.594322Z", + "language": "en", + "last_active": "2024-08-30T14:16:49.393869Z", "name": "Leia Organa", "online": false, "private_settings": { @@ -31,7 +31,7 @@ } }, "role": "admin", - "updated_at": "2024-07-15T12:34:51.215666Z" + "updated_at": "2024-08-29T18:09:28.42841Z" }, "user_id": "leia_organa" }, @@ -42,8 +42,8 @@ "created_at": "2024-04-04T09:42:00.68335Z", "id": "leia_organa", "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png", - "language": "de", - "last_active": "2024-08-02T06:35:06.594322Z", + "language": "en", + "last_active": "2024-08-30T14:16:49.393869Z", "name": "Leia Organa", "online": false, "private_settings": { @@ -55,6 +55,6 @@ } }, "role": "admin", - "updated_at": "2024-07-15T12:34:51.215666Z" + "updated_at": "2024-08-29T18:09:28.42841Z" } } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_health_check.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_health_check.json index d4684a966c6..733e2d0eb30 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_health_check.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_health_check.json @@ -1,7 +1,7 @@ { "cid": "*", - "connection_id": "668e88d0-0a15-1e61-0200-000000002e9f", - "created_at": "2024-08-02T10:15:59.598114374Z", + "connection_id": "66cc79d8-0a15-1e61-0200-000000000778", + "created_at": "2024-09-01T00:16:07.067591173Z", "me": { "banned": false, "birthland": "Tatooine", @@ -12,7 +12,7 @@ "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "invisible": false, "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "mutes": [], "name": "Luke Skywalker", "online": true, @@ -25,11 +25,13 @@ } }, "role": "admin", + "team": "test", "total_unread_count": 0, + "type": "team", "unread_channels": 0, "unread_count": 0, - "unread_threads": 3, - "updated_at": "2024-07-31T12:39:19.150927Z" + "unread_threads": 1, + "updated_at": "2024-08-29T10:37:09.068681Z" }, "type": "health.check" } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json index f4b54a14bf6..9b54479d4d4 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json @@ -1,16 +1,16 @@ { - "channel_id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "channel_id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "channel_type": "messaging", - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.321958185Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.321890059Z", "message": { "attachments": [], "before_message_send_failed": true, - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.297535Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.276243Z", "deleted_reply_count": 0, "html": "

Test

\n", - "id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [], "mentioned_users": [], "own_reactions": [], @@ -26,7 +26,7 @@ "silent": false, "text": "Test", "type": "regular", - "updated_at": "2024-08-02T10:16:01.297535Z", + "updated_at": "2024-09-01T00:16:11.276243Z", "user": { "banned": false, "birthland": "Tatooine", @@ -34,11 +34,13 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } }, "total_unread_count": 0, @@ -52,11 +54,13 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "watcher_count": 1 } \ No newline at end of file diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json index 819c1c92831..f0242c2111f 100644 --- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json +++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json @@ -1,22 +1,22 @@ { - "channel_id": "36d17a7e-50b8-11ef-a447-1eebe6661088", + "channel_id": "62291f0a-67f7-11ef-bc15-c6eef6ed0640", "channel_type": "messaging", - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.502485421Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.551696085Z", "message": { "attachments": [], - "cid": "messaging:36d17a7e-50b8-11ef-a447-1eebe6661088", - "created_at": "2024-08-02T10:16:01.297535Z", + "cid": "messaging:62291f0a-67f7-11ef-bc15-c6eef6ed0640", + "created_at": "2024-09-01T00:16:11.276243Z", "deleted_reply_count": 0, "html": "

Test

\n", - "id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "latest_reactions": [ { - "created_at": "2024-08-02T10:16:01.481963Z", - "message_id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "created_at": "2024-09-01T00:16:11.531837Z", + "message_id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "score": 1, "type": "like", - "updated_at": "2024-08-02T10:16:01.481963Z", + "updated_at": "2024-09-01T00:16:11.531837Z", "user": { "banned": false, "birthland": "Tatooine", @@ -24,11 +24,13 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" } @@ -45,8 +47,8 @@ "reaction_groups": { "like": { "count": 1, - "first_reaction_at": "2024-08-02T10:16:01.481963Z", - "last_reaction_at": "2024-08-02T10:16:01.481963Z", + "first_reaction_at": "2024-09-01T00:16:11.531837Z", + "last_reaction_at": "2024-09-01T00:16:11.531837Z", "sum_scores": 1 } }, @@ -58,7 +60,7 @@ "silent": false, "text": "Test", "type": "regular", - "updated_at": "2024-08-02T10:16:01.488725Z", + "updated_at": "2024-09-01T00:16:11.543157Z", "user": { "banned": false, "birthland": "Tatooine", @@ -66,19 +68,21 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } }, "reaction": { - "created_at": "2024-08-02T10:16:01.481963Z", - "message_id": "374a68d0-50b8-11ef-a447-1eebe6661088", + "created_at": "2024-09-01T00:16:11.531837Z", + "message_id": "638e6710-67f7-11ef-bc15-c6eef6ed0640", "score": 1, "type": "like", - "updated_at": "2024-08-02T10:16:01.481963Z", + "updated_at": "2024-09-01T00:16:11.531837Z", "user": { "banned": false, "birthland": "Tatooine", @@ -86,11 +90,13 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" }, "user_id": "luke_skywalker" }, @@ -102,10 +108,12 @@ "id": "luke_skywalker", "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg", "language": "fr", - "last_active": "2024-08-02T10:15:59.583702446Z", + "last_active": "2024-09-01T00:16:06.991601863Z", "name": "Luke Skywalker", "online": true, "role": "admin", - "updated_at": "2024-07-31T12:39:19.150927Z" + "team": "test", + "type": "team", + "updated_at": "2024-08-29T10:37:09.068681Z" } } \ No newline at end of file diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/AnyAttachmentPayload_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/AnyAttachmentPayload_Mock.swift index 9effc27611a..41c94c1f816 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/AnyAttachmentPayload_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/AnyAttachmentPayload_Mock.swift @@ -26,6 +26,7 @@ public extension AnyAttachmentPayload { id: id, type: type, payload: payload, + downloadingState: nil, uploadingState: localFileURL.map { .init( localFileURL: $0, diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageAudioAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageAudioAttachment_Mock.swift new file mode 100644 index 00000000000..818101fdb07 --- /dev/null +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageAudioAttachment_Mock.swift @@ -0,0 +1,43 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamChat +import Foundation + +public extension ChatMessageAudioAttachment { + static func mock( + id: AttachmentId, + title: String = "Sample.wav", + audioRemoteURL: URL = URL(string: "http://asset.url/file.wav")!, + file: AttachmentFile = AttachmentFile(type: .wav, size: 120, mimeType: "audio/wav"), + localState: LocalAttachmentState? = nil, + localDownloadState: LocalAttachmentDownloadState? = nil, + extraData: [String: RawJSON]? = nil + ) -> Self { + ChatMessageAudioAttachment( + id: id, + type: .audio, + payload: .init( + title: title, + audioRemoteURL: audioRemoteURL, + file: file, + extraData: extraData + ), + downloadingState: localDownloadState.map { + .init( + localFileURL: $0 == .downloaded ? .newTemporaryFileURL() : nil, + state: $0, + file: file + ) + }, + uploadingState: localState.map { + .init( + localFileURL: .newTemporaryFileURL(), + state: $0, + file: file + ) + } + ) + } +} diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageFileAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageFileAttachment_Mock.swift index 9e656d77d1a..a29a5f5da06 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageFileAttachment_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageFileAttachment_Mock.swift @@ -13,6 +13,7 @@ public extension ChatMessageFileAttachment { assetURL: URL = URL(string: "http://asset.url")!, file: AttachmentFile = AttachmentFile(type: .pdf, size: 120, mimeType: "application/pdf"), localState: LocalAttachmentState? = .uploaded, + localDownloadState: LocalAttachmentDownloadState? = nil, extraData: [String: RawJSON]? = nil ) -> Self { .init( @@ -24,6 +25,13 @@ public extension ChatMessageFileAttachment { file: file, extraData: extraData ), + downloadingState: localDownloadState.map { + .init( + localFileURL: $0 == .downloaded ? .newTemporaryFileURL() : nil, + state: $0, + file: file + ) + }, uploadingState: localState.map { .init( localFileURL: assetURL, diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift index 4772c2cf690..3c57c457f53 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageImageAttachment_Mock.swift @@ -12,6 +12,7 @@ extension ChatMessageImageAttachment { imageURL: URL = .localYodaImage, title: String = URL.localYodaImage.lastPathComponent, localState: LocalAttachmentState? = nil, + localDownloadState: LocalAttachmentDownloadState? = nil, extraData: [String: RawJSON]? = nil ) -> Self { .init( @@ -22,6 +23,13 @@ extension ChatMessageImageAttachment { imageRemoteURL: imageURL, extraData: extraData ), + downloadingState: localDownloadState.map { + .init( + localFileURL: $0 == .downloaded ? .newTemporaryFileURL() : nil, + state: $0, + file: try! AttachmentFile(url: imageURL) + ) + }, uploadingState: localState.map { .init( localFileURL: imageURL, diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageLinkAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageLinkAttachment_Mock.swift index 7178d116c1b..2fe095ed038 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageLinkAttachment_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageLinkAttachment_Mock.swift @@ -29,6 +29,7 @@ extension ChatMessageLinkAttachment { assetURL: assetURL, previewURL: previewURL ), + downloadingState: nil, uploadingState: nil ) } diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVideoAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVideoAttachment_Mock.swift new file mode 100644 index 00000000000..f3520480916 --- /dev/null +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVideoAttachment_Mock.swift @@ -0,0 +1,45 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamChat +import Foundation + +public extension ChatMessageVideoAttachment { + static func mock( + id: AttachmentId, + title: String = "Sample.mp4", + thumbnailURL: URL? = nil, + videoRemoteURL: URL = URL(string: "http://asset.url/video.mp4")!, + file: AttachmentFile = AttachmentFile(type: .mp4, size: 1200, mimeType: "video/mp4"), + localState: LocalAttachmentState? = .uploaded, + localDownloadState: LocalAttachmentDownloadState? = nil, + extraData: [String: RawJSON]? = nil + ) -> Self { + .init( + id: id, + type: .video, + payload: VideoAttachmentPayload( + title: title, + videoRemoteURL: videoRemoteURL, + thumbnailURL: thumbnailURL, + file: file, + extraData: extraData + ), + downloadingState: localDownloadState.map { + .init( + localFileURL: $0 == .downloaded ? .newTemporaryFileURL() : nil, + state: $0, + file: file + ) + }, + uploadingState: localState.map { + .init( + localFileURL: .newTemporaryFileURL(), + state: $0, + file: file + ) + } + ) + } +} diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVoiceRecordingAttachment_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVoiceRecordingAttachment_Mock.swift index 9e65859e62b..d46ca5318d7 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVoiceRecordingAttachment_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVoiceRecordingAttachment_Mock.swift @@ -13,6 +13,7 @@ public extension ChatMessageVoiceRecordingAttachment { assetURL: URL = URL(string: "http://asset.url")!, file: AttachmentFile = AttachmentFile(type: .aac, size: 120, mimeType: "audio/aac"), localState: LocalAttachmentState? = .uploaded, + localDownloadState: LocalAttachmentDownloadState? = nil, duration: TimeInterval? = nil, waveformData: [Float] = [], extraData: [String: RawJSON]? = nil @@ -28,6 +29,13 @@ public extension ChatMessageVoiceRecordingAttachment { waveformData: waveformData, extraData: extraData ), + downloadingState: localDownloadState.map { + .init( + localFileURL: $0 == .downloaded ? .newTemporaryFileURL() : nil, + state: $0, + file: file + ) + }, uploadingState: localState.map { .init( localFileURL: assetURL, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift index a32ac46c0d3..e2909c43e4a 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift @@ -344,6 +344,10 @@ class DatabaseSession_Mock: DatabaseSession { func delete(attachment: AttachmentDTO) { underlyingSession.delete(attachment: attachment) } + + func allLocallyDownloadedAttachments() -> [StreamChat.AttachmentDTO] { + underlyingSession.allLocallyDownloadedAttachments() + } func saveChannelMute( payload: MutedChannelPayload diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift index cc7cbd5c297..c250fb5b77c 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift @@ -5,8 +5,9 @@ import Foundation @testable import StreamChat -public class Chat_Mock: Chat { - +public class Chat_Mock: Chat, Spy { + public let spyState = SpyState() + static let cid = try! ChannelId(cid: "mock:channel") init( @@ -68,6 +69,10 @@ public class Chat_Mock: Chat { public override func loadMessages(around messageId: MessageId, limit: Int? = nil) async throws { loadPageAroundMessageIdCallCount += 1 } + + public override func watch() async throws { + record() + } } public extension Chat_Mock { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift index 6410ba82bb0..53b271d5582 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift @@ -12,6 +12,7 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { @Atomic var updateUserData_imageURL: URL? @Atomic var updateUserData_userExtraData: [String: RawJSON]? @Atomic var updateUserData_privacySettings: UserPrivacySettings? + @Atomic var updateUserData_unset: Set? @Atomic var updateUserData_completion: ((Error?) -> Void)? @Atomic var addDevice_id: DeviceId? @@ -29,6 +30,9 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { @Atomic var markAllRead_completion: ((Error?) -> Void)? @Atomic var markAllRead_completion_result: Result? + + @Atomic var deleteAllLocalAttachmentDownloads_completion: ((Error?) -> Void)? + @Atomic var deleteAllLocalAttachmentDownloads_completion_result: Result? override func updateUserData( currentUserId: UserId, @@ -37,6 +41,7 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { privacySettings: UserPrivacySettings?, role: UserRole?, userExtraData: [String: RawJSON]?, + unset: Set, completion: ((Error?) -> Void)? = nil ) { updateUserData_currentUserId = currentUserId @@ -44,6 +49,7 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { updateUserData_imageURL = imageURL updateUserData_userExtraData = userExtraData updateUserData_privacySettings = privacySettings + updateUserData_unset = unset updateUserData_completion = completion } @@ -75,13 +81,20 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { fetchDevices_currentUserId = currentUserId fetchDevices_completion = completion } + + override func deleteAllLocalAttachmentDownloads(completion: @escaping ((any Error)?) -> Void) { + deleteAllLocalAttachmentDownloads_completion = completion + deleteAllLocalAttachmentDownloads_completion_result?.invoke(with: completion) + } // Cleans up all recorded values func cleanUp() { updateUserData_currentUserId = nil updateUserData_name = nil updateUserData_imageURL = nil + updateUserData_privacySettings = nil updateUserData_userExtraData = nil + updateUserData_unset = nil updateUserData_completion = nil addDevice_id = nil @@ -97,6 +110,9 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { markAllRead_completion = nil markAllRead_completion_result = nil + + deleteAllLocalAttachmentDownloads_completion = nil + deleteAllLocalAttachmentDownloads_completion_result = nil } override func markAllRead(completion: ((Error?) -> Void)? = nil) { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift index b422d1a7069..115daecfafb 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift @@ -16,6 +16,12 @@ final class MessageUpdater_Mock: MessageUpdater { @Atomic var deleteMessage_completion_result: Result? @Atomic var deleteMessage_hard: Bool? + @Atomic var downloadAttachment_attachmentId: AttachmentId? + @Atomic var downloadAttachment_completion_result: Result? + + @Atomic var deleteLocalAttachmentDownload_attachmentId: AttachmentId? + @Atomic var deleteLocalAttachmentDownload_completion_result: Result? + @Atomic var editMessage_messageId: MessageId? @Atomic var editMessage_text: String? @Atomic var editMessage_skipEnrichUrl: Bool? @@ -136,6 +142,12 @@ final class MessageUpdater_Mock: MessageUpdater { deleteMessage_completion = nil deleteMessage_completion_result = nil + deleteLocalAttachmentDownload_attachmentId = nil + deleteLocalAttachmentDownload_completion_result = nil + + downloadAttachment_attachmentId = nil + downloadAttachment_completion_result = nil + editMessage_messageId = nil editMessage_text = nil editMessage_completion = nil @@ -248,6 +260,32 @@ final class MessageUpdater_Mock: MessageUpdater { deleteMessage_completion_result?.invoke(with: completion) } + override func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: @escaping ((any Error)?) -> Void) { + deleteLocalAttachmentDownload_attachmentId = attachmentId + deleteLocalAttachmentDownload_completion_result?.invoke(with: completion) + } + + override func downloadAttachment( + _ attachment: ChatMessageAttachment, + completion: @escaping (Result, any Error>) -> Void + ) where Payload : DownloadableAttachmentPayload { + downloadAttachment_attachmentId = attachment.id + switch downloadAttachment_completion_result { + case .success(let anyAttachment): + if let result = anyAttachment.attachment(payloadType: Payload.self) { + completion(.success(result)) + } else { + completion(.failure(TestError())) + } + case .failure(let error): + completion(.failure(error)) + case nil: + break + } + + //downloadAttachment_completion_result? .invoke(with: completion) + } + override func editMessage( messageId: MessageId, text: String, diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift index 96ce09bf784..b2677886012 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift @@ -32,6 +32,11 @@ final class APIClient_Spy: APIClient, Spy { @Atomic var unmanagedRequest_completion: Any? @Atomic var unmanagedRequest_allRecordedCalls: [(endpoint: AnyEndpoint, completion: Any?)] = [] + @Atomic var downloadFile_remoteURL: URL? + @Atomic var downloadFile_localURL: URL? + @Atomic var downloadFile_completion_result: Result? + @Atomic var downloadFile_expectation: XCTestExpectation + /// The last endpoint `uploadFile` function was called with. @Atomic var uploadFile_attachment: AnyChatMessageAttachment? @Atomic var uploadFile_progress: ((Double) -> Void)? @@ -62,6 +67,10 @@ final class APIClient_Spy: APIClient, Spy { recoveryRequest_allRecordedCalls = [] recoveryRequest_completion = nil + downloadFile_remoteURL = nil + downloadFile_localURL = nil + downloadFile_completion_result = nil + uploadFile_attachment = nil uploadFile_progress = nil uploadFile_completion = nil @@ -74,12 +83,14 @@ final class APIClient_Spy: APIClient, Spy { sessionConfiguration: URLSessionConfiguration, requestEncoder: RequestEncoder, requestDecoder: RequestDecoder, + attachmentDownloader: AttachmentDownloader, attachmentUploader: AttachmentUploader ) { init_sessionConfiguration = sessionConfiguration init_requestEncoder = requestEncoder init_requestDecoder = requestDecoder init_attachmentUploader = attachmentUploader + downloadFile_expectation = .init() request_expectation = .init() recoveryRequest_expectation = .init() uploadRequest_expectation = .init() @@ -88,6 +99,7 @@ final class APIClient_Spy: APIClient, Spy { sessionConfiguration: sessionConfiguration, requestEncoder: requestEncoder, requestDecoder: requestDecoder, + attachmentDownloader: attachmentDownloader, attachmentUploader: attachmentUploader ) } @@ -155,6 +167,18 @@ final class APIClient_Spy: APIClient, Spy { } } + override func downloadFile( + from remoteURL: URL, + to localURL: URL, + progress: ((Double) -> Void)?, + completion: @escaping ((any Error)?) -> Void + ) { + downloadFile_remoteURL = remoteURL + downloadFile_localURL = localURL + downloadFile_completion_result?.invoke(with: completion) + downloadFile_expectation.fulfill() + } + override func uploadAttachment( _ attachment: AnyChatMessageAttachment, progress: ((Double) -> Void)?, @@ -204,6 +228,7 @@ extension APIClient_Spy { sessionConfiguration: .ephemeral, requestEncoder: DefaultRequestEncoder(baseURL: .unique(), apiKey: .init(.unique)), requestDecoder: DefaultRequestDecoder(), + attachmentDownloader: AttachmentDownloader_Spy(), attachmentUploader: AttachmentUploader_Spy() ) } diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift new file mode 100644 index 00000000000..b8be0295072 --- /dev/null +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift @@ -0,0 +1,25 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation +@testable import StreamChat + +final class AttachmentDownloader_Spy: AttachmentDownloader, Spy { + let spyState = SpyState() + @Atomic var downloadAttachmentProgress: Double? + @Atomic var downloadAttachmentResult: Error? + + func download(from remoteURL: URL, to localURL: URL, progress: ((Double) -> Void)?, completion: @escaping ((any Error)?) -> Void) { + record() + if let downloadAttachmentProgress { + progress?(downloadAttachmentProgress) + } + + if let downloadAttachmentResult { + DispatchQueue.main.async { + completion(downloadAttachmentResult) + } + } + } +} diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/ChatMessageAttachment.swift b/TestTools/StreamChatTestTools/TestData/DummyData/ChatMessageAttachment.swift index c9861e562fe..f31f359af2f 100644 --- a/TestTools/StreamChatTestTools/TestData/DummyData/ChatMessageAttachment.swift +++ b/TestTools/StreamChatTestTools/TestData/DummyData/ChatMessageAttachment.swift @@ -10,12 +10,14 @@ extension AnyChatMessageAttachment { id: AttachmentId = .unique, type: AttachmentType = .image, payload: Data = "payload".data(using: .utf8)!, + downloadingState: AttachmentDownloadingState? = nil, uploadingState: AttachmentUploadingState? = nil ) -> AnyChatMessageAttachment { AnyChatMessageAttachment( id: id, type: type, payload: payload, + downloadingState: downloadingState, uploadingState: uploadingState ) } diff --git a/Tests/StreamChatTests/APIClient/APIClient_Tests.swift b/Tests/StreamChatTests/APIClient/APIClient_Tests.swift index 08bb42f50c1..b739bc1dad5 100644 --- a/Tests/StreamChatTests/APIClient/APIClient_Tests.swift +++ b/Tests/StreamChatTests/APIClient/APIClient_Tests.swift @@ -17,6 +17,7 @@ final class APIClient_Tests: XCTestCase { var encoder: RequestEncoder_Spy! var decoder: RequestDecoder_Spy! + var attachmentDownloader: AttachmentDownloader_Spy! var attachmentUploader: AttachmentUploader_Spy! var tokenRefresher: ((@escaping () -> Void) -> Void)! var queueOfflineRequest: QueueOfflineRequestBlock! @@ -39,6 +40,7 @@ final class APIClient_Tests: XCTestCase { encoder = RequestEncoder_Spy(baseURL: baseURL, apiKey: apiKey) decoder = RequestDecoder_Spy() + attachmentDownloader = AttachmentDownloader_Spy() attachmentUploader = AttachmentUploader_Spy() tokenRefresher = { _ in } queueOfflineRequest = { _ in } @@ -47,6 +49,7 @@ final class APIClient_Tests: XCTestCase { sessionConfiguration: sessionConfiguration, requestEncoder: encoder, requestDecoder: decoder, + attachmentDownloader: attachmentDownloader, attachmentUploader: attachmentUploader ) apiClient.tokenRefresher = tokenRefresher @@ -64,6 +67,7 @@ final class APIClient_Tests: XCTestCase { uniqueHeaderValue = nil encoder = nil decoder = nil + attachmentDownloader = nil attachmentUploader = nil tokenRefresher = nil queueOfflineRequest = nil @@ -709,6 +713,7 @@ extension APIClient_Tests { sessionConfiguration: sessionConfiguration, requestEncoder: encoder, requestDecoder: decoder, + attachmentDownloader: attachmentDownloader, attachmentUploader: attachmentUploader ) apiClient.tokenRefresher = self.tokenRefresher diff --git a/Tests/StreamChatTests/APIClient/ChatPushNotificationContent_Tests.swift b/Tests/StreamChatTests/APIClient/ChatPushNotificationContent_Tests.swift index 71388804fb0..63536bce5a7 100644 --- a/Tests/StreamChatTests/APIClient/ChatPushNotificationContent_Tests.swift +++ b/Tests/StreamChatTests/APIClient/ChatPushNotificationContent_Tests.swift @@ -36,7 +36,7 @@ final class ChatPushNotificationContent_Tests: XCTestCase { var env = ChatClient.Environment() env.databaseContainerBuilder = { _, _, _, _, _, _ in self.database } - env.apiClientBuilder = { _, _, _, _ in self.apiClient } + env.apiClientBuilder = { _, _, _, _, _ in self.apiClient } env.extensionLifecycleBuilder = { _ in self.extensionLifecycle } env.messageRepositoryBuilder = { _, _ in self.messageRepository } diff --git a/Tests/StreamChatTests/APIClient/Endpoints/UserEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/UserEndpoints_Tests.swift index 60b25531da2..f970bdc68e2 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/UserEndpoints_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/UserEndpoints_Tests.swift @@ -41,10 +41,12 @@ final class UserEndpoints_Tests: XCTestCase { role: .anonymous, extraData: ["company": .string(.unique)] ) + let unset = ["image", "name"] let users: [String: AnyEncodable] = [ "id": AnyEncodable(userId), - "set": AnyEncodable(payload) + "set": AnyEncodable(payload), + "unset": AnyEncodable(unset) ] let body: [String: AnyEncodable] = [ "users": AnyEncodable([users]) @@ -58,7 +60,7 @@ final class UserEndpoints_Tests: XCTestCase { body: body ) - let endpoint: Endpoint = .updateUser(id: userId, payload: payload) + let endpoint: Endpoint = .updateUser(id: userId, payload: payload, unset: unset) XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint)) XCTAssertEqual("users", endpoint.path.value) diff --git a/Tests/StreamChatTests/ChatClient_Tests.swift b/Tests/StreamChatTests/ChatClient_Tests.swift index c18336df07b..f050c16e26a 100644 --- a/Tests/StreamChatTests/ChatClient_Tests.swift +++ b/Tests/StreamChatTests/ChatClient_Tests.swift @@ -58,6 +58,17 @@ final class ChatClient_Tests: XCTestCase { // MARK: - Database stack tests + func test_multipleInstance_whenLocalStorageURLIsTheSame() { + let client1 = ChatClient(config: ChatClientConfig(apiKeyString: "123")) + let client2 = ChatClient(config: ChatClientConfig(apiKeyString: "123")) + XCTAssertEqual(1, ChatClient.activeLocalStorageURLs.count) + // We only log an error when misuse happens + XCTAssertEqual( + client1.databaseContainer.persistentStoreDescriptions.compactMap(\.url), + client2.databaseContainer.persistentStoreDescriptions.compactMap(\.url) + ) + } + func test_clientDatabaseStackInitialization_whenLocalStorageEnabled_respectsConfigValues() { // Prepare a config with the local storage let storeFolderURL = URL.newTemporaryDirectoryURL() @@ -905,7 +916,8 @@ private class TestEnvironment { sessionConfiguration: $0, requestEncoder: $1, requestDecoder: $2, - attachmentUploader: $3 + attachmentDownloader: $3, + attachmentUploader: $4 ) return self.apiClient! }, diff --git a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift index 7538c3a09eb..e4695edf596 100644 --- a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController_Tests.swift @@ -752,6 +752,31 @@ final class CurrentUserController_Tests: XCTestCase { wait(for: [exp], timeout: defaultTimeout) } + + // MARK: - Delete All Attachment Downloads + + func test_deleteAllLocalAttachmentDownloads_propagatesErrorFromUpdater() { + let testError = TestError() + let expectation = XCTestExpectation() + controller.deleteAllLocalAttachmentDownloads { [callbackQueueID] error in + AssertTestQueue(withId: callbackQueueID) + XCTAssertEqual(testError, error as? TestError) + expectation.fulfill() + } + env.currentUserUpdater.deleteAllLocalAttachmentDownloads_completion?(testError) + wait(for: [expectation], timeout: defaultTimeout) + } + + func test_deleteAllLocalAttachmentDownloads_success() { + let expectation = XCTestExpectation() + controller.deleteAllLocalAttachmentDownloads { [callbackQueueID] error in + AssertTestQueue(withId: callbackQueueID) + XCTAssertNil(error) + expectation.fulfill() + } + env.currentUserUpdater.deleteAllLocalAttachmentDownloads_completion?(nil) + wait(for: [expectation], timeout: defaultTimeout) + } } private class TestEnvironment { diff --git a/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift b/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift index 3a3b0a52a45..98e89fb9ee8 100644 --- a/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift +++ b/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift @@ -66,7 +66,12 @@ final class DatabaseContainer_Tests: XCTestCase { func test_removingAllData() throws { let container = DatabaseContainer(kind: .inMemory) - // // Create data for all our entities in the DB + // Create dummy local download + let localDownload = URL.streamAttachmentLocalStorageURL(forRelativePath: "mypath") + try FileManager.default.createDirectory(at: localDownload.deletingLastPathComponent(), withIntermediateDirectories: true) + try "1".write(to: localDownload, atomically: false, encoding: .utf8) + + // Create data for all our entities in the DB try writeDataForAllEntities(to: container) // Fetch the data from all out entities @@ -118,6 +123,9 @@ final class DatabaseContainer_Tests: XCTestCase { XCTAssertNil(context.currentUser) } } + + // Assert that local downloads were removed + XCTAssertEqual(false, FileManager.default.fileExists(atPath: URL.streamAttachmentDownloadsDirectory.path)) } func test_removingAllData_whileAnotherWrite() throws { diff --git a/Tests/StreamChatTests/Models/Attachments/AnyAttachmentPayload_Tests.swift b/Tests/StreamChatTests/Models/Attachments/AnyAttachmentPayload_Tests.swift index 61f49d75b3b..eebde5f22ca 100644 --- a/Tests/StreamChatTests/Models/Attachments/AnyAttachmentPayload_Tests.swift +++ b/Tests/StreamChatTests/Models/Attachments/AnyAttachmentPayload_Tests.swift @@ -138,6 +138,7 @@ final class AnyAttachmentPayload_Tests: XCTestCase { id: .unique, type: .image, payload: .init(title: nil, imageRemoteURL: .localYodaImage), + downloadingState: nil, uploadingState: nil ).asAnyAttachment @@ -150,6 +151,7 @@ final class AnyAttachmentPayload_Tests: XCTestCase { id: .unique, type: .image, payload: .init(title: nil, imageRemoteURL: .localYodaImage), + downloadingState: nil, uploadingState: try .mock(localFileURL: .localYodaImage, state: .uploaded) ).asAnyAttachment @@ -162,6 +164,7 @@ final class AnyAttachmentPayload_Tests: XCTestCase { id: .unique, type: .image, payload: .init(title: nil, imageRemoteURL: .localYodaImage), + downloadingState: nil, uploadingState: try .mock(localFileURL: .localYodaImage, state: .uploadingFailed) ).asAnyAttachment diff --git a/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift b/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift index d59bcc5cd17..6397124108d 100644 --- a/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift +++ b/Tests/StreamChatTests/Models/Attachments/AnyAttachmentUpdater_Tests.swift @@ -14,6 +14,7 @@ final class AnyAttachmentUpdater_Tests: XCTestCase { id: .init(cid: .unique, messageId: .unique, index: .unique), type: .image, payload: .init(title: "old", imageRemoteURL: .localYodaImage, extraData: [:]), + downloadingState: nil, uploadingState: nil ).asAnyAttachment diff --git a/Tests/StreamChatTests/Models/Attachments/ChatMessageAttachment_Tests.swift b/Tests/StreamChatTests/Models/Attachments/ChatMessageAttachment_Tests.swift index fe120db2676..9f8a4eb17c5 100644 --- a/Tests/StreamChatTests/Models/Attachments/ChatMessageAttachment_Tests.swift +++ b/Tests/StreamChatTests/Models/Attachments/ChatMessageAttachment_Tests.swift @@ -84,6 +84,7 @@ final class ChatMessageAttachment_Tests: XCTestCase { id: .unique, type: .unknown, payload: fileAttachmentPayload, + downloadingState: nil, uploadingState: nil ) @@ -111,6 +112,7 @@ final class ChatMessageAttachment_Tests: XCTestCase { id: .unique, type: .unknown, payload: try JSONEncoder().encode(joke), + downloadingState: nil, uploadingState: try .mock() ) @@ -119,6 +121,7 @@ final class ChatMessageAttachment_Tests: XCTestCase { id: typeErasedAttachment.id, type: typeErasedAttachment.type, payload: joke, + downloadingState: nil, uploadingState: typeErasedAttachment.uploadingState ) XCTAssertEqual(typeErasedAttachment.attachment(payloadType: Joke.self), jokeAttachment) diff --git a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift index abd202fc466..ea799a6a8ef 100644 --- a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift @@ -205,6 +205,13 @@ class SyncRepository_Tests: XCTestCase { let chatController = ChatChannelController_Spy(client: client) chatController.state = .remoteDataFetched repository.startTrackingChannelController(chatController) + + let chat = Chat_Mock( + chatClient: client, + channelQuery: .init(cid: Chat_Mock.cid), + channelListQuery: nil + ) + repository.startTrackingChat(chat) let eventDate = Date.unique waitForSyncLocalStateRun(requestResult: .success(messageEventPayload(cid: cid, with: [eventDate]))) @@ -214,7 +221,9 @@ class SyncRepository_Tests: XCTestCase { // Write: API Response, lastSyncAt XCTAssertEqual(database.writeSessionCounter, 2) XCTAssertEqual(repository.activeChannelControllers.count, 1) - if !repository.usesV2Sync { + if repository.usesV2Sync { + XCTAssertCall("watch()", on: chat, times: 1) + } else { XCTAssertCall("recoverWatchedChannel(completion:)", on: chatController, times: 1) } XCTAssertEqual(repository.activeChannelListControllers.count, 0) diff --git a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift index c11d6a1e728..2eae2be05a1 100644 --- a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift @@ -381,7 +381,7 @@ final class Chat_Tests: XCTestCase { } } - func test_deleteMessage_whenMessageUpdaterFails_thenDeleteMessageSucceeds() async throws { + func test_deleteMessage_whenMessageUpdaterFails_thenDeleteMessageFails() async throws { for hard in [true, false] { env.messageUpdaterMock.deleteMessage_completion_result = .failure(expectedTestError) let messageId: MessageId = .unique @@ -391,6 +391,45 @@ final class Chat_Tests: XCTestCase { } } + func test_downloadAttachment_whenMessageUpdaterSucceeds_thenSucceess() async throws { + let attachmentId = AttachmentId.unique + let expected = ChatMessageFileAttachment.mock(id: attachmentId) + env.messageUpdaterMock.downloadAttachment_completion_result = .success(expected.asAnyAttachment) + let result = try await chat.downloadAttachment(expected) + XCTAssertEqual(expected, result) + XCTAssertEqual(attachmentId, env.messageUpdaterMock.downloadAttachment_attachmentId) + } + + func test_downloadAttachment_whenMessageUpdaterFails_thenFailure() async throws { + let attachmentId = AttachmentId.unique + let attachment = ChatMessageFileAttachment.mock(id: attachmentId) + let expected = TestError() + env.messageUpdaterMock.downloadAttachment_completion_result = .failure(expected) + await XCTAssertAsyncFailure( + try await chat.downloadAttachment(attachment), + expected + ) + XCTAssertEqual(attachmentId, env.messageUpdaterMock.downloadAttachment_attachmentId) + } + + func test_deleteLocalAttachmentDownload_whenMessageUpdaterSucceeds_thenSucceess() async throws { + let attachmentId = AttachmentId.unique + env.messageUpdaterMock.deleteLocalAttachmentDownload_completion_result = .success(()) + try await chat.deleteLocalAttachmentDownload(for: attachmentId) + XCTAssertEqual(attachmentId, env.messageUpdaterMock.deleteLocalAttachmentDownload_attachmentId) + } + + func test_deleteLocalAttachmentDownload_whenMessageUpdaterFails_thenFailure() async throws { + let attachmentId = AttachmentId.unique + let expected = TestError() + env.messageUpdaterMock.deleteLocalAttachmentDownload_completion_result = .failure(expected) + await XCTAssertAsyncFailure( + try await chat.deleteLocalAttachmentDownload(for: attachmentId), + expected + ) + XCTAssertEqual(attachmentId, env.messageUpdaterMock.deleteLocalAttachmentDownload_attachmentId) + } + func test_resendAttachment_whenAPIRequestSucceeds_thenResendAttachmentSucceeds() async throws { try await setUpChat(usesMockedUpdaters: false) diff --git a/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift b/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift index 1aab8feebed..e4f1c969b06 100644 --- a/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift @@ -158,6 +158,26 @@ final class ConnectedUser_Tests: XCTestCase { XCTAssertEqual(id, env.userUpdaterMock.unblockUser_userId) } + // MARK: - Delete All Attachment Downloads + + func test_deleteAllLocalAttachmentDownloads_propagatesErrorFromUpdater() async throws { + try await setUpConnectedUser(usesMockedUpdaters: true) + + let testError = TestError() + env.currentUserUpdaterMock.deleteAllLocalAttachmentDownloads_completion_result = .failure(testError) + await XCTAssertAsyncFailure( + try await connectedUser.deleteAllLocalAttachmentDownloads(), + testError + ) + } + + func test_deleteAllLocalAttachmentDownloads_success() async throws { + try await setUpConnectedUser(usesMockedUpdaters: true) + + env.currentUserUpdaterMock.deleteAllLocalAttachmentDownloads_completion_result = .success(()) + try await connectedUser.deleteAllLocalAttachmentDownloads() + } + // MARK: - Test Data @MainActor private func setUpConnectedUser(usesMockedUpdaters: Bool, loadState: Bool = true, initialDeviceCount: Int = 0) async throws { diff --git a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift index 183db2e8283..0569637230f 100644 --- a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift @@ -100,7 +100,41 @@ final class CurrentUserUpdater_Tests: XCTestCase { ), role: expectedRole, extraData: [:] - ) + ), + unset: [] + ) + XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(expectedEndpoint)) + } + + func test_updateUser_makesCorrectAPICall_whenOnlyUnsetProperties() throws { + // Simulate user already set + let userPayload: CurrentUserPayload = .dummy(userId: .unique, role: .user) + try database.writeSynchronously { + try $0.saveCurrentUser(payload: userPayload) + } + + currentUserUpdater.updateUserData( + currentUserId: userPayload.id, + name: nil, + imageURL: nil, + privacySettings: nil, + role: nil, + userExtraData: nil, + unset: ["image"], + completion: { _ in } + ) + + // Assert that request is made to the correct endpoint + let expectedEndpoint: Endpoint = .updateUser( + id: userPayload.id, + payload: .init( + name: nil, + imageURL: nil, + privacySettings: nil, + role: nil, + extraData: nil + ), + unset: ["image"] ) XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(expectedEndpoint)) } @@ -643,4 +677,54 @@ final class CurrentUserUpdater_Tests: XCTestCase { // THEN AssertAsync.willBeEqual(completionCalledError as? TestError, error) } + + // MARK: - Delete Local Downloads + + func test_deleteAllLocalAttachmentDownloads_success() throws { + let storedFileCount: () -> Int = { + let paths = try? FileManager.default.subpathsOfDirectory(atPath: URL.streamAttachmentDownloadsDirectory.path) + return paths?.count ?? 0 + } + if FileManager.default.fileExists(atPath: URL.streamAttachmentDownloadsDirectory.path) { + try FileManager.default.removeItem(at: .streamAttachmentDownloadsDirectory) + } + + let attachmentIds = try (0..<5).map { _ in try setUpDownloadedAttachment(with: .mockFile) } + XCTAssertEqual(5, storedFileCount()) + + let error = try waitFor { currentUserUpdater.deleteAllLocalAttachmentDownloads(completion: $0) } + XCTAssertNil(error) + XCTAssertEqual(0, storedFileCount()) + + try database.readSynchronously { session in + for attachmentId in attachmentIds { + guard let dto = session.attachment(id: attachmentId) else { + throw ClientError.AttachmentDoesNotExist(id: attachmentId) + } + XCTAssertEqual(nil, dto.localState) + XCTAssertEqual(nil, dto.localRelativePath) + XCTAssertEqual(nil, dto.localURL) + } + } + } + + // MARK: - + + private func setUpDownloadedAttachment(with payload: AnyAttachmentPayload, messageId: MessageId = .unique, cid: ChannelId = .unique) throws -> AttachmentId { + let attachmentId: AttachmentId = .init(cid: cid, messageId: messageId, index: 0) + try FileManager.default.createDirectory(at: .streamAttachmentDownloadsDirectory, withIntermediateDirectories: true) + try database.createChannel(cid: cid, withMessages: false) + try database.createMessage(id: messageId, cid: cid) + try database.writeSynchronously { session in + let dto = try session.createNewAttachment(attachment: payload, id: attachmentId) + let localRelativePath = messageId + "-file.txt" + dto.localDownloadState = .downloaded + dto.localRelativePath = localRelativePath + let localFileURL = URL.streamAttachmentLocalStorageURL(forRelativePath: localRelativePath) + try FileManager.default.createDirectory(at: localFileURL.deletingLastPathComponent(), withIntermediateDirectories: true) + try UUID().uuidString.write(to: localFileURL, atomically: false, encoding: .utf8) + XCTAssertTrue(FileManager.default.fileExists(atPath: localFileURL.path)) + } + return attachmentId + } } diff --git a/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift b/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift index f7463de502a..23afb08c5db 100644 --- a/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift @@ -1908,6 +1908,153 @@ final class MessageUpdater_Tests: XCTestCase { XCTAssertTrue(message.isPinned) XCTAssertEqual(pinExpires, message.pinDetails?.expiresAt) } + + // MARK: - Download Attachments + + func test_downloadAttachment_propagatesDownloadError() throws { + let attachment = try setUpAttachment(attachment: ChatMessageAudioAttachment.mock(id: .unique)) + let testError = TestError() + apiClient.downloadFile_completion_result = .failure(testError) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let error = try XCTUnwrap(result.error) + XCTAssertEqual(testError, error as? TestError) + } + + func test_downloadAttachment_audioAttachment_success() throws { + let attachment = try setUpAttachment(attachment: ChatMessageAudioAttachment.mock(id: .unique)) + apiClient.downloadFile_completion_result = .success(()) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let value = try XCTUnwrap(result.value) + XCTAssertEqual("Sample.wav", apiClient.downloadFile_localURL?.lastPathComponent) + XCTAssertEqual("http://asset.url/file.wav", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(attachment.id, value.id) + XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) + XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL) + } + + func test_downloadAttachment_fileAttachment_success() throws { + let attachment = try setUpAttachment( + attachment: ChatMessageFileAttachment.mock(id: .unique) + ) + apiClient.downloadFile_completion_result = .success(()) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let value = try XCTUnwrap(result.value) + XCTAssertEqual("Sample.pdf", apiClient.downloadFile_localURL?.lastPathComponent) + XCTAssertEqual("http://asset.url", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(attachment.id, value.id) + XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) + XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL) + } + + func test_downloadAttachment_imageAttachment_success() throws { + let attachment = try setUpAttachment( + attachment: ChatMessageImageAttachment.mock( + id: .unique, + imageURL: URL(string: "http://asset.url/image.jpg")!, + localState: nil + ) + ) + apiClient.downloadFile_completion_result = .success(()) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let value = try XCTUnwrap(result.value) + XCTAssertEqual("yoda.jpg", apiClient.downloadFile_localURL?.lastPathComponent) + XCTAssertEqual("http://asset.url/image.jpg", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(attachment.id, value.id) + XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) + XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL) + } + + func test_downloadAttachment_videoAttachment_success() throws { + let attachment = try setUpAttachment( + attachment: ChatMessageVideoAttachment.mock(id: .unique) + ) + apiClient.downloadFile_completion_result = .success(()) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let value = try XCTUnwrap(result.value) + XCTAssertEqual("Sample.mp4", apiClient.downloadFile_localURL?.lastPathComponent) + XCTAssertEqual("http://asset.url/video.mp4", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(attachment.id, value.id) + XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) + XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL) + } + + func test_downloadAttachment_voiceRecordingAttachment_success() throws { + let attachment = try setUpAttachment( + attachment: ChatMessageVoiceRecordingAttachment.mock( + id: .unique, + assetURL: URL(string: "http://asset.url/myrecording.aac")! + ) + ) + apiClient.downloadFile_completion_result = .success(()) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let value = try XCTUnwrap(result.value) + XCTAssertEqual("recording.aac", apiClient.downloadFile_localURL?.lastPathComponent) + XCTAssertEqual("http://asset.url/myrecording.aac", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(attachment.id, value.id) + XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) + XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL) + } + + func test_downloadAttachment_customAttachment_success() throws { + let attachment = try setUpAttachment( + attachment: ChatMessageCustomLocationAttachment( + id: .unique, + type: .customLocation, + payload: .init( + coordinate: .init(latitude: 52.3676, longitude: 4.9041), + mapURL: URL(string: "https://asset.url/map_preview")! + ), + downloadingState: nil, + uploadingState: nil + ) + ) + apiClient.downloadFile_completion_result = .success(()) + let result = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let value = try XCTUnwrap(result.value) + XCTAssertEqual("52.3676-4.9041", apiClient.downloadFile_localURL?.lastPathComponent) + XCTAssertEqual("https://asset.url/map_preview", apiClient.downloadFile_remoteURL?.absoluteString) + XCTAssertEqual(attachment.id, value.id) + XCTAssertEqual(LocalAttachmentDownloadState.downloaded, value.downloadingState?.state) + XCTAssertEqual(URL.streamAttachmentLocalStorageURL(forRelativePath: value.relativeStoragePath), value.downloadingState?.localFileURL) + } + + // MARK: - Delete Attachments + + func test_deleteLocalAttachmentDownload_propagatesAttachmentDoesNotExistError() throws { + let attachmentId = AttachmentId.unique + let error = try XCTUnwrap(waitFor { messageUpdater.deleteLocalAttachmentDownload(for: attachmentId, completion: $0) }) + XCTAssertEqual(ClientError.AttachmentDoesNotExist(id: attachmentId), error) + } + + func test_deleteLocalAttachmentDownload_success() throws { + let attachment = try setUpAttachment( + attachment: ChatMessageFileAttachment.mock(id: .unique) + ) + + // Download + apiClient.downloadFile_completion_result = .success(()) + let downloadResult = try waitFor { messageUpdater.downloadAttachment(attachment, completion: $0) } + let localFileURL = try XCTUnwrap(downloadResult.value?.downloadingState?.localFileURL) + + // Dummy file + try FileManager.default.createDirectory(at: localFileURL.deletingLastPathComponent(), withIntermediateDirectories: true) + try UUID().uuidString.write(to: localFileURL, atomically: false, encoding: .utf8) + XCTAssertTrue(FileManager.default.fileExists(atPath: localFileURL.path)) + + // Delete + let error = try waitFor { messageUpdater.deleteLocalAttachmentDownload(for: attachment.id, completion: $0) } + XCTAssertNil(error) + try database.readSynchronously { session in + guard let dto = session.attachment(id: attachment.id) else { + throw ClientError.AttachmentDoesNotExist(id: attachment.id) + } + XCTAssertEqual(nil, dto.localDownloadState) + XCTAssertEqual(nil, dto.localState) + XCTAssertEqual(nil, dto.localRelativePath) + XCTAssertEqual(nil, dto.localURL) + } + XCTAssertFalse(FileManager.default.fileExists(atPath: localFileURL.path)) + } // MARK: - Restart failed attachment uploading @@ -2932,4 +3079,52 @@ extension MessageUpdater_Tests { line: line ) } + + private func setUpAttachment( + attachment: ChatMessageAttachment, + messageId: MessageId = .unique, + cid: ChannelId = .unique + ) throws -> ChatMessageAttachment where PayloadData: DownloadableAttachmentPayload { + let attachmentId: AttachmentId = .init(cid: cid, messageId: messageId, index: 0) + try database.createChannel(cid: cid, withMessages: false) + try database.createMessage(id: messageId, cid: cid) + var result: ChatMessageAttachment! + try database.writeSynchronously { session in + let anyPayload = AnyAttachmentPayload(type: attachment.type, payload: attachment.payload, localFileURL: nil) + let dto = try session.createNewAttachment(attachment: anyPayload, id: attachmentId) + guard let anyModel = dto.asAnyModel() else { throw ClientError.AttachmentDecoding() } + guard let model = anyModel.attachment(payloadType: PayloadData.self) else { throw ClientError.AttachmentDecoding() } + result = model + } + return result + } } + +private extension AttachmentType { + static let customLocation = Self(rawValue: "custom_location") +} + +private struct LocationCoordinate: Codable, Hashable { + let latitude: Double + let longitude: Double +} + +private struct CustomLocationAttachmentPayload: AttachmentPayload { + static var type: AttachmentType = .customLocation + + var coordinate: LocationCoordinate + + var mapURL: URL +} + +extension CustomLocationAttachmentPayload: AttachmentPayloadDownloading { + var localStorageFileName: String { + "\(coordinate.latitude)-\(coordinate.longitude)" + } + + var remoteURL: URL { + mapURL + } +} + +private typealias ChatMessageCustomLocationAttachment = ChatMessageAttachment diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift index a30046c2890..504a30a7854 100644 --- a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift @@ -584,6 +584,7 @@ final class ChatChannelVC_Tests: XCTestCase { ) ] ), + downloadingState: nil, uploadingState: nil ).asAnyAttachment ], diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatFileAttachmentListView+ItemView_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatFileAttachmentListView+ItemView_Tests.swift index 45663b30f97..3311ad73172 100644 --- a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatFileAttachmentListView+ItemView_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatFileAttachmentListView+ItemView_Tests.swift @@ -28,6 +28,15 @@ final class ChatFileAttachmentListViewItemView_Tests: XCTestCase { fileAttachmentView.content = .mock(id: .unique) AssertSnapshot(fileAttachmentView, variants: [.defaultLight]) } + + func test_appearance_pdf_whenDownloadedThenShareIcon() throws { + let oldValue = Components.default.isDownloadFileAttachmentsEnabled + defer { Components.default.isDownloadFileAttachmentsEnabled = oldValue } + Components.default.isDownloadFileAttachmentsEnabled = true + fileAttachmentView.content = .mock(id: .unique, localState: nil, localDownloadState: .downloaded) + AssertSnapshot(fileAttachmentView, variants: [.defaultLight]) + Components.default.isDownloadFileAttachmentsEnabled = false + } func test_appearance_pdf_whenUploadingStateIsNil() { fileAttachmentView.content = .mock(id: .unique, localState: nil) diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatMessageVoiceRecordingAttachmentListViewItemView_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatMessageVoiceRecordingAttachmentListViewItemView_Tests.swift index e59e6899230..418d617692c 100644 --- a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatMessageVoiceRecordingAttachmentListViewItemView_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/ChatMessageVoiceRecordingAttachmentListViewItemView_Tests.swift @@ -189,7 +189,7 @@ final class ChatMessageVoiceRecordingAttachmentListViewItemView_Tests: XCTestCas subject.content = .mock(id: .unique, assetURL: .unique(), localState: .pendingUpload) subject.updateContent() - XCTAssertEqual(subject.fileSizeLabel.text, "0/120 bytes") + XCTAssertEqual(subject.fileSizeLabel.text, "0 / 120 bytes") } func test_updateContent_contentIsNil_loadingIndicatorWasConfiguredCorrectly() { diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/VideoAttachmentGalleryPreview_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/VideoAttachmentGalleryPreview_Tests.swift index d0e80d5baf5..d1c46f225c3 100644 --- a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/VideoAttachmentGalleryPreview_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/VideoAttachmentGalleryPreview_Tests.swift @@ -27,6 +27,7 @@ final class VideoAttachmentGalleryPreview_Tests: XCTestCase { file: try! .init(url: url), extraData: nil ), + downloadingState: nil, uploadingState: nil ) diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloaded.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloaded.default-light.png new file mode 100644 index 00000000000..06cdb11e2f9 Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloaded.default-light.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloadedThenShareIcon.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloadedThenShareIcon.default-light.png new file mode 100644 index 00000000000..06cdb11e2f9 Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloadedThenShareIcon.default-light.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/ChatMessageListVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/ChatMessageListVC_Tests.swift index d7f5d5af8d6..7af4d5bd264 100644 --- a/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/ChatMessageListVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/ChatMessageList/ChatMessageListVC_Tests.swift @@ -224,6 +224,7 @@ final class ChatMessageListVC_Tests: XCTestCase { id: attachmentId, type: .unknown, payload: attachmentWithCommentsData, + downloadingState: nil, uploadingState: nil ) } diff --git a/Tests/StreamChatUITests/SnapshotTests/CommonViews/QuotedChatMessageView/QuotedChatMessageView_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/CommonViews/QuotedChatMessageView/QuotedChatMessageView_Tests.swift index d89aaba8e4b..e26e90c4dda 100644 --- a/Tests/StreamChatUITests/SnapshotTests/CommonViews/QuotedChatMessageView/QuotedChatMessageView_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/CommonViews/QuotedChatMessageView/QuotedChatMessageView_Tests.swift @@ -163,6 +163,7 @@ final class QuotedChatMessageView_Tests: XCTestCase { id: .unique, type: .giphy, payload: .init(title: "", previewURL: TestImages.yoda.url, actions: []), + downloadingState: nil, uploadingState: nil ) diff --git a/Tests/StreamChatUITests/SnapshotTests/Gallery/Cells/VideoAttachmentGalleryCell_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/Gallery/Cells/VideoAttachmentGalleryCell_Tests.swift index 7b27258af9a..6a0103353d9 100644 --- a/Tests/StreamChatUITests/SnapshotTests/Gallery/Cells/VideoAttachmentGalleryCell_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/Gallery/Cells/VideoAttachmentGalleryCell_Tests.swift @@ -27,6 +27,7 @@ final class VideoAttachmentGalleryCell_Tests: XCTestCase { file: try! .init(url: url), extraData: nil ), + downloadingState: nil, uploadingState: nil ).asAnyAttachment diff --git a/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift index 6cd5f6c05c8..08b2676055d 100644 --- a/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift @@ -80,6 +80,7 @@ final class GalleryVC_Tests: XCTestCase { file: try! .init(url: TestImages.chewbacca.url), extraData: nil ), + downloadingState: nil, uploadingState: nil ) diff --git a/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift index 19bbb507a91..875690836f8 100644 --- a/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/VoiceRecording/Components/AudioQueuePlayerNextItemProvider_Tests.swift @@ -155,6 +155,7 @@ final class AudioQueuePlayerNextItemProvider_Tests: XCTestCase { id: .init(cid: .unique, messageId: .unique, index: 0), type: type, payload: try! encoder.encode(payload), + downloadingState: nil, uploadingState: nil ) } diff --git a/docusaurus/docs/iOS/client/attachment-downloads.md b/docusaurus/docs/iOS/client/attachment-downloads.md new file mode 100644 index 00000000000..110e733d015 --- /dev/null +++ b/docusaurus/docs/iOS/client/attachment-downloads.md @@ -0,0 +1,151 @@ +--- +title: Attachment Downloads +--- + +:::note +Available from `StreamChat` version 4.63.0. +::: + +## Introduction + +`StreamChat` supports uploading photos, audio, voice recordings, videos, and also other types of files. Uploaded files are represented with message attachments. Starting from 4.63.0 we provide a way of downloading these attachments locally on the device. + +## Downloading Attachments + +`StreamChat` has two different methods for downloading attachments locally depending on if you prefer to use completion handler based methods or the newer async-await supported [state layer](../state-layer/state-layer-overview.md). + +`ChatMessageController` has a new method `downloadAttachment(_:completion:)`. The method requires to pass in an instance of `ChatMessageAttachment` which we can retrieve from the controller managed `ChatMessage`. + +Here is an example of creating an instance of `ChatMessageController` using a channel id and a message id. `ChatMessage` has convenient properties for looking up attachments with a specific attachment type. In the example below, we know that this particular message has file attachments and therefore we go ahead and download the first file attachment of the message. When the attachment download has finished, the completion is called with updated `ChatMessageAttachment` value which contains the local file URL of the download. + +```swift +let message: ChatMessage = … +guard let fileAttachment = message.fileAttachments.first else { return } +let messageController = client.messageController( + cid: cid, + messageId: message.id +) +messageController.downloadAttachment(fileAttachment) { result in + switch result { + case .success(let downloadedAttachment): + let localFileURL = downloadedAttachment.downloadingState?.localFileURL + // … + case .failure(let error): + // … + } +} +``` + +If your app uses state-layer and its async-await method looks like this: + +```swift +let message: ChatMessage = … +let chat = client.makeChat(for: cid) +guard let fileAttachment = message.fileAttachments.first else { return } +let downloadedAttachment = try await chat.downloadAttachment(fileAttachment) +let localFileURL = downloadedAttachment.downloadingState?.localFileURL +``` + +When the attachment is being downloaded, its `downloadingState.state` is updated with the download progress and when the download finishes, the last state is stored which is either `downloaded` or `downloadFailed`. + +Here is an example of observing the download progress of a file attachment download. + +```swift +messageController.messageChangePublisher + .compactMap(\.item.fileAttachments.first?.downloadingState?.state) + .sink { state in + switch state { + case .downloaded: + print("Downloaded") + case .downloading(let progress): + print("Downloading: \(progress)") + case .downloadingFailed: + print("Downloading failed") + } + } + .store(in: &cancellables) +messageController.downloadAttachment(attachment) { result in + // … +} +``` + +The same, but with async-await compatible state-layer. + +```swift +let chat = client.makeChat(for: cid) +let messageState = try await chat.messageState(for: message.id) +messageState.$message + .compactMap(\.fileAttachments.first?.downloadingState?.state) + .sink { state in + switch state { + case .downloaded: + print("Downloaded") + case .downloading(let progress): + print("Downloading: \(progress)") + case .downloadingFailed: + print("Downloading failed") + } + } + .store(in: &cancellables) +try await chat.downloadAttachment(attachment) +``` + +:::note +Always access the local file URL using Stream's API because the absolute URL can change between app launches. +::: + +### Supporting Custom Attachment Downloads + +If your app is using custom attachment types then we can enable downloading the custom attachment by conforming to the `AttachmentPayloadDownloading` protocol. The protocol requires to define a file name used for storing the attachment locally and a URL of the downloadable file. Below we can see an example of a custom attachment which conforms to the `AttachmentPayloadDownloading`. + +```swift +extension AttachmentType { + static let customLocation = Self(rawValue: "custom_location") +} + +struct LocationCoordinate: Codable, Hashable { + let latitude: Double + let longitude: Double +} + +struct CustomLocationAttachmentPayload: AttachmentPayload { + static var type: AttachmentType = .customLocation + var coordinate: LocationCoordinate + var mapURL: URL +} + +extension CustomLocationAttachmentPayload: AttachmentPayloadDownloading { + var localStorageFileName: String { + "\(coordinate.latitude)-\(coordinate.longitude)" + } + + var remoteURL: URL { + mapURL + } +} + +typealias ChatMessageCustomLocationAttachment = ChatMessageAttachment +``` + +## Deleting Local Downloads + +When the local download is not needed anymore, we can delete it. `ChatMessageController` and `Chat` have a delete attachment method and if we prefer to delete all the local downloads, then we can use `CurrentChatUser` and `ConnectedUser` methods to do so. + +```swift +// A delete single download +let controller = client.messageController(cid: cid, messageId: message.id) +controller.deleteLocalAttachmentDownload(for: attachment.id) { error in + // … +} +// Delete all downloads +client.currentUserController().deleteAllLocalAttachmentDownloads { error in + // … +} +``` + +```swift +// A delete single download +try await chat.deleteLocalAttachmentDownload(for: attachment.id) +// Delete all downloads +try await client.makeConnectedUser().deleteAllLocalAttachmentDownloads() +``` diff --git a/docusaurus/docs/iOS/swiftui/channel-list-components/helper-views.md b/docusaurus/docs/iOS/swiftui/channel-list-components/helper-views.md index 6c4d3907ac5..b177c100e73 100644 --- a/docusaurus/docs/iOS/swiftui/channel-list-components/helper-views.md +++ b/docusaurus/docs/iOS/swiftui/channel-list-components/helper-views.md @@ -263,3 +263,105 @@ var body: some Scene { } } ``` + +## Search Results View + +You can change the search results view with your own implementation. In order to do that, you should implement the following method: + +```swift +func makeSearchResultsView( + selectedChannel: Binding, + searchResults: [ChannelSelectionInfo], + loadingSearchResults: Bool, + onlineIndicatorShown: @escaping (ChatChannel) -> Bool, + channelNaming: @escaping (ChatChannel) -> String, + imageLoader: @escaping (ChatChannel) -> UIImage, + onSearchResultTap: @escaping (ChannelSelectionInfo) -> Void, + onItemAppear: @escaping (Int) -> Void +) -> some View { + CustomSearchResultsView( + factory: self, + selectedChannel: selectedChannel, + searchResults: searchResults, + loadingSearchResults: loadingSearchResults, + onlineIndicatorShown: onlineIndicatorShown, + channelNaming: channelNaming, + imageLoader: imageLoader, + onSearchResultTap: onSearchResultTap, + onItemAppear: onItemAppear + ) +} +``` + +In case you want to use the default implementation, you can still customize the individual search result items, with the following method: + +```swift +func makeChannelListSearchResultItem( + searchResult: ChannelSelectionInfo, + onlineIndicatorShown: Bool, + channelName: String, + avatar: UIImage, + onSearchResultTap: @escaping (ChannelSelectionInfo) -> Void, + channelDestination: @escaping (ChannelSelectionInfo) -> ChannelDestination +) -> some View { + SearchResultItem( + searchResult: searchResult, + onlineIndicatorShown: onlineIndicatorShown, + channelName: channelName, + avatar: avatar, + onSearchResultTap: onSearchResultTap, + channelDestination: channelDestination + ) +} +``` + +## Navigation Bar display mode + +You can change the display mode of the navigation bar to be either `large`, `automatic` or `inline` (which is the default value). To do that, you should implement the following method: + +```swift +func navigationBarDisplayMode() -> NavigationBarItem.TitleDisplayMode { + .inline +} +``` + +## More Channel Actions View + +In the default implementation, when you press on the more button of a channel list item, a view with the actions about the channel is shown (muting, deleting and more). You can provide your own implementation of this view, with the following method: + +```swift +func makeMoreChannelActionsView( + for channel: ChatChannel, + swipedChannelId: Binding, + onDismiss: @escaping () -> Void, + onError: @escaping (Error) -> Void + ) -> some View { + MoreChannelActionsView( + channel: channel, + channelActions: supportedMoreChannelActions( + for: channel, + onDismiss: onDismiss, + onError: onError + ), + swipedChannelId: swipedChannelId, + onDismiss: onDismiss + ) + } +``` + +Additionally, you can customize only the presented actions in the view above, by implementing the following method instead: + +```swift +func supportedMoreChannelActions( + for channel: ChatChannel, + onDismiss: @escaping () -> Void, + onError: @escaping (Error) -> Void +) -> [ChannelAction] { + ChannelAction.defaultActions( + for: channel, + chatClient: chatClient, + onDismiss: onDismiss, + onError: onError + ) +} +``` \ No newline at end of file diff --git a/docusaurus/docs/iOS/swiftui/message-components/attachments.md b/docusaurus/docs/iOS/swiftui/message-components/attachments.md index 78643150ba4..28571959be6 100644 --- a/docusaurus/docs/iOS/swiftui/message-components/attachments.md +++ b/docusaurus/docs/iOS/swiftui/message-components/attachments.md @@ -82,6 +82,110 @@ var body: some Scene { These are all the steps needed to change the default SDK view with your custom one. +### Image Attachment View + +Similarly, you can change the other types of attachments view in the SDK. To update the view that presents images, you need to implement the following method: + +```swift +func makeImageAttachmentView( + for message: ChatMessage, + isFirst: Bool, + availableWidth: CGFloat, + scrolledId: Binding +) -> some View { + CustomImageAttachment( + factory: self, + message: message, + width: availableWidth, + isFirst: isFirst, + scrolledId: scrolledId + ) +} +``` + +### Giphy Attachment View + +To update the view that presents gifs, you should implement the following method: + +```swift +func makeGiphyAttachmentView( + for message: ChatMessage, + isFirst: Bool, + availableWidth: CGFloat, + scrolledId: Binding +) -> some View { + GiphyAttachmentView( + factory: self, + message: message, + width: availableWidth, + isFirst: isFirst, + scrolledId: scrolledId + ) +} +``` + +### Link Attachment View + +You can also change the way links are displayed in the message list, by implementing the following method: + +```swift +func makeLinkAttachmentView( + for message: ChatMessage, + isFirst: Bool, + availableWidth: CGFloat, + scrolledId: Binding +) -> some View { + CustomLinkAttachmentView( + factory: self, + message: message, + width: availableWidth, + isFirst: isFirst, + scrolledId: scrolledId + ) +} +``` + +### File Attachment View + +File attachments can be customized by implementing the method below: + +```swift +func makeFileAttachmentView( + for message: ChatMessage, + isFirst: Bool, + availableWidth: CGFloat, + scrolledId: Binding +) -> some View { + CustomFileAttachmentsView( + factory: self, + message: message, + width: availableWidth, + isFirst: isFirst, + scrolledId: scrolledId + ) +} +``` + +### Video Attachments + +To replace the way video attachments are presented, you need to provide your own implementation of this method: + +```swift +func makeVideoAttachmentView( + for message: ChatMessage, + isFirst: Bool, + availableWidth: CGFloat, + scrolledId: Binding +) -> some View { + CustomVideoAttachmentsView( + factory: self, + message: message, + width: availableWidth, + scrolledId: scrolledId + ) +} +``` + ## Handling Custom Attachments You can go a step further and introduce your own custom attachments with their corresponding custom views. Use-cases can be workout attachments, food delivery, money sending and anything else that might be supported within your apps. diff --git a/docusaurus/sidebars-ios.json b/docusaurus/sidebars-ios.json index a70ae020b05..2bc94cd85f7 100644 --- a/docusaurus/sidebars-ios.json +++ b/docusaurus/sidebars-ios.json @@ -121,6 +121,7 @@ "client/push-notifications", "guides/video-integration", "client/custom-cdn", + "client/attachment-downloads", "guides/moderation", "guides/go-live-checklist", { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f366656ca55..768e71e3df6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -17,6 +17,7 @@ metrics_git = 'git@github.com:GetStream/stream-internal-metrics.git' xcmetrics_path = "metrics/#{github_repo.split('/').last}-xcmetrics.json" buildcache_xcargs = 'CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++' testlab_bucket = 'gs://test-lab-af3rt9m4yh360-mqm1zzm767nhc' +swift_environment_path = File.absolute_path('../Sources/StreamChat/Generated/SystemEnvironment+Version.swift') is_localhost = !is_ci @force_check = false @@ -58,7 +59,6 @@ desc 'Start a new release' lane :release do |options| previous_version_number = last_git_tag artifacts_path = File.absolute_path('../StreamChatArtifacts.json') - swift_environment_path = File.absolute_path('../Sources/StreamChat/Generated/SystemEnvironment+Version.swift') extra_changes = lambda do |release_version| # Set the framework version on the artifacts artifacts = JSON.parse(File.read(artifacts_path)) @@ -86,10 +86,18 @@ lane :release do |options| ) end +lane :merge_release do |options| + merge_release_to_main(author: options[:author]) +end + desc 'Completes an SDK Release' lane :publish_release do |options| - xcversion(version: '14.0.1') + release_version = File.read(swift_environment_path).match(/String\s+=\s+"([\d.]+)"/)[1] + UI.user_error!("Release #{release_version} has already been published.") if git_tag_exists(tag: release_version, remote: true) + UI.user_error!('Release version cannot be empty') if release_version.to_s.empty? + ensure_git_branch(branch: 'main') + xcversion(version: '14.0.1') clean_products build_xcframeworks compress_frameworks @@ -97,66 +105,18 @@ lane :publish_release do |options| publish_ios_sdk( skip_git_status_check: false, - version: options[:version], + version: release_version, sdk_names: sdk_names, podspec_names: ['StreamChat', 'StreamChat-XCFramework', 'StreamChatUI', 'StreamChatUI-XCFramework'], github_repo: github_repo, upload_assets: ['Products/StreamChat.zip', 'Products/StreamChatUI.zip', 'Products/StreamChat-All.zip'] ) - update_spm(version: options[:version]) + update_spm(version: release_version) merge_main_to_develop end -lane :merge_release_to_main do |options| - ensure_git_status_clean - - release_branch = - if is_ci - # This API operation needs the "admin:org" scope. - ios_team = sh('gh api orgs/GetStream/teams/ios-developers/members -q ".[].login"', log: false).split - UI.user_error!("#{options[:author]} is not a member of the iOS Team") unless ios_team.include?(options[:author]) - - current_branch - else - release_branches = sh(command: 'git branch -a', log: false).delete(' ').split("\n").grep(%r(origin/.*release/)) - UI.user_error!("Expected 1 release branch, found #{release_branches.size}") if release_branches.size != 1 - - release_branches.first - end - - UI.user_error!("`#{release_branch}`` branch does not match the release branch pattern: `release/*`") unless release_branch.start_with?('release/') - - sh('git config pull.ff only') - sh('git fetch --all --tags --prune') - sh("git checkout #{release_branch}") - sh("git pull origin #{release_branch} --ff-only") - sh('git checkout main') - sh('git pull origin main --ff-only') - - # Merge release branch to main. For more info, read: https://notion.so/iOS-Branching-Strategy-37c10127dc26493e937769d44b1d6d9a - sh("git merge #{release_branch} --ff-only") - sh('git push origin main') - - comment = "[Publication of the release](https://github.com/#{github_repo}/actions/workflows/release-publish.yml) has been launched 👍" - UI.important(comment) - pr_comment(text: comment) -end - -lane :merge_main_to_develop do - ensure_git_status_clean - sh('git config pull.ff only') - sh('git fetch --all --tags --prune') - sh('git checkout main') - sh('git pull origin main --ff-only') - sh('git checkout develop') - sh('git pull origin develop --ff-only') - sh('git log develop..main') - sh('git merge main') - sh('git push origin develop') -end - desc 'Compresses the XCFrameworks into zip files' lane :compress_frameworks do Dir.chdir('..') do diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 507100b655b..f588e87c06f 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -5,4 +5,4 @@ gem 'fastlane-plugin-versioning' gem 'fastlane-plugin-create_xcframework' gem 'fastlane-plugin-sonarcloud_metric_kit' -gem 'fastlane-plugin-stream_actions', '0.3.60' +gem 'fastlane-plugin-stream_actions', '0.3.63'