From 6596740f247cd3af43c639d5e96d77dd46c64556 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 19 Aug 2024 10:52:04 +0100 Subject: [PATCH 01/17] [CI] Share lanes across projects (#3391) --- .github/workflows/release-merge.yml | 2 +- Gemfile.lock | 4 +-- fastlane/Fastfile | 52 +++-------------------------- fastlane/Pluginfile | 2 +- 4 files changed, 8 insertions(+), 52 deletions(-) diff --git a/.github/workflows/release-merge.yml b/.github/workflows/release-merge.yml index a100effec1..c8c29253c3 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/Gemfile.lock b/Gemfile.lock index 3849c5a49a..fd3489e1c1 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) @@ -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/fastlane/Fastfile b/fastlane/Fastfile index f366656ca5..75ede29f5d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -86,6 +86,10 @@ 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') @@ -109,54 +113,6 @@ lane :publish_release do |options| 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 507100b655..f588e87c06 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' From 87bf483691da5a81ad049d45f26781b156434bdf Mon Sep 17 00:00:00 2001 From: Sagar Dagdu Date: Mon, 19 Aug 2024 16:09:46 +0530 Subject: [PATCH 02/17] Fix the thread name printed by the `Logger` (#3382) * Fix the thread name printed by the `Logger` - The thread name was being read inside `loggerQueue.async{}`, which made it always print `io.getstream.logconfig`. Fixed to read it before entering the dispatch queue, and now it prints the correct thread name. * Update CHANGELOG.md --------- Co-authored-by: Nuno Vieira --- CHANGELOG.md | 4 +++- Sources/StreamChat/Utils/Logger/Logger.swift | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b91bcc2c82..be6a66b22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming -### 🔄 Changed +## StreamChat +### 🐞 Fixed +- Fix Logger printing the incorrect thread name [#3382](https://github.com/GetStream/stream-chat-swift/pull/3382) # [4.62.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.62.0) _August 15, 2024_ diff --git a/Sources/StreamChat/Utils/Logger/Logger.swift b/Sources/StreamChat/Utils/Logger/Logger.swift index e5f463a92f..d3daeb552b 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 From f64d4e33161de3077e86be1476f27eb7d9564777 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 22 Aug 2024 10:45:43 +0100 Subject: [PATCH 03/17] [CI] Update publish release flow (#3388) --- .github/workflows/release-publish.yml | 30 ++------------------------- fastlane/Fastfile | 11 ++++++---- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c73861b766..8139338ef1 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 @@ -29,28 +21,10 @@ jobs: - 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/fastlane/Fastfile b/fastlane/Fastfile index 75ede29f5d..03f7b655fa 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)) @@ -92,8 +92,11 @@ 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) + ensure_git_branch(branch: 'main') + xcversion(version: '14.0.1') clean_products build_xcframeworks compress_frameworks @@ -101,14 +104,14 @@ 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 From 137775c59e92b4adce4c2994ac09008136d4fab0 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 22 Aug 2024 11:07:03 +0100 Subject: [PATCH 04/17] [CI] Ensure release version is not empty (#3392) --- fastlane/Fastfile | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 03f7b655fa..768e71e3df 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -94,6 +94,7 @@ desc 'Completes an SDK Release' lane :publish_release do |options| 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') From 29ff68b149db562db18a368955634454c0662267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 09:15:31 +0300 Subject: [PATCH 05/17] Bump rexml from 3.3.3 to 3.3.6 (#3396) Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.6. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fd3489e1c1..824883faec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) From cf2645a5f922e34aa0719959d3137ed9998b3a75 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Fri, 30 Aug 2024 12:48:01 +0200 Subject: [PATCH 06/17] Updated the SwiftUI docs for customizing views (#3400) --- .../channel-list-components/helper-views.md | 102 +++++++++++++++++ .../swiftui/message-components/attachments.md | 104 ++++++++++++++++++ 2 files changed, 206 insertions(+) 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 6c4d3907ac..b177c100e7 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 78643150ba..28571959be 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. From db33d318001e147cc13d1a900d6536da91519f98 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 30 Aug 2024 12:23:28 +0100 Subject: [PATCH 07/17] [CI] Do not cache iOS Simulator Runtimes nightly (#3401) --- .github/actions/setup-ios-runtime/action.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index 2aa2f04547..aeec23e132 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -3,13 +3,6 @@ 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: | From 16ae812f1694a4a6a7fad7a5e3fc6b28f5451afd Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Fri, 30 Aug 2024 15:47:36 +0300 Subject: [PATCH 08/17] Download and share attachments (#3393) * Download and share file attachments * Add methods for deleting local downloads and clear downloaded files on logout * Make AttachmentDownloadingState.localFileURL optional because it is valid only when downloading has been finished * Add tests * Add a separate property for storing the download state and a new enum for representing it * Allow downloading any kind of attachment * Add snapshot test for downloaded file * Add a feature toggle * Add documentation for file downloads * Show download button for failed download --- CHANGELOG.md | 9 + .../AppConfigViewController.swift | 5 + .../DemoChatChannelListRouter.swift | 14 ++ Sources/StreamChat/APIClient/APIClient.swift | 32 ++- .../AttachmentDownloader.swift | 71 +++++++ .../StreamChat/ChatClient+Environment.swift | 10 +- Sources/StreamChat/ChatClientFactory.swift | 2 + .../CurrentUserController.swift | 12 ++ .../MessageController/MessageController.swift | 35 +++- .../Database/DTOs/AttachmentDTO.swift | 129 +++++++++++- .../Database/DatabaseContainer.swift | 8 + .../StreamChat/Database/DatabaseSession.swift | 3 + .../StreamChatModel.xcdatamodel/contents | 4 +- .../Models/Attachments/AttachmentTypes.swift | 10 + .../Attachments/ChatMessageAttachment.swift | 72 +++++++ .../ChatMessageAudioAttachment.swift | 12 ++ .../ChatMessageFileAttachment.swift | 12 ++ .../ChatMessageGiphyAttachment.swift | 2 +- .../ChatMessageImageAttachment.swift | 12 ++ .../ChatMessageVideoAttachment.swift | 12 ++ .../ChatMessageVoiceRecordingAttachment.swift | 12 ++ Sources/StreamChat/StateLayer/Chat.swift | 39 +++- .../StreamChat/StateLayer/ConnectedUser.swift | 11 + .../StreamChat/Workers/ChannelUpdater.swift | 1 + .../Workers/CurrentUserUpdater.swift | 32 +++ .../StreamChat/Workers/MessageUpdater.swift | 113 +++++++++- Sources/StreamChatUI/Appearance+Images.swift | 14 +- .../ChatFileAttachmentListView+ItemView.swift | 64 ++++-- .../UnsupportedAttachmentViewInjector.swift | 1 + .../Attachments/UploadingOverlayView.swift | 50 ++++- ...RecordingAttachmentListView+ItemView.swift | 4 +- .../ChatMessageList/ChatMessageListVC.swift | 32 ++- Sources/StreamChatUI/Components.swift | 3 + StreamChat.xcodeproj/project.pbxproj | 28 ++- .../AnyAttachmentPayload_Mock.swift | 1 + .../ChatMessageAudioAttachment_Mock.swift | 43 ++++ .../ChatMessageFileAttachment_Mock.swift | 8 + .../ChatMessageImageAttachment_Mock.swift | 8 + .../ChatMessageLinkAttachment_Mock.swift | 1 + .../ChatMessageVideoAttachment_Mock.swift | 45 ++++ ...MessageVoiceRecordingAttachment_Mock.swift | 8 + .../Database/DatabaseSession_Mock.swift | 4 + .../Workers/CurrentUserUpdater_Mock.swift | 11 + .../Workers/MessageUpdater_Mock.swift | 38 ++++ .../SpyPattern/Spy/APIClient_Spy.swift | 25 +++ .../Spy/AttachmentDownloader_Spy.swift | 25 +++ .../DummyData/ChatMessageAttachment.swift | 2 + .../APIClient/APIClient_Tests.swift | 5 + .../ChatPushNotificationContent_Tests.swift | 2 +- Tests/StreamChatTests/ChatClient_Tests.swift | 3 +- .../CurrentUserController_Tests.swift | 25 +++ .../Database/DatabaseContainer_Tests.swift | 10 +- .../AnyAttachmentPayload_Tests.swift | 3 + .../AnyAttachmentUpdater_Tests.swift | 1 + .../ChatMessageAttachment_Tests.swift | 3 + .../StateLayer/Chat_Tests.swift | 41 +++- .../StateLayer/ConnectedUser_Tests.swift | 20 ++ .../Workers/CurrentUserUpdater_Tests.swift | 50 +++++ .../Workers/MessageUpdater_Tests.swift | 195 ++++++++++++++++++ .../ChatChannel/ChatChannelVC_Tests.swift | 1 + ...ileAttachmentListView+ItemView_Tests.swift | 9 + ...dingAttachmentListViewItemView_Tests.swift | 2 +- .../VideoAttachmentGalleryPreview_Tests.swift | 1 + ...rance_pdf_whenDownloaded.default-light.png | Bin 0 -> 5561 bytes ...nDownloadedThenShareIcon.default-light.png | Bin 0 -> 5561 bytes .../ChatMessageListVC_Tests.swift | 1 + .../QuotedChatMessageView_Tests.swift | 1 + .../VideoAttachmentGalleryCell_Tests.swift | 1 + .../Gallery/GalleryVC_Tests.swift | 1 + ...dioQueuePlayerNextItemProvider_Tests.swift | 1 + .../docs/iOS/client/attachment-downloads.md | 151 ++++++++++++++ docusaurus/sidebars-ios.json | 1 + 72 files changed, 1555 insertions(+), 62 deletions(-) create mode 100644 Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift create mode 100644 TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageAudioAttachment_Mock.swift create mode 100644 TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/ChatMessageVideoAttachment_Mock.swift create mode 100644 TestTools/StreamChatTestTools/SpyPattern/Spy/AttachmentDownloader_Spy.swift create mode 100644 Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloaded.default-light.png create mode 100644 Tests/StreamChatUITests/SnapshotTests/ChatMessageList/Attachments/__Snapshots__/ChatFileAttachmentListView+ItemView_Tests/test_appearance_pdf_whenDownloadedThenShareIcon.default-light.png create mode 100644 docusaurus/docs/iOS/client/attachment-downloads.md diff --git a/CHANGELOG.md b/CHANGELOG.md index be6a66b22e..99969a8044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming ## 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` ### 🐞 Fixed - Fix Logger printing the incorrect thread name [#3382](https://github.com/GetStream/stream-chat-swift/pull/3382) +## 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 16d14ab5a7..1611c9761c 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 03e5772702..291e78c25a 100644 --- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift +++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift @@ -518,6 +518,20 @@ 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) + } }) ]) } diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift index d753ae7b53..27ec8606fc 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 } @@ -288,7 +293,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 0000000000..8db4b99038 --- /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/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift index df00cd15d8..9d86085105 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/ChatClientFactory.swift b/Sources/StreamChat/ChatClientFactory.swift index dc7834c5d3..39fd01a0ba 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 d85e5f0efb..cf01b2215f 100644 --- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift +++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift @@ -256,6 +256,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 558bdccb78..54ae0d23df 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 a127f61150..44674e3528 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 4fb3ec1fe1..ec2edebfb2 100644 --- a/Sources/StreamChat/Database/DatabaseContainer.swift +++ b/Sources/StreamChat/Database/DatabaseContainer.swift @@ -340,6 +340,14 @@ 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 fb9c26b08f..900ffbdbdc 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 dd35fc9f1f..86a53ad607 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/Models/Attachments/AttachmentTypes.swift b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift index 9eef570990..0093a4bff3 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 f0a3d33ca1..de7e45ec92 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 57c2339cd2..029b960c5e 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 3feb03b163..65d3e4090d 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 69e26e304e..45cb5fde47 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 979ccadaa1..cc47f895fe 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 49fd4c7615..028145c2d5 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 d77159542f..700b94ed23 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/StateLayer/Chat.swift b/Sources/StreamChat/StateLayer/Chat.swift index 4476f6dcfe..66eb12b457 100644 --- a/Sources/StreamChat/StateLayer/Chat.swift +++ b/Sources/StreamChat/StateLayer/Chat.swift @@ -352,16 +352,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/ConnectedUser.swift b/Sources/StreamChat/StateLayer/ConnectedUser.swift index 06d42a0028..6c01e60a24 100644 --- a/Sources/StreamChat/StateLayer/ConnectedUser.swift +++ b/Sources/StreamChat/StateLayer/ConnectedUser.swift @@ -176,6 +176,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/Workers/ChannelUpdater.swift b/Sources/StreamChat/Workers/ChannelUpdater.swift index 6417555271..72c7c3f676 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 e442a24c7d..d3eb9a6974 100644 --- a/Sources/StreamChat/Workers/CurrentUserUpdater.swift +++ b/Sources/StreamChat/Workers/CurrentUserUpdater.swift @@ -155,6 +155,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 +248,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 diff --git a/Sources/StreamChat/Workers/MessageUpdater.swift b/Sources/StreamChat/Workers/MessageUpdater.swift index fe4276b9ab..8f88d44dec 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 b935e9a212..1d0e5ea6c4 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 170753ba8f..8a6f3a3784 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 b163b25f88..9623510c89 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 230ea176ed..a6d59fa41e 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 8e733085d0..40e4a3a542 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 4247f7b71e..3eec30f129 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 694c5187e7..82e5bf0397 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/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index 68f3a47d2b..fe26cb9982 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -255,6 +255,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 +305,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 +326,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 */; }; @@ -3090,6 +3095,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 +3125,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 +3137,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 +5187,14 @@ path = StateLayer; sourceTree = ""; }; + 4FF9B2662C6F695C00A3B711 /* AttachmentDownloader */ = { + isa = PBXGroup; + children = ( + 4FF9B2672C6F696B00A3B711 /* AttachmentDownloader.swift */, + ); + path = AttachmentDownloader; + sourceTree = ""; + }; 790881AB254327C800896F03 /* StreamChat */ = { isa = PBXGroup; children = ( @@ -5775,6 +5792,7 @@ 79DDF80D249CB920002F4412 /* RequestDecoder.swift */, 7964F3BB249A5E60002A09EC /* RequestEncoder.swift */, ADB951A3291BD7F700800554 /* CDNClient */, + 4FF9B2662C6F695C00A3B711 /* AttachmentDownloader */, ADB951A7291BD85300800554 /* AttachmentUploader */, 79877A122498E4EE00015F8B /* Endpoints */, ); @@ -6658,10 +6676,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 = ""; @@ -8126,6 +8146,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 +10840,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 +10914,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 +10996,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 +11059,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 */, @@ -12078,6 +12103,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 */, diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/AnyAttachmentPayload_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/Attachments/AnyAttachmentPayload_Mock.swift index 9effc27611..41c94c1f81 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 0000000000..818101fdb0 --- /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 9e656d77d1..a29a5f5da0 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 4772c2cf69..3c57c457f5 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 7178d116c1..2fe095ed03 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 0000000000..f352048091 --- /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 9e65859e62..d46ca5318d 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 a32ac46c0d..e2909c43e4 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/Workers/CurrentUserUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift index 6410ba82bb..14b735f34c 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift @@ -29,6 +29,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, @@ -75,6 +78,11 @@ 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() { @@ -97,6 +105,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 b422d1a706..115daecfaf 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 96ce09bf78..b267788601 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 0000000000..b8be029507 --- /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 c9861e562f..f31f359af2 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 08bb42f50c..b739bc1dad 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 71388804fb..63536bce5a 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/ChatClient_Tests.swift b/Tests/StreamChatTests/ChatClient_Tests.swift index c18336df07..491bc96b76 100644 --- a/Tests/StreamChatTests/ChatClient_Tests.swift +++ b/Tests/StreamChatTests/ChatClient_Tests.swift @@ -905,7 +905,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 7538c3a09e..e4695edf59 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 3a3b0a52a4..98e89fb9ee 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 61f49d75b3..eebde5f22c 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 d59bcc5cd1..6397124108 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 fe120db267..9f8a4eb17c 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/StateLayer/Chat_Tests.swift b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift index c11d6a1e72..2eae2be05a 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 1aab8feebe..e4f1c969b0 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 183db2e828..c042d982f9 100644 --- a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift @@ -643,4 +643,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 f7463de502..23afb08c5d 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 a30046c289..504a30a785 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 45663b30f9..3311ad7317 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 e59e689923..418d617692 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 d0e80d5baf..d1c46f225c 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 0000000000000000000000000000000000000000..06cdb11e2f9ebec98d45c4ee3d0f425677c9d30c GIT binary patch literal 5561 zcmYLt1y~eO_xJ2B-KEqLl7e)%3oNmu(hVXlCEcxn0)li1E7DSnbP2L_Nry10YD4F{6E_Oa{IqNPymQ` z1@QjsV*%=aMH3XT^?x-C4f+3XL_`1QjX}fyXUqL-W}D6UuQy_EO>!m?{Q2D!9U8(lZ|p(O-Vew=iD#yXnR+~J;VZ7)z+9z4Jq5s~x8_(}VNB%^ z7-2|}VLe+^06c3F;MD5MxgiD8uRRH0|2;c+gVfL%ji+RL86I|dqPrEfG~iz=%eub` z@g|Mv3ldj(AbSha!xa|i6&dw;A+~>iWtbK-!c9!Rv(7>pbPTlOzkksA16Dyy4Dh%y z?VlfB_Qn$WT%LFYUtRdC^zoWc)*kGCR}DT|Put}{b>EMYw=i$6Hh-s{0+*#(PK$`R zq!+ds0@vR0bazp1t!yw~+RI^W;#GrPFI3ihIqLrPZ?FH1gm72)j~bgo^z~9#C|#Yo zZUjt>)cyxH(V*Ggd^kt+jl)ENd6iKm$KmRD`n|Bkcfp5F7%_L(rOvh%SE7+7!*ab! zL2m7=S#&>oJHvl#&R8+vWb@OV+s$WkL}yog35wB^wsMHD9h{^It^eHz{k$5>n&Wwr zhVjnBFV&bN-MEf7KczRi&Qy8!L_uzy@`rzI_HeV@nyX1aT#wiYy*P4hDz$3!5u(yi zSFftI?|(cxXN}NF|8+zgnr=q31DQr{SUTfXqX5h8E^IWmlh1P(*875l^ z4?fwR-`vjZ76cJOo1aWqTN=lXAW7Xjia86yX{ap#m=r%UeRdR0WkJa7_xMir@J zujEa?mT70}ZJ;^=c8==a-W0OBeg6Ab`fR14_EfQ&BRGk&x6S2yPsy1b#&Sg46Q;}c zg^k~uWLkFwwBdrGqRWhuZW$}%`KBJRH=A5%)P-z1*EiQT29p@YtxhI##WjAf40C=V zYqDr^GcFK9S_km2o0RF~mS|@SH3aV2pfdS0GlXqz0{hE!^EV}-szE`mD4E#bQVB0KN0Qg|7X_PC0o2xcAoT=dQs&F` z*}CFg>2KU9-C0mh@<@8<4$C?E)oD~|kXP-dLCFRb1>lq3q#NjO=F!copy)DJKrVrM zpt9R%?VFkXoOySLPpo-6LGb#-yvWPR|X-*LGoy8X5y+102KgisWSxHzixe1kNV zN1rE)&!3P-jRzV;>j;K^L@U>#w9`1AEdSAaB{z&~dStCYi9^I8oy(~dLykUngRArj zC5o;vEKk9ufPdZ&j-ZE4K0xOyrr)H>2z8t<nS(yS87AYpu-J2)QHHzhK^M zVi@J(+!tE!`+rQlmk|}qps}peGjifVDEfJ+JN#`XG0f%?9Cy;2)4gwX_hV5Ua$*=b zSGc!2=NDic&;+3`K+R|!W{*&Co#s+pi`68JN_0Tp;S``7KwCX!pqk8+)SetCES11P z=wvohSmciuxB{Ni+^3Ez6tHO2%n~p!K8lMFv}hD*d3Nf7Pzc&zmifH=XIt+hX$&!S zUZ}F{w$IuAgKQ51tOBKneBiFT7n9igSg3s!WhX11g3Le!;MN>kMI-b1YMBXfYBG4T zs#{D-iCP)XlYFa3kVpb861<0wQdg9D00I==#7Nxz6jic>`RQb z!v)PEHOzjqHsDq;$8>{#`cYeK&_e6u5H|Oi*gP2U0n-&d92GQ@!FO^iGsa=E(E3~u zWe&1AT0Ka$0$ypOo@OR6+1$K?-qhhG|bmD5H8 zk7upZs^{cL^Vtm?Uk@FctzvvaONkWQ^d{_MV9G~OGw<^ZOzeFSnx-6M4KkfwSc3@+2i21Bq>-F#$LqK%C z0WT}Q_tr$PRLDq8$2EDIB}H>}GK(C&TspM>z3+ITThXu$P5!f9BsoWr$upTjP8pSq zF%wa%5rc=n76N0?OhCrfN5&}SS0J*L$rVOd5cjyk7G)p*h&z!FIjY$F$R_8f=}&+~ z1`lxED!o83YdNfbl4`68Io_-)kI?gfg5fu+K+A>o!r3!c*C!9TO_d^U49lP#rffMv zYnoKKQks1}>oKe4^|uR7sp64iksKy+_dfx%*?ZU+2Fg_I(au-XN+@-*2i-nEX2^|Z zVRu<*#jH#g+46cVw)?Bl5Uf{Nv3t8tfMKF?Eo?sz4%8#YkZ;%&LmHbG-#Js33sJpe zp%wj}yT5EwW0A;LH$@@W$DkTd@yiH*1aQ`vgkmGAV9pZ-qPh}Vh9_^3LtpiuEz8Q&sMJ3{;0LoJ9ApoF9tu64c}P05x$Y(l%$F$R18n;1&kf%4L}`o?uhZ!B)uO>x0D2 zJcgWprJg$tup+JH6I#s2My#~2Dbpk(>7(T@IUx9=?2>!G$Qcr!AsvcIk79OSfvBPQ z*lA`mJD#{niYvCWnS8ZKNXWL^YEZ(l``$jvhP1Vh2xts~1gYx$5=^fB2f!H?)f zyn{%Po?l;^OB6i;`L@qL&b*WV+0a-6J{bH=|6KZIE?3!?hk3&}N@Y4c!-h5nJh@|B z!9&^u&$^Otgc8aVsAFTok^M`lanDaN3Sc zLQ-<0;rqqfuj4)H1ZLp|@eW_M(+lL#qu^QDy?CQE%$9t)d})uDmAvV1#o#_RGe*eiI8*s=cijRNvbDwMRxrxv7W}Cf8`uyCixo z8?0Ciezl=jM@(xw%3(aXoQ5OZDKVNM>hSmPlICWq;u`_5213lCMoQ^yEjz_2ML5A@ zYwK4=CMIsANjlyz*Ai!igud|2FGJ7y5+Dub%aY!MBv?u|r3{T1@PONxUUQ?+d0}=> zch|J;XX7L(WrURNibFwawKiS+ZEbDSb8|NZt`VR7e~sKg>FZ~QU!DvfZ+&L@{!r~7 zN^H-+(`aaDXjC9Eq480*)Xow}Yiw*xh41ll?$2_iVMiC4pL@MjQ^Kv(Cf(UXj(>|p z+4fu5*3R-L*J%14s{^DZ@0=&ITxZILKRh9gO>LO6>IkU84i1iv<%;|N1^JgF$UmVA zJm{UZ@qFHN9^IE9hZ$NKPIm-(8E(tI5L4&w6dW+all{h%zInIKx`XG@T=i5a$sO5M zuXrI^#!e&`Dmv+{pn+8$_iw+>AvVTco5dU0;5YGFwrbapRJr$i<$qVS<32I(e0y&T zaxxAOk*R=wl{`l!PEJ+TU@+FYdV0=vyB5L%!f<$YHtl|uLeJw%)oN6-g{(>D=5aV& zC0i>UFG#;YWOFE^+h02U%)38ET?T&Cu^cGYKiDZ<){_s>=xT` z1Kv{js3cZCU^~@!ZNfDn$`|BWy)Qz3ub@!6x;wy^`FGBa14)cNo3nh1_we!YaRy%B zy1V*%9`s_G;XmRCUlPF!A!B{+F{OIiuo6IaYmEC(B(w(zBO9<+V z-|hfmX1(Ih6JLfemYy=ab%AE(C;^olls!{7?cRKzTI$gPK734su2l-E!Mj@7JnwcasXt-baMT zD^$z+-+D+SaBi4!179j-4YTADL@OpPethWSO`A$R#31lzB=e5HgZ~2+78!5E-KYL# zLL4SJi7un$hM&_zc7hYW!TBh)yheAvMS2j*a+_OPUxj{k!Vjo2;(ugyBym z<53sO=e&Y9H)V#%I!|Vn-fT~$#Tzjol!L#0X4Blog3w_>xIw$O@5x5%p!F`E(0yN8 z5^2ET+Y9_g5Z#Y%Ev-kmQ95e&wTT;?jejxo~Sz zjodz^2jkaV#9&QI^>gsWXLd}Q*i;bj*}9oTwJ0lbk43Y4ZZlfSN(vsO{R$X*`UuSQ zaU9b(>v2jhN7CsDE?&PYD7AHS!R&K0mO<~%-eO1jVob@5uz=Y4(F~ z-<8FcM2H7(cPk~H*0N7TdYB=Zx8mH{F=~rSE4bYBx>>adNYFz5#@xZWv6OMlPzp31Tty~wI+Wg^E-2U4)2e> zVxCfwy@YJv?7B>=&0b6L|8+XLd-A=;yVt_DN2B^L+0dbK-=4y_Ts9~#g5i`7S)08rqU?&_WjqbUr_~iv`(Fbp83uK9$Sw_FwqW`8uaB0A7viLuJjy}Ypmm-{ZgAR@9NU)y8#TI-=+1)U2aB+IHuv6So6( zbf7xm`Dy9LrOc-VcK?)bP932k<)k)umWckGQb(D#8cUKr^= z3*9GaVY2&sUzfswSAnvlgZ4#)Y1hq8>U9NZP|C9LuSUxUstq`0$^nIV-=BW96Waf} z(EyY_9YivBG6l$6W%~Z5Wfjq=?fR-%C+UUv;RSfasY+VT(nEd)`JUq@5#KUTdbh&H zb1w;if8ewVaE(?~HnC=ob9oxFz;O1{TOeM3e(Tle0}DnXPQZDeAaMUQNA+)qt_C@r zq;|%Oq*AN9@AcR)kw#d7ap)9ttc%$H^P(qSiUh{4T`y10YD4F{6E_Oa{IqNPymQ` z1@QjsV*%=aMH3XT^?x-C4f+3XL_`1QjX}fyXUqL-W}D6UuQy_EO>!m?{Q2D!9U8(lZ|p(O-Vew=iD#yXnR+~J;VZ7)z+9z4Jq5s~x8_(}VNB%^ z7-2|}VLe+^06c3F;MD5MxgiD8uRRH0|2;c+gVfL%ji+RL86I|dqPrEfG~iz=%eub` z@g|Mv3ldj(AbSha!xa|i6&dw;A+~>iWtbK-!c9!Rv(7>pbPTlOzkksA16Dyy4Dh%y z?VlfB_Qn$WT%LFYUtRdC^zoWc)*kGCR}DT|Put}{b>EMYw=i$6Hh-s{0+*#(PK$`R zq!+ds0@vR0bazp1t!yw~+RI^W;#GrPFI3ihIqLrPZ?FH1gm72)j~bgo^z~9#C|#Yo zZUjt>)cyxH(V*Ggd^kt+jl)ENd6iKm$KmRD`n|Bkcfp5F7%_L(rOvh%SE7+7!*ab! zL2m7=S#&>oJHvl#&R8+vWb@OV+s$WkL}yog35wB^wsMHD9h{^It^eHz{k$5>n&Wwr zhVjnBFV&bN-MEf7KczRi&Qy8!L_uzy@`rzI_HeV@nyX1aT#wiYy*P4hDz$3!5u(yi zSFftI?|(cxXN}NF|8+zgnr=q31DQr{SUTfXqX5h8E^IWmlh1P(*875l^ z4?fwR-`vjZ76cJOo1aWqTN=lXAW7Xjia86yX{ap#m=r%UeRdR0WkJa7_xMir@J zujEa?mT70}ZJ;^=c8==a-W0OBeg6Ab`fR14_EfQ&BRGk&x6S2yPsy1b#&Sg46Q;}c zg^k~uWLkFwwBdrGqRWhuZW$}%`KBJRH=A5%)P-z1*EiQT29p@YtxhI##WjAf40C=V zYqDr^GcFK9S_km2o0RF~mS|@SH3aV2pfdS0GlXqz0{hE!^EV}-szE`mD4E#bQVB0KN0Qg|7X_PC0o2xcAoT=dQs&F` z*}CFg>2KU9-C0mh@<@8<4$C?E)oD~|kXP-dLCFRb1>lq3q#NjO=F!copy)DJKrVrM zpt9R%?VFkXoOySLPpo-6LGb#-yvWPR|X-*LGoy8X5y+102KgisWSxHzixe1kNV zN1rE)&!3P-jRzV;>j;K^L@U>#w9`1AEdSAaB{z&~dStCYi9^I8oy(~dLykUngRArj zC5o;vEKk9ufPdZ&j-ZE4K0xOyrr)H>2z8t<nS(yS87AYpu-J2)QHHzhK^M zVi@J(+!tE!`+rQlmk|}qps}peGjifVDEfJ+JN#`XG0f%?9Cy;2)4gwX_hV5Ua$*=b zSGc!2=NDic&;+3`K+R|!W{*&Co#s+pi`68JN_0Tp;S``7KwCX!pqk8+)SetCES11P z=wvohSmciuxB{Ni+^3Ez6tHO2%n~p!K8lMFv}hD*d3Nf7Pzc&zmifH=XIt+hX$&!S zUZ}F{w$IuAgKQ51tOBKneBiFT7n9igSg3s!WhX11g3Le!;MN>kMI-b1YMBXfYBG4T zs#{D-iCP)XlYFa3kVpb861<0wQdg9D00I==#7Nxz6jic>`RQb z!v)PEHOzjqHsDq;$8>{#`cYeK&_e6u5H|Oi*gP2U0n-&d92GQ@!FO^iGsa=E(E3~u zWe&1AT0Ka$0$ypOo@OR6+1$K?-qhhG|bmD5H8 zk7upZs^{cL^Vtm?Uk@FctzvvaONkWQ^d{_MV9G~OGw<^ZOzeFSnx-6M4KkfwSc3@+2i21Bq>-F#$LqK%C z0WT}Q_tr$PRLDq8$2EDIB}H>}GK(C&TspM>z3+ITThXu$P5!f9BsoWr$upTjP8pSq zF%wa%5rc=n76N0?OhCrfN5&}SS0J*L$rVOd5cjyk7G)p*h&z!FIjY$F$R_8f=}&+~ z1`lxED!o83YdNfbl4`68Io_-)kI?gfg5fu+K+A>o!r3!c*C!9TO_d^U49lP#rffMv zYnoKKQks1}>oKe4^|uR7sp64iksKy+_dfx%*?ZU+2Fg_I(au-XN+@-*2i-nEX2^|Z zVRu<*#jH#g+46cVw)?Bl5Uf{Nv3t8tfMKF?Eo?sz4%8#YkZ;%&LmHbG-#Js33sJpe zp%wj}yT5EwW0A;LH$@@W$DkTd@yiH*1aQ`vgkmGAV9pZ-qPh}Vh9_^3LtpiuEz8Q&sMJ3{;0LoJ9ApoF9tu64c}P05x$Y(l%$F$R18n;1&kf%4L}`o?uhZ!B)uO>x0D2 zJcgWprJg$tup+JH6I#s2My#~2Dbpk(>7(T@IUx9=?2>!G$Qcr!AsvcIk79OSfvBPQ z*lA`mJD#{niYvCWnS8ZKNXWL^YEZ(l``$jvhP1Vh2xts~1gYx$5=^fB2f!H?)f zyn{%Po?l;^OB6i;`L@qL&b*WV+0a-6J{bH=|6KZIE?3!?hk3&}N@Y4c!-h5nJh@|B z!9&^u&$^Otgc8aVsAFTok^M`lanDaN3Sc zLQ-<0;rqqfuj4)H1ZLp|@eW_M(+lL#qu^QDy?CQE%$9t)d})uDmAvV1#o#_RGe*eiI8*s=cijRNvbDwMRxrxv7W}Cf8`uyCixo z8?0Ciezl=jM@(xw%3(aXoQ5OZDKVNM>hSmPlICWq;u`_5213lCMoQ^yEjz_2ML5A@ zYwK4=CMIsANjlyz*Ai!igud|2FGJ7y5+Dub%aY!MBv?u|r3{T1@PONxUUQ?+d0}=> zch|J;XX7L(WrURNibFwawKiS+ZEbDSb8|NZt`VR7e~sKg>FZ~QU!DvfZ+&L@{!r~7 zN^H-+(`aaDXjC9Eq480*)Xow}Yiw*xh41ll?$2_iVMiC4pL@MjQ^Kv(Cf(UXj(>|p z+4fu5*3R-L*J%14s{^DZ@0=&ITxZILKRh9gO>LO6>IkU84i1iv<%;|N1^JgF$UmVA zJm{UZ@qFHN9^IE9hZ$NKPIm-(8E(tI5L4&w6dW+all{h%zInIKx`XG@T=i5a$sO5M zuXrI^#!e&`Dmv+{pn+8$_iw+>AvVTco5dU0;5YGFwrbapRJr$i<$qVS<32I(e0y&T zaxxAOk*R=wl{`l!PEJ+TU@+FYdV0=vyB5L%!f<$YHtl|uLeJw%)oN6-g{(>D=5aV& zC0i>UFG#;YWOFE^+h02U%)38ET?T&Cu^cGYKiDZ<){_s>=xT` z1Kv{js3cZCU^~@!ZNfDn$`|BWy)Qz3ub@!6x;wy^`FGBa14)cNo3nh1_we!YaRy%B zy1V*%9`s_G;XmRCUlPF!A!B{+F{OIiuo6IaYmEC(B(w(zBO9<+V z-|hfmX1(Ih6JLfemYy=ab%AE(C;^olls!{7?cRKzTI$gPK734su2l-E!Mj@7JnwcasXt-baMT zD^$z+-+D+SaBi4!179j-4YTADL@OpPethWSO`A$R#31lzB=e5HgZ~2+78!5E-KYL# zLL4SJi7un$hM&_zc7hYW!TBh)yheAvMS2j*a+_OPUxj{k!Vjo2;(ugyBym z<53sO=e&Y9H)V#%I!|Vn-fT~$#Tzjol!L#0X4Blog3w_>xIw$O@5x5%p!F`E(0yN8 z5^2ET+Y9_g5Z#Y%Ev-kmQ95e&wTT;?jejxo~Sz zjodz^2jkaV#9&QI^>gsWXLd}Q*i;bj*}9oTwJ0lbk43Y4ZZlfSN(vsO{R$X*`UuSQ zaU9b(>v2jhN7CsDE?&PYD7AHS!R&K0mO<~%-eO1jVob@5uz=Y4(F~ z-<8FcM2H7(cPk~H*0N7TdYB=Zx8mH{F=~rSE4bYBx>>adNYFz5#@xZWv6OMlPzp31Tty~wI+Wg^E-2U4)2e> zVxCfwy@YJv?7B>=&0b6L|8+XLd-A=;yVt_DN2B^L+0dbK-=4y_Ts9~#g5i`7S)08rqU?&_WjqbUr_~iv`(Fbp83uK9$Sw_FwqW`8uaB0A7viLuJjy}Ypmm-{ZgAR@9NU)y8#TI-=+1)U2aB+IHuv6So6( zbf7xm`Dy9LrOc-VcK?)bP932k<)k)umWckGQb(D#8cUKr^= z3*9GaVY2&sUzfswSAnvlgZ4#)Y1hq8>U9NZP|C9LuSUxUstq`0$^nIV-=BW96Waf} z(EyY_9YivBG6l$6W%~Z5Wfjq=?fR-%C+UUv;RSfasY+VT(nEd)`JUq@5#KUTdbh&H zb1w;if8ewVaE(?~HnC=ob9oxFz;O1{TOeM3e(Tle0}DnXPQZDeAaMUQNA+)qt_C@r zq;|%Oq*AN9@AcR)kw#d7ap)9ttc%$H^P(qSiUh{4T` +``` + +## 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/sidebars-ios.json b/docusaurus/sidebars-ios.json index a70ae020b0..2bc94cd85f 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", { From e2cd1632421808d909ca095f01dfa319263d26f6 Mon Sep 17 00:00:00 2001 From: Stream SDK Bot <60655709+Stream-SDK-Bot@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:00:54 +0100 Subject: [PATCH 09/17] [CI] Sync Mock Server (#3402) --- .../Fixtures/JSONs/http_add_member.json | 62 ++++++++----- .../Fixtures/JSONs/http_attachment.json | 4 +- .../Fixtures/JSONs/http_channel_creation.json | 92 ++++++++++--------- .../Fixtures/JSONs/http_channel_removal.json | 52 ++++++----- .../Fixtures/JSONs/http_channels.json | 92 ++++++++++--------- .../Fixtures/JSONs/http_events.json | 14 +-- .../Fixtures/JSONs/http_message.json | 24 ++--- .../JSONs/http_message_ephemeral.json | 76 +++++++-------- .../Fixtures/JSONs/http_reaction.json | 88 +++++++----------- .../Fixtures/JSONs/http_truncate.json | 84 ++++++++++------- .../Fixtures/JSONs/http_unsplash_link.json | 30 +++--- .../Fixtures/JSONs/http_youtube_link.json | 26 ++---- .../Fixtures/JSONs/ws_events.json | 12 ++- .../Fixtures/JSONs/ws_events_channel.json | 60 ++++++------ .../Fixtures/JSONs/ws_events_member.json | 22 ++--- .../Fixtures/JSONs/ws_health_check.json | 12 ++- .../Fixtures/JSONs/ws_message.json | 26 +++--- .../Fixtures/JSONs/ws_reaction.json | 54 ++++++----- 18 files changed, 428 insertions(+), 402 deletions(-) diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json index 25036c3044..e80dc5ffda 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 00303c2dc1..126cd3936d 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 5c62f58a40..feadbe44d2 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 663ab6ea33..1eb3d3e344 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 40a858b96e..d8e4157000 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 19486a8104..eed2437f6c 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 63fb2b5480..a86c2c36ef 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 e9cb7edee0..9ba9926723 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 054f83e624..41a628245a 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 b9fc2abed4..7e2dc2a707 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 dcb69c2f83..da8519ae77 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 8b01563c91..55b63e1401 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 1c01f0c1ee..e1cf583002 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 7aa9b5ee63..0fd8f17029 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 832e0e0d26..414f75abd1 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 d4684a966c..733e2d0eb3 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 f4b54a14bf..9b54479d4d 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 819c1c9283..f0242c2111 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 From d11abaf154bbc977758478f4f6c2e131f043b5dd Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 3 Sep 2024 16:04:24 +0100 Subject: [PATCH 10/17] [CI] Clean up some disk space on CI before downloading iOS runtimes (#3405) --- .github/actions/setup-ios-runtime/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index aeec23e132..89164b238a 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -6,8 +6,10 @@ runs: - 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 From 6762aa39648238381557b4e63137eb0dbf4fcb31 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 4 Sep 2024 11:52:16 +0300 Subject: [PATCH 11/17] Log cURL representation of the URLRequest to the debug log (#3407) --- Sources/StreamChat/APIClient/APIClient.swift | 9 +---- .../Extensions/URLRequest+cURL.swift | 38 +++++++++++++++++++ StreamChat.xcodeproj/project.pbxproj | 8 +++- 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 Sources/StreamChat/Extensions/URLRequest+cURL.swift diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift index 27ec8606fc..2e54f808f3 100644 --- a/Sources/StreamChat/APIClient/APIClient.swift +++ b/Sources/StreamChat/APIClient/APIClient.swift @@ -231,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 { diff --git a/Sources/StreamChat/Extensions/URLRequest+cURL.swift b/Sources/StreamChat/Extensions/URLRequest+cURL.swift new file mode 100644 index 0000000000..f6f23eaea4 --- /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/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index fe26cb9982..6915fd4433 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 */; }; @@ -3084,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 = ""; }; @@ -7456,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 = ""; @@ -11173,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 */, @@ -12259,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 */, From 3b777a9f9c42e512d4902753f59c9710a7094150 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 4 Sep 2024 13:36:41 +0300 Subject: [PATCH 12/17] Add `unset` argument to `CurrentChatUserController.updateUserData` and `ConnectedUser.update` for clearing user data fields (#3404) --- CHANGELOG.md | 1 + .../DemoChatChannelListRouter.swift | 14 ++++++++ .../APIClient/Endpoints/UserEndpoints.swift | 6 ++-- .../CurrentUserController.swift | 4 ++- .../StreamChat/StateLayer/ConnectedUser.swift | 9 +++-- .../Workers/CurrentUserUpdater.swift | 12 ++++--- .../Workers/CurrentUserUpdater_Mock.swift | 5 +++ .../Endpoints/UserEndpoints_Tests.swift | 6 ++-- .../Workers/CurrentUserUpdater_Tests.swift | 36 ++++++++++++++++++- 9 files changed, 80 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99969a8044..36d16dc83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 [#3403](https://github.com/GetStream/stream-chat-swift/pull/3403) ### 🐞 Fixed - Fix Logger printing the incorrect thread name [#3382](https://github.com/GetStream/stream-chat-swift/pull/3382) diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift index 291e78c25a..44ea4864fe 100644 --- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift +++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift @@ -532,6 +532,20 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { } 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/Sources/StreamChat/APIClient/Endpoints/UserEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/UserEndpoints.swift index a751d542cb..040979ce5a 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/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift index cf01b2215f..218bb20d24 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 { diff --git a/Sources/StreamChat/StateLayer/ConnectedUser.swift b/Sources/StreamChat/StateLayer/ConnectedUser.swift index 6c01e60a24..7e77f537aa 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 ) } diff --git a/Sources/StreamChat/Workers/CurrentUserUpdater.swift b/Sources/StreamChat/Workers/CurrentUserUpdater.swift index d3eb9a6974..ec5d37f1c1 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 @@ -286,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( @@ -295,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/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/CurrentUserUpdater_Mock.swift index 14b735f34c..53b271d558 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? @@ -40,6 +41,7 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { privacySettings: UserPrivacySettings?, role: UserRole?, userExtraData: [String: RawJSON]?, + unset: Set, completion: ((Error?) -> Void)? = nil ) { updateUserData_currentUserId = currentUserId @@ -47,6 +49,7 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { updateUserData_imageURL = imageURL updateUserData_userExtraData = userExtraData updateUserData_privacySettings = privacySettings + updateUserData_unset = unset updateUserData_completion = completion } @@ -89,7 +92,9 @@ final class CurrentUserUpdater_Mock: CurrentUserUpdater { updateUserData_currentUserId = nil updateUserData_name = nil updateUserData_imageURL = nil + updateUserData_privacySettings = nil updateUserData_userExtraData = nil + updateUserData_unset = nil updateUserData_completion = nil addDevice_id = nil diff --git a/Tests/StreamChatTests/APIClient/Endpoints/UserEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/UserEndpoints_Tests.swift index 60b25531da..f970bdc68e 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/Workers/CurrentUserUpdater_Tests.swift b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift index c042d982f9..0569637230 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)) } From 34caca9057fb8b53a3fbb9692db52d45fcdad6af Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Wed, 4 Sep 2024 12:30:20 +0100 Subject: [PATCH 13/17] [CI] Update git fetch-depth on release (#3410) --- .github/workflows/release-publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 8139338ef1..e7dfbe83d9 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -18,6 +18,8 @@ jobs: ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 - uses: ./.github/actions/ruby-cache From 75d3046efde869a0a456fff086cadc4aa891f418 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 4 Sep 2024 16:06:09 +0300 Subject: [PATCH 14/17] Local database integrity (#3399) * Discard offline state changes when saving database changes fails * Add an error log when ChatClient misuse is detected --- CHANGELOG.md | 2 ++ Sources/StreamChat/ChatClient.swift | 20 +++++++++++++++++++ .../Database/DatabaseContainer.swift | 13 ++---------- Tests/StreamChatTests/ChatClient_Tests.swift | 11 ++++++++++ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d16dc83f..cdd2980a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add `unset` argument to `CurrentChatUserController.updateUserData` and `ConnectedUser.update` for clearing user data fields [#3403](https://github.com/GetStream/stream-chat-swift/pull/3403) ### 🐞 Fixed - Fix Logger printing the incorrect thread name [#3382](https://github.com/GetStream/stream-chat-swift/pull/3382) +### 🔄 Changed +- Discard offline state changes when saving database changes fails [#3399](https://github.com/GetStream/stream-chat-swift/pull/3399) ## StreamChatUI ### ✅ Added diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift index 626b465ef3..d5a303f1a6 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/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift index ec2edebfb2..d93cfadc1e 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 @@ -349,7 +341,6 @@ class DatabaseContainer: NSPersistentContainer { } } } - self?.canWriteData = true completion?(lastEncounteredError) } } diff --git a/Tests/StreamChatTests/ChatClient_Tests.swift b/Tests/StreamChatTests/ChatClient_Tests.swift index 491bc96b76..f050c16e26 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() From 391f61bc3913ed989e5c10aef2c492ebcfd4a7b0 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 4 Sep 2024 21:39:23 +0300 Subject: [PATCH 15/17] Channel watching did not resume on web-socket reconnection (#3409) --- CHANGELOG.md | 1 + .../Repositories/SyncOperations.swift | 28 ++++++++++++++++++- .../Repositories/SyncRepository.swift | 6 ++++ Sources/StreamChat/StateLayer/Chat.swift | 3 ++ Sources/StreamChat/StateLayer/ChatState.swift | 6 +++- .../Mocks/StreamChat/State/Chat_Mock.swift | 9 ++++-- .../Repositories/SyncRepository_Tests.swift | 11 +++++++- 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd2980a64..66cfa28208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add `unset` argument to `CurrentChatUserController.updateUserData` and `ConnectedUser.update` for clearing user data fields [#3403](https://github.com/GetStream/stream-chat-swift/pull/3403) ### 🐞 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 [#3408](https://github.com/GetStream/stream-chat-swift/pull/3408) ### 🔄 Changed - Discard offline state changes when saving database changes fails [#3399](https://github.com/GetStream/stream-chat-swift/pull/3399) diff --git a/Sources/StreamChat/Repositories/SyncOperations.swift b/Sources/StreamChat/Repositories/SyncOperations.swift index 1220b70e96..6a9a11174f 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 35b237d7d5..65563b5d81 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 66eb12b457..93d60b871b 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 } diff --git a/Sources/StreamChat/StateLayer/ChatState.swift b/Sources/StreamChat/StateLayer/ChatState.swift index 7320f80d82..b199a74b9e 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/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift index cc7cbd5c29..c250fb5b77 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/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift index abd202fc46..ea799a6a8e 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) From a03b3375b4fb95343043cadcfeb90c2145af5ffa Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Thu, 5 Sep 2024 09:57:55 +0300 Subject: [PATCH 16/17] Fix PR links in the CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66cfa28208..3dc5e23549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 [#3403](https://github.com/GetStream/stream-chat-swift/pull/3403) +- 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 [#3408](https://github.com/GetStream/stream-chat-swift/pull/3408) +- 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) From e0e58501c4f183c7b5562dae1f2b92b2a45aae56 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 5 Sep 2024 07:09:33 +0000 Subject: [PATCH 17/17] Bump 4.63.0 --- CHANGELOG.md | 5 +++++ README.md | 4 ++-- Sources/StreamChat/Generated/SystemEnvironment+Version.swift | 2 +- Sources/StreamChat/Info.plist | 2 +- Sources/StreamChatUI/Info.plist | 2 +- StreamChat-XCFramework.podspec | 2 +- StreamChat.podspec | 2 +- StreamChatArtifacts.json | 2 +- StreamChatUI-XCFramework.podspec | 2 +- StreamChatUI.podspec | 2 +- 10 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc5e23549..d49bee01f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🔄 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) diff --git a/README.md b/README.md index 481ac8107b..45d332c622 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/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift index 32fd2eebea..d32e783add 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 4f55c7aea3..24ce17378e 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/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist index 4f55c7aea3..24ce17378e 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 626cd32489..6d4646f6b1 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 11158ae6d3..c297940e5e 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/StreamChatArtifacts.json b/StreamChatArtifacts.json index 32af7ff6b4..bc00e4ff59 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 9ebdec6ed6..764ee488be 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 6fa4b8f522..0c329663a4 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."