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 @@
-
-
+
+
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."