diff --git a/.github/actions/python-cache/action.yml b/.github/actions/python-cache/action.yml
deleted file mode 100644
index 21fc1af5f4c..00000000000
--- a/.github/actions/python-cache/action.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-name: 'Python Cache'
-description: 'Cache Python dependencies'
-runs:
- using: "composite"
- steps:
- - uses: actions/setup-python@v4
- with:
- python-version: 3.11
- cache: 'pip'
- - run: pip3 install -r requirements.txt --break-system-packages
- shell: bash
diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml
index 423005d9dda..9ef01bac050 100644
--- a/.github/workflows/cron-checks.yml
+++ b/.github/workflows/cron-checks.yml
@@ -97,16 +97,6 @@ jobs:
- name: Allure TestOps Launch Removal
if: cancelled()
run: bundle exec fastlane allure_launch_removal launch_id:$LAUNCH_ID
- - uses: 8398a7/action-slack@v3
- with:
- status: ${{ job.status }}
- text: "You shall not pass!"
- job_name: "${{ github.workflow }}: ${{ github.job }}"
- fields: message,commit,author,action,workflow,job,took
- env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
- if: failure() && github.event_name == 'schedule'
- name: Parse xcresult
if: failure()
run: |
@@ -165,16 +155,6 @@ jobs:
- name: Run LLC Tests (Debug)
run: bundle exec fastlane test device:"${{ matrix.device }} (${{ matrix.ios }})" cron:true
timeout-minutes: 100
- - uses: 8398a7/action-slack@v3
- with:
- status: ${{ job.status }}
- text: "You shall not pass!"
- job_name: "${{ github.workflow }}: ${{ github.job }}"
- fields: message,commit,author,action,workflow,job,took
- env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
- if: failure() && github.event_name == 'schedule'
- name: Parse xcresult
if: failure()
run: |
@@ -224,3 +204,18 @@ jobs:
- run: bundle exec fastlane rubocop
- run: ./Scripts/run-linter.sh
- run: bundle exec fastlane pod_lint
+
+ slack:
+ name: Slack Report
+ runs-on: ubuntu-latest
+ needs: [build-and-test-debug, test-e2e-debug, build-test-app-and-frameworks, build-old-xcode, automated-code-review]
+ if: failure() && github.event_name == 'schedule'
+ steps:
+ - uses: 8398a7/action-slack@v3
+ with:
+ status: cancelled
+ text: "You shall not pass!"
+ job_name: "${{ github.workflow }}: ${{ github.job }}"
+ fields: repo,commit,author,workflow
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_NIGHTLY_CHECKS }}
diff --git a/.github/workflows/record-snapshots.yml b/.github/workflows/record-snapshots.yml
index 2bbbd7d8790..c05f727ab6b 100644
--- a/.github/workflows/record-snapshots.yml
+++ b/.github/workflows/record-snapshots.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- - run: gh workflow run smoke-checks.yml --ref "${GITHUB_REF#refs/heads/}" -f snapshots=true
+ - run: gh workflow run smoke-checks.yml --ref "${GITHUB_REF#refs/heads/}" -f record_snapshots=true
timeout-minutes: 5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/smoke-checks-llc.yml b/.github/workflows/smoke-checks-llc.yml
deleted file mode 100644
index 9b789e6abf4..00000000000
--- a/.github/workflows/smoke-checks-llc.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Smoke Checks LLC
-
-on:
- push:
- branches:
- - develop
- - main
-
- pull_request:
- paths-ignore:
- - 'README.md'
- - 'CHANGELOG.md'
-
- workflow_dispatch:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-env:
- HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
- IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.1)"
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GITHUB_PR_NUM: ${{ github.event.pull_request.number }}
-
-jobs:
- test-llc-debug:
- name: Test LLC (Debug)
- runs-on: macos-15
- steps:
- - uses: actions/checkout@v4.1.1
- with:
- fetch-depth: 100
- - uses: ./.github/actions/bootstrap
- env:
- INSTALL_YEETD: true
- INSTALL_SONAR: true
- - uses: ./.github/actions/python-cache
- - name: Run LLC Tests (Debug)
- run: bundle exec fastlane test device:"${{ env.IOS_SIMULATOR_DEVICE }}"
- timeout-minutes: 60
- - name: Run Sonar analysis
- run: bundle exec fastlane sonar_upload
- env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- PR_NUMBER: ${{ github.event.number }}
- - uses: 8398a7/action-slack@v3
- with:
- status: ${{ job.status }}
- text: "You shall not pass!"
- job_name: "Test LLC (Debug)"
- fields: message,commit,author,action,workflow,job,took
- env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
- if: ${{ github.event_name == 'push' && failure() }}
- - name: Parse xcresult
- if: failure()
- run: |
- brew install chargepoint/xcparse/xcparse
- xcparse logs fastlane/test_output/StreamChat.xcresult fastlane/test_output/logs/
- - uses: actions/upload-artifact@v4
- if: failure()
- with:
- name: Test Data LLC
- path: |
- fastlane/test_output/logs/*/Diagnostics/**/*.txt
- fastlane/test_output/logs/*/Diagnostics/simctl_diagnostics/DiagnosticReports/*
diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index d0fa2e6de2b..e5a73918c49 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -8,8 +8,8 @@ on:
workflow_dispatch:
inputs:
- snapshots:
- description: 'Should Snapshots be recorded on CI?'
+ record_snapshots:
+ description: 'Record snapshots on CI?'
type: boolean
required: false
default: false
@@ -28,7 +28,6 @@ jobs:
build-test-app-and-frameworks:
name: Build Test App and Frameworks
runs-on: macos-15
- if: ${{ github.event_name != 'push' }}
steps:
- uses: actions/checkout@v4.1.1
- uses: ./.github/actions/ruby-cache
@@ -50,7 +49,7 @@ jobs:
runs-on: macos-15
env:
XCODE_VERSION: "15.4"
- if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }}
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
steps:
- uses: actions/checkout@v4.1.1
with:
@@ -69,7 +68,7 @@ jobs:
build-old-xcode:
name: Build LLC + UI (Xcode 15)
runs-on: macos-15
- if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }}
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
env:
XCODE_VERSION: "15.4"
steps:
@@ -84,11 +83,49 @@ jobs:
run: bundle exec fastlane test_ui device:"iPhone 13" build_for_testing:true
timeout-minutes: 25
+ test-llc-debug:
+ name: Test LLC (Debug)
+ runs-on: macos-15
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
+ steps:
+ - uses: actions/checkout@v4.1.1
+ with:
+ fetch-depth: 100
+ - uses: ./.github/actions/bootstrap
+ env:
+ INSTALL_YEETD: true
+ INSTALL_SONAR: true
+ - name: Run LLC Tests (Debug)
+ run: bundle exec fastlane test device:"${{ env.IOS_SIMULATOR_DEVICE }}"
+ timeout-minutes: 60
+ - name: Run Sonar analysis
+ run: bundle exec fastlane sonar_upload
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ PR_NUMBER: ${{ github.event.number }}
+ - name: Parse xcresult
+ if: failure()
+ run: |
+ brew install chargepoint/xcparse/xcparse
+ xcparse logs fastlane/test_output/StreamChat.xcresult fastlane/test_output/logs/
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: Test Data LLC
+ path: |
+ fastlane/test_output/logs/*/Diagnostics/**/*.txt
+ fastlane/test_output/logs/*/Diagnostics/simctl_diagnostics/DiagnosticReports/*
+ - name: Upload Test Coverage
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-coverage-${{ github.event.pull_request.number }}
+ path: reports/sonarqube-generic-coverage.xml
+
test-ui-debug:
name: Test UI (Debug)
runs-on: macos-15
needs: build-test-app-and-frameworks
- if: ${{ github.event_name != 'push' }}
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/download-artifact@v4
@@ -100,7 +137,7 @@ jobs:
INSTALL_YEETD: true
SKIP_MINT_BOOTSTRAP: true
- name: Run UI Tests (Debug)
- run: bundle exec fastlane test_ui device:"${{ env.IOS_SIMULATOR_DEVICE }}" skip_build:true record:${{ github.event.inputs.snapshots }}
+ run: bundle exec fastlane test_ui device:"${{ env.IOS_SIMULATOR_DEVICE }}" skip_build:true record:"${{ github.event.inputs.record_snapshots }}"
timeout-minutes: 120
env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} # to open a PR
@@ -120,7 +157,7 @@ jobs:
allure_testops_launch:
name: Launch Allure TestOps
runs-on: macos-14
- if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }}
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
needs: build-test-app-and-frameworks
outputs:
launch_id: ${{ steps.get_launch_id.outputs.launch_id }}
@@ -139,7 +176,7 @@ jobs:
test-e2e-debug:
name: Test E2E UI (Debug)
runs-on: macos-15
- if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }}
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
needs:
- allure_testops_launch
- build-test-app-and-frameworks
@@ -195,7 +232,7 @@ jobs:
name: Build Demo App + Example Apps
runs-on: macos-14
needs: build-test-app-and-frameworks
- if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }}
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/download-artifact@v4
@@ -224,7 +261,7 @@ jobs:
name: Test Integration
runs-on: macos-14
needs: build-test-app-and-frameworks
- if: ${{ github.event_name != 'push' && github.event.inputs.snapshots != 'true' }}
+ if: ${{ github.event.inputs.record_snapshots != 'true' }}
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/download-artifact@v4
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
new file mode 100644
index 00000000000..9bd2c2eee38
--- /dev/null
+++ b/.github/workflows/sonar.yml
@@ -0,0 +1,50 @@
+name: Sonar
+
+on:
+ push:
+ branches:
+ - develop
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+ sonar:
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v4.1.1
+
+ - uses: actions/github-script@v6
+ id: get_pr_data
+ with:
+ script: |
+ return (
+ await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ commit_sha: context.sha,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ })
+ ).data[0];
+
+ - uses: ./.github/actions/bootstrap
+ env:
+ INSTALL_SONAR: true
+ SKIP_MINT_BOOTSTRAP: true
+
+ - name: Run Sonar analysis
+ run: |
+ ARTIFACT_NAME="test-coverage-${{ fromJson(steps.get_pr_data.outputs.result).number }}"
+ ARTIFACT=$(gh api repos/${{ github.repository }}/actions/artifacts | jq -r ".artifacts | map(select(.name==\"$ARTIFACT_NAME\")) | first")
+ if [[ "$ARTIFACT" == null || "$ARTIFACT" == "" ]]; then
+ echo "Artifact not found. Skipping Sonar analysis."
+ else
+ gh run download $(echo $ARTIFACT | jq .workflow_run.id) -n "$ARTIFACT_NAME" -D reports
+ bundle exec fastlane sonar_upload
+ fi
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.github/workflows/sync-mock-server.yml b/.github/workflows/sync-mock-server.yml
index 0db87e3488b..7f61287de6f 100644
--- a/.github/workflows/sync-mock-server.yml
+++ b/.github/workflows/sync-mock-server.yml
@@ -27,8 +27,7 @@ jobs:
status: ${{ job.status }}
text: "You shall not pass!"
job_name: "${{ github.workflow }}: ${{ github.job }}"
- fields: message,commit,author,action,workflow,job,took
+ fields: repo,commit,author,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
if: failure()
diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml
index 9d4444aad82..7444975d897 100644
--- a/.github/workflows/testflight.yml
+++ b/.github/workflows/testflight.yml
@@ -35,8 +35,7 @@ jobs:
with:
status: ${{ job.status }}
text: "You shall not pass!"
- fields: message,commit,author,action,workflow,job,took
+ fields: repo,commit,author,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
if: failure()
diff --git a/.github/workflows/update-copyright.yml b/.github/workflows/update-copyright.yml
index 1649699d350..fd3bf72645a 100644
--- a/.github/workflows/update-copyright.yml
+++ b/.github/workflows/update-copyright.yml
@@ -26,8 +26,7 @@ jobs:
status: ${{ job.status }}
text: "You shall not pass!"
job_name: "${{ github.workflow }}: ${{ github.job }}"
- fields: message,commit,author,action,workflow,job,took
+ fields: repo,commit,author,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
if: failure()
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 224ce362fef..7385687d1d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🔄 Changed
+# [4.73.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.73.0)
+_February 27, 2025_
+
+## StreamChat
+### ✅ Added
+- Add sending messages to only specific members [#3595](https://github.com/GetStream/stream-chat-swift/pull/3595)
+- Add Draft Messages Support [#3588](https://github.com/GetStream/stream-chat-swift/pull/3588)
+ - Add `ChatChannel.draftMessage`
+ - Add `ChatMessage.draftReply`
+ - Add `ChannelController`:
+ - `updateDraftMessage()`
+ - `deleteDraftMessage()`
+ - `loadDraftMessage()`
+ - Add `MessageController`:
+ - `updateDraftReply()`
+ - `deleteDraftReply()`
+ - `loadDraftReply()`
+ - Add `CurrentUserController`:
+ - `deleteDraft()`
+ - `loadDraftMessages()`
+ - `loadMoreDraftMessages()`
+
+### 🐞 Fixed
+- Update channel's preview message when coming back to online [#3574](https://github.com/GetStream/stream-chat-swift/pull/3574)
+- Fix message transformer not being applied when editing a message [#3602](https://github.com/GetStream/stream-chat-swift/pull/3602)
+
+## StreamChatUI
+### ✅ Added
+- Add `Components.isDraftMessagesEnabled` to enable Draft Messages [#3588](https://github.com/GetStream/stream-chat-swift/pull/3588)
+- Add draft preview in Channel List and Thread List if drafts are enabled [#3588](https://github.com/GetStream/stream-chat-swift/pull/3588)
+
# [4.72.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.72.0)
_February 04, 2025_
### 🔄 Changed
- Revert 'Improve performance of model conversions with large extra data' [#3576](https://github.com/GetStream/stream-chat-swift/pull/3576)
+- Expand `StreamAudioPlayer` to allow passing an `options` field to `AVURLAsset` initialiser [#3586](https://github.com/GetStream/stream-chat-swift/pull/3586)
# [4.71.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.71.0)
_January 28, 2025_
diff --git a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift
index fde449f275b..5bb0d19a69f 100644
--- a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift
+++ b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift
@@ -194,6 +194,7 @@ class AppConfigViewController: UITableViewController {
}
enum ChatClientConfigOption: String, CaseIterable {
+ case baseURL
case isLocalStorageEnabled
case staysConnectedInBackground
case reconnectionTimeout
@@ -351,6 +352,9 @@ class AppConfigViewController: UITableViewController {
cell.textLabel?.text = option.rawValue
switch option {
+ case .baseURL:
+ cell.detailTextLabel?.text = chatClientConfig.baseURL.description
+ cell.accessoryType = .disclosureIndicator
case .isLocalStorageEnabled:
cell.accessoryView = makeSwitchButton(chatClientConfig.isLocalStorageEnabled) { [weak self] newValue in
self?.chatClientConfig.isLocalStorageEnabled = newValue
@@ -384,6 +388,8 @@ class AppConfigViewController: UITableViewController {
) {
let option = options[indexPath.row]
switch option {
+ case .baseURL:
+ showBaseURLInputAlert()
case .deletedMessagesVisibility:
pushDeletedMessagesVisibilitySelectorVC()
case .reconnectionTimeout:
@@ -634,6 +640,36 @@ class AppConfigViewController: UITableViewController {
navigationController?.pushViewController(selectorViewController, animated: true)
}
+ private func showBaseURLInputAlert() {
+ let alert = UIAlertController(
+ title: "Base URL",
+ message: "Input the base URL for the Chat Client.",
+ preferredStyle: .alert
+ )
+
+ alert.addTextField { textField in
+ textField.placeholder = "Base URL"
+ textField.autocapitalizationType = .none
+ textField.autocorrectionType = .no
+ textField.text = self.chatClientConfig.baseURL.description
+ textField.textContentType = .URL
+ }
+
+ alert.addAction(.init(title: "Set", style: .default, handler: { _ in
+ guard let urlString = alert.textFields?.first?.text,
+ let url = URL(string: urlString)
+ else {
+ return
+ }
+ self.chatClientConfig.baseURL = .init(url: url)
+ self.tableView.reloadData()
+ }))
+
+ alert.addAction(.init(title: "Cancel", style: .destructive, handler: nil))
+
+ present(alert, animated: true, completion: nil)
+ }
+
private func showTokenDetailsAlert() {
let alert = UIAlertController(
title: "Token Refreshing",
diff --git a/DemoApp/Screens/DemoAppTabBarController.swift b/DemoApp/Screens/DemoAppTabBarController.swift
index cbe714c7989..082f36de7f4 100644
--- a/DemoApp/Screens/DemoAppTabBarController.swift
+++ b/DemoApp/Screens/DemoAppTabBarController.swift
@@ -9,15 +9,18 @@ import UIKit
class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDelegate {
let channelListVC: UIViewController
let threadListVC: UIViewController
+ let draftListVC: UIViewController
let currentUserController: CurrentChatUserController
init(
channelListVC: UIViewController,
threadListVC: UIViewController,
+ draftListVC: UIViewController,
currentUserController: CurrentChatUserController
) {
self.channelListVC = channelListVC
self.threadListVC = threadListVC
+ self.draftListVC = draftListVC
self.currentUserController = currentUserController
super.init(nibName: nil, bundle: nil)
}
@@ -60,7 +63,10 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
threadListVC.tabBarItem.image = UIImage(systemName: "text.bubble")
threadListVC.tabBarItem.badgeColor = .red
- viewControllers = [channelListVC, threadListVC]
+ draftListVC.tabBarItem.title = "Drafts"
+ draftListVC.tabBarItem.image = UIImage(systemName: "bubble.and.pencil")
+
+ viewControllers = [channelListVC, threadListVC, draftListVC]
}
func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUserUnreadCount: UnreadCount) {
diff --git a/DemoApp/Screens/DemoDraftMessageListVC.swift b/DemoApp/Screens/DemoDraftMessageListVC.swift
new file mode 100644
index 00000000000..2765be5d8e5
--- /dev/null
+++ b/DemoApp/Screens/DemoDraftMessageListVC.swift
@@ -0,0 +1,274 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import StreamChat
+import StreamChatUI
+import UIKit
+
+class DemoDraftMessageListVC: UIViewController, ThemeProvider {
+ var onLogout: (() -> Void)?
+ var onDisconnect: (() -> Void)?
+
+ private let currentUserController: CurrentChatUserController
+ private var drafts: [DraftMessage] = []
+ private var isPaginatingDrafts = false
+
+ lazy var userAvatarView: CurrentChatUserAvatarView = components
+ .currentUserAvatarView.init()
+
+ private lazy var tableView: UITableView = {
+ let tableView = UITableView()
+ tableView.translatesAutoresizingMaskIntoConstraints = false
+ tableView.delegate = self
+ tableView.dataSource = self
+ tableView.register(DemoDraftMessageCell.self, forCellReuseIdentifier: "DemoDraftMessageCell")
+ return tableView
+ }()
+
+ private lazy var loadingIndicator: UIActivityIndicatorView = {
+ let indicator = UIActivityIndicatorView(style: .large)
+ indicator.translatesAutoresizingMaskIntoConstraints = false
+ return indicator
+ }()
+
+ private lazy var emptyStateLabel: UILabel = {
+ let label = UILabel()
+ label.translatesAutoresizingMaskIntoConstraints = false
+ label.text = "No draft messages"
+ label.textAlignment = .center
+ label.textColor = Appearance.default.colorPalette.subtitleText
+ label.isHidden = true
+ return label
+ }()
+
+ init(currentUserController: CurrentChatUserController) {
+ self.currentUserController = currentUserController
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ title = "Drafts"
+
+ userAvatarView.controller = currentUserController
+ userAvatarView.addTarget(self, action: #selector(didTapOnCurrentUserAvatar), for: .touchUpInside)
+ userAvatarView.translatesAutoresizingMaskIntoConstraints = false
+
+ navigationItem.backButtonTitle = ""
+ navigationItem.leftBarButtonItem = UIBarButtonItem(customView: userAvatarView)
+
+ setupViews()
+ loadDrafts()
+ }
+
+ private func setupViews() {
+ view.backgroundColor = Appearance.default.colorPalette.background
+ tableView.backgroundColor = Appearance.default.colorPalette.background
+
+ view.addSubview(tableView)
+ view.addSubview(loadingIndicator)
+ view.addSubview(emptyStateLabel)
+
+ NSLayoutConstraint.activate([
+ tableView.topAnchor.constraint(equalTo: view.topAnchor),
+ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+
+ loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
+
+ emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ emptyStateLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+ }
+
+ private func loadDrafts() {
+ currentUserController.delegate = self
+ loadingIndicator.startAnimating()
+ currentUserController.loadDraftMessages { [weak self] _ in
+ self?.loadingIndicator.stopAnimating()
+ }
+ }
+
+ private func loadMoreDrafts() {
+ guard !isPaginatingDrafts && !currentUserController.hasLoadedAllDrafts else {
+ return
+ }
+
+ isPaginatingDrafts = true
+ currentUserController.loadMoreDraftMessages { [weak self] _ in
+ self?.isPaginatingDrafts = false
+ }
+ }
+
+ @objc private func didTapOnCurrentUserAvatar(_ sender: Any) {
+ presentUserOptionsAlert(
+ onLogout: onLogout,
+ onDisconnect: onDisconnect,
+ client: currentUserController.client
+ )
+ }
+}
+
+extension DemoDraftMessageListVC: CurrentChatUserControllerDelegate {
+ func currentUserController(
+ _ controller: CurrentChatUserController,
+ didChangeDraftMessages draftMessages: [DraftMessage]
+ ) {
+ drafts = draftMessages
+ tableView.reloadData()
+ emptyStateLabel.isHidden = !drafts.isEmpty
+ tableView.isHidden = drafts.isEmpty
+ }
+}
+
+extension DemoDraftMessageListVC: UITableViewDataSource, UITableViewDelegate {
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ drafts.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "DemoDraftMessageCell", for: indexPath) as? DemoDraftMessageCell
+ let draft = drafts[indexPath.row]
+ if let cid = draft.cid {
+ cell?.configure(with: draft, channel: currentUserController.dataStore.channel(cid: cid))
+ }
+ return cell ?? .init()
+ }
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ let threshold: CGFloat = 100
+ let contentOffset = scrollView.contentOffset.y
+ let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
+
+ if maximumOffset - contentOffset <= threshold {
+ loadMoreDrafts()
+ }
+ }
+
+ func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completion in
+ guard let self = self else { return }
+ let draft = self.drafts[indexPath.row]
+ guard let cid = draft.cid else { return }
+
+ self.currentUserController.deleteDraftMessage(for: cid, threadId: draft.threadId)
+ completion(true)
+ }
+
+ return UISwipeActionsConfiguration(actions: [deleteAction])
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let draft = drafts[indexPath.row]
+ guard let cid = draft.cid else { return }
+
+ let channelController = currentUserController.client.channelController(
+ for: cid,
+ messageOrdering: .topToBottom
+ )
+
+ if let parentId = draft.threadId {
+ let messageController = currentUserController.client.messageController(
+ cid: cid,
+ messageId: parentId
+ )
+ let threadVC = DemoChatThreadVC()
+ threadVC.messageController = messageController
+ threadVC.channelController = channelController
+ navigationController?.pushViewController(threadVC, animated: true)
+ return
+ }
+
+ let channelVC = DemoChatChannelVC()
+ channelVC.channelController = channelController
+ navigationController?.pushViewController(channelVC, animated: true)
+ }
+}
+
+class DemoDraftMessageCell: UITableViewCell {
+ private let channelNameLabel: UILabel = {
+ let label = UILabel()
+ label.font = Appearance.default.fonts.bodyBold
+ label.textColor = Appearance.default.colorPalette.text
+ return label
+ }()
+
+ private let messageLabel: UILabel = {
+ let label = UILabel()
+ label.font = Appearance.default.fonts.footnote
+ label.numberOfLines = 2
+ label.textColor = Appearance.default.colorPalette.subtitleText
+ return label
+ }()
+
+ private let dateLabel: UILabel = {
+ let label = UILabel()
+ label.font = Appearance.default.fonts.footnote
+ label.textColor = Appearance.default.colorPalette.subtitleText
+ return label
+ }()
+
+ private let pencilImageView: UIImageView = {
+ let imageView = UIImageView(image: UIImage(systemName: "bubble.and.pencil"))
+ imageView.contentMode = .scaleAspectFit
+ imageView.tintColor = Appearance.default.colorPalette.subtitleText
+ return imageView
+ }()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ backgroundColor = Appearance.default.colorPalette.background
+ setupViews()
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func setupViews() {
+ VContainer(spacing: 4) {
+ HContainer {
+ channelNameLabel
+ Spacer()
+ dateLabel
+ }
+ HContainer(spacing: 4) {
+ pencilImageView
+ .width(20)
+ .height(20)
+ messageLabel
+ }
+ }.embed(in: contentView, insets: .init(top: 8, leading: 16, bottom: 8, trailing: 16))
+ }
+
+ func configure(with draft: DraftMessage, channel: ChatChannel?) {
+ if let channel = channel {
+ let channelName = Appearance.default.formatters.channelName.format(
+ channel: channel,
+ forCurrentUserId: StreamChatWrapper.shared.client?.currentUserId
+ ) ?? ""
+ if draft.threadId != nil {
+ channelNameLabel.text = "Thread in # \(channelName)"
+ } else {
+ channelNameLabel.text = "# \(channelName)"
+ }
+ }
+
+ messageLabel.text = draft.text
+
+ let dateFormatter = Appearance.default.formatters.messageTimestamp
+ dateLabel.text = dateFormatter.format(draft.createdAt)
+ }
+}
diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
index 7920e40a6c0..3d63ce1573d 100644
--- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
+++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
@@ -570,6 +570,16 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
channelController.createNewMessage(text: message, skipPush: true)
}
}),
+ .init(title: "Say Hi to a specific member", isEnabled: canSendMessage, handler: { [unowned self] _ in
+ self.rootViewController.presentAlert(title: "Enter the channel member id", textFieldPlaceholder: "Send message") { userId in
+ guard let userId, !userId.isEmpty,
+ channelController.channel?.lastActiveMembers.map(\.id).contains(userId) == true else {
+ self.rootViewController.presentAlert(title: "user id is not valid")
+ return
+ }
+ channelController.createNewMessage(text: "Hi", restrictedVisibility: [userId])
+ }
+ }),
.init(title: "Send system message", isEnabled: canSendMessage, handler: { [unowned self] _ in
self.rootViewController.presentAlert(title: "Enter the message text", textFieldPlaceholder: "Send message") { message in
guard let message = message, !message.isEmpty else {
diff --git a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
index d700810c91d..e63244d3ff0 100644
--- a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
+++ b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
@@ -47,10 +47,22 @@ extension DemoAppCoordinator {
threadListVC.onDisconnect = { [weak self] in
self?.disconnect()
}
+
+ let draftsVC = DemoDraftMessageListVC(
+ currentUserController: client.currentUserController()
+ )
+ draftsVC.onLogout = { [weak self] in
+ self?.logOut()
+ }
+ draftsVC.onDisconnect = { [weak self] in
+ self?.disconnect()
+ }
+
let tabBarViewController = DemoAppTabBarController(
channelListVC: chatVC,
threadListVC: UINavigationController(rootViewController: threadListVC),
- currentUserController: StreamChatWrapper.shared.client!.currentUserController()
+ draftListVC: UINavigationController(rootViewController: draftsVC),
+ currentUserController: client.currentUserController()
)
set(rootViewController: tabBarViewController, animated: animated)
DemoAppConfiguration.showPerformanceTracker()
diff --git a/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift b/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
index 49262cb9832..d266b3f5afe 100644
--- a/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
+++ b/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
@@ -49,6 +49,7 @@ extension StreamChatWrapper {
Components.default.isJumpToUnreadEnabled = true
Components.default.messageSwipeToReplyEnabled = true
Components.default.isComposerLinkPreviewEnabled = true
+ Components.default.isDraftMessagesEnabled = true
Components.default.channelListSearchStrategy = .messages
// Customize UI components
diff --git a/Gemfile.lock b/Gemfile.lock
index 878033382db..0249c21c9be 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -279,7 +279,7 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- mini_portile2 (2.8.7)
+ mini_portile2 (2.8.8)
minitest (5.25.1)
molinillo (0.8.0)
multi_json (1.15.0)
@@ -295,7 +295,7 @@ GEM
netrc (0.11.0)
nio4r (2.7.3)
nkf (0.2.0)
- nokogiri (1.16.7)
+ nokogiri (1.18.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
octokit (9.1.0)
@@ -316,7 +316,7 @@ GEM
puma (6.4.3)
nio4r (~> 2.0)
racc (1.8.1)
- rack (3.1.8)
+ rack (3.1.10)
rack-protection (4.1.0)
base64 (>= 0.1.0)
logger (>= 1.6.0)
diff --git a/Githubfile b/Githubfile
index 95a4bae9b64..6af32281d0b 100644
--- a/Githubfile
+++ b/Githubfile
@@ -1,7 +1,7 @@
#!/bin/bash
-export ALLURECTL_VERSION='2.15.1'
-export XCRESULTS_VERSION='1.16.3'
+export ALLURECTL_VERSION='2.16.0'
+export XCRESULTS_VERSION='1.19.1'
export YEETD_VERSION='1.0'
export GCLOUD_VERSION='464.0.0'
export MINT_VERSION='0.17.5'
diff --git a/README.md b/README.md
index 206faf1c526..ebde461e2d1 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/APIClient/Endpoints/CallEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/CallEndpoints.swift
deleted file mode 100644
index 68501d697f0..00000000000
--- a/Sources/StreamChat/APIClient/Endpoints/CallEndpoints.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// Copyright © 2025 Stream.io Inc. All rights reserved.
-//
-
-import Foundation
-
-extension Endpoint {
- static func getCallToken(callId: String) -> Endpoint {
- .init(
- path: .callToken(callId),
- method: .post,
- queryItems: nil,
- requiresConnectionId: false,
- body: nil
- )
- }
-
- static func createCall(cid: ChannelId, callId: String, type: String) -> Endpoint {
- .init(
- path: .createCall(cid.apiPath),
- method: .post,
- queryItems: nil,
- requiresConnectionId: false,
- body: CallRequestBody(id: callId, type: type)
- )
- }
-}
diff --git a/Sources/StreamChat/APIClient/Endpoints/DraftEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/DraftEndpoints.swift
new file mode 100644
index 00000000000..34f156cd49a
--- /dev/null
+++ b/Sources/StreamChat/APIClient/Endpoints/DraftEndpoints.swift
@@ -0,0 +1,64 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+extension Endpoint {
+ static func drafts(query: DraftListQuery) -> Endpoint {
+ .init(
+ path: .drafts,
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ requiresToken: true,
+ body: query
+ )
+ }
+
+ static func updateDraftMessage(
+ channelId: ChannelId,
+ requestBody: DraftMessageRequestBody
+ ) -> Endpoint {
+ let body: [String: AnyEncodable] = [
+ "message": AnyEncodable(requestBody)
+ ]
+ return .init(
+ path: .draftMessage(channelId),
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: body
+ )
+ }
+
+ static func getDraftMessage(
+ channelId: ChannelId,
+ threadId: MessageId?
+ ) -> Endpoint {
+ .init(
+ path: .draftMessage(channelId),
+ method: .get,
+ queryItems: threadId.map {
+ ["parent_id": $0]
+ },
+ requiresConnectionId: false,
+ body: nil
+ )
+ }
+
+ static func deleteDraftMessage(
+ channelId: ChannelId,
+ threadId: MessageId?
+ ) -> Endpoint {
+ .init(
+ path: .draftMessage(channelId),
+ method: .delete,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: threadId.map {
+ ["parent_id": $0]
+ }
+ )
+ }
+}
diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
index 92df26052ef..232ab780388 100644
--- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
@@ -7,7 +7,7 @@ import Foundation
extension EndpointPath {
var shouldBeQueuedOffline: Bool {
switch self {
- case .sendMessage, .editMessage, .deleteMessage, .pinMessage, .unpinMessage, .addReaction, .deleteReaction:
+ case .sendMessage, .editMessage, .deleteMessage, .pinMessage, .unpinMessage, .addReaction, .deleteReaction, .draftMessage:
return true
case .createChannel, .connect, .sync, .users, .guest, .members, .partialMemberUpdate, .search, .devices, .channels, .updateChannel,
.deleteChannel, .channelUpdate, .muteChannel, .showChannel, .truncateChannel, .markChannelRead, .markChannelUnread,
@@ -15,7 +15,7 @@ extension EndpointPath {
.replies, .reactions, .messageAction, .banMember, .flagUser, .flagMessage, .muteUser, .translateMessage,
.callToken, .createCall, .deleteFile, .deleteImage, .og, .appSettings, .threads, .thread, .markThreadRead, .markThreadUnread,
.polls, .pollsQuery, .poll, .pollOption, .pollOptions, .pollVotes, .pollVoteInMessage, .pollVote,
- .unread, .blockUser, .unblockUser:
+ .unread, .blockUser, .unblockUser, .drafts:
return false
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
index 0bba5d72ba5..8a31cc30a76 100644
--- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
@@ -51,6 +51,10 @@ enum EndpointPath: Codable {
case messageAction(MessageId)
case translateMessage(MessageId)
+ // Drafts
+ case drafts
+ case draftMessage(ChannelId)
+
case banMember
case flagUser(Bool)
case flagMessage(Bool)
@@ -128,6 +132,9 @@ enum EndpointPath: Codable {
case let .messageAction(messageId): return "messages/\(messageId)/action"
case let .translateMessage(messageId): return "messages/\(messageId)/translate"
+ case .drafts: return "drafts/query"
+ case let .draftMessage(channelId): return "channels/\(channelId.apiPath)/draft"
+
case .banMember: return "moderation/ban"
case let .flagUser(flag): return "moderation/\(flag ? "flag" : "unflag")"
case let .flagMessage(flag): return "moderation/\(flag ? "flag" : "unflag")"
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/CallPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/CallPayloads.swift
deleted file mode 100644
index 4c033027978..00000000000
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/CallPayloads.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-//
-// Copyright © 2025 Stream.io Inc. All rights reserved.
-//
-
-import Foundation
-
-struct CallTokenPayload: Decodable, Equatable {
- private enum CodingKeys: String, CodingKey {
- case token
- case agoraUid = "agora_uid"
- case agoraAppId = "agora_app_id"
- }
-
- /// The call token.
- let token: String
- /// The UID related to this token (Agora only).
- let agoraUid: UInt?
- /// The Agora App Id (Agora only).
- let agoraAppId: String?
-
- init(token: String, agoraUid: UInt?, agoraAppId: String?) {
- self.token = token
- self.agoraUid = agoraUid
- self.agoraAppId = agoraAppId
- }
-}
-
-struct AgoraPayload: Decodable, Equatable {
- let channel: String
-}
-
-struct HMSPayload: Decodable, Equatable {
- private enum CodingKeys: String, CodingKey {
- case roomId = "room_id"
- case roomName = "room_name"
- }
-
- let roomId: String
- let roomName: String
-}
-
-struct CallPayload: Decodable, Equatable {
- let id: String
- let provider: String
- let agora: AgoraPayload?
- let hms: HMSPayload?
-}
-
-struct CreateCallPayload: Decodable, Equatable {
- private enum CodingKeys: String, CodingKey {
- case token
- case agoraUid = "agora_uid"
- case agoraAppId = "agora_app_id"
- case call
- }
-
- /// The call object.
- let call: CallPayload
-
- /// The call token.
- let token: String
-
- /// The UID related to this token (Agora only).
- let agoraUid: UInt?
-
- /// The Agora App Id (Agora only).
- let agoraAppId: String?
-
- init(call: CallPayload, token: String, agoraUid: UInt?, agoraAppId: String?) {
- self.call = call
- self.token = token
- self.agoraUid = agoraUid
- self.agoraAppId = agoraAppId
- }
-}
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
index f1c7dcf0c76..2d365f39cfb 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
@@ -45,6 +45,8 @@ struct ChannelPayload {
let channelReads: [ChannelReadPayload]
let isHidden: Bool?
+
+ let draft: DraftPayload?
}
extension ChannelPayload {
@@ -68,6 +70,7 @@ extension ChannelPayload: Decodable {
case membership
case watcherCount = "watcher_count"
case hidden
+ case draft
}
init(from decoder: Decoder) throws {
@@ -83,7 +86,8 @@ extension ChannelPayload: Decodable {
pendingMessages: try container.decodeArrayIfPresentIgnoringFailures([MessagePayload.Boxed].self, forKey: .pendingMessages)?.map(\.message),
pinnedMessages: try container.decodeArrayIgnoringFailures([MessagePayload].self, forKey: .pinnedMessages),
channelReads: try container.decodeArrayIfPresentIgnoringFailures([ChannelReadPayload].self, forKey: .channelReads) ?? [],
- isHidden: try container.decodeIfPresent(Bool.self, forKey: .hidden)
+ isHidden: try container.decodeIfPresent(Bool.self, forKey: .hidden),
+ draft: try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
)
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift
new file mode 100644
index 00000000000..a94b633cfbb
--- /dev/null
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift
@@ -0,0 +1,176 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+class DraftPayloadResponse: Decodable {
+ let draft: DraftPayload
+
+ init(draft: DraftPayload) {
+ self.draft = draft
+ }
+}
+
+class DraftListPayloadResponse: Decodable {
+ let drafts: [DraftPayload]
+ let next: String?
+
+ init(drafts: [DraftPayload], next: String? = nil) {
+ self.drafts = drafts
+ self.next = next
+ }
+}
+
+class DraftPayload: Decodable {
+ let cid: ChannelId?
+ let channelPayload: ChannelDetailPayload?
+ let createdAt: Date
+ let message: DraftMessagePayload
+ let quotedMessage: MessagePayload?
+ let parentId: String?
+ let parentMessage: MessagePayload?
+
+ init(
+ cid: ChannelId?,
+ channelPayload: ChannelDetailPayload?,
+ createdAt: Date,
+ message: DraftMessagePayload,
+ quotedMessage: MessagePayload?,
+ parentId: String?,
+ parentMessage: MessagePayload?
+ ) {
+ self.cid = cid
+ self.channelPayload = channelPayload
+ self.createdAt = createdAt
+ self.message = message
+ self.quotedMessage = quotedMessage
+ self.parentId = parentId
+ self.parentMessage = parentMessage
+ }
+
+ required init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: MessagePayloadsCodingKeys.self)
+ cid = try container.decodeIfPresent(ChannelId.self, forKey: .channelId)
+ parentId = try container.decodeIfPresent(String.self, forKey: .parentId)
+ channelPayload = try container.decodeIfPresent(ChannelDetailPayload.self, forKey: .channel)
+ createdAt = try container.decode(Date.self, forKey: .createdAt)
+ quotedMessage = try container.decodeIfPresent(MessagePayload.self, forKey: .quotedMessage)
+ parentMessage = try container.decodeIfPresent(MessagePayload.self, forKey: .parentMessage)
+ message = try container.decode(DraftMessagePayload.self, forKey: .message)
+ }
+}
+
+class DraftMessagePayload: Decodable {
+ let id: String
+ let text: String
+ let command: String?
+ let args: String?
+ let showReplyInChannel: Bool
+ let mentionedUsers: [UserPayload]?
+ let extraData: [String: RawJSON]
+ let attachments: [MessageAttachmentPayload]?
+ let isSilent: Bool
+
+ init(
+ id: String,
+ text: String,
+ command: String?,
+ args: String?,
+ showReplyInChannel: Bool,
+ mentionedUsers: [UserPayload]?,
+ extraData: [String: RawJSON],
+ attachments: [MessageAttachmentPayload]?,
+ isSilent: Bool
+ ) {
+ self.id = id
+ self.text = text
+ self.command = command
+ self.args = args
+ self.showReplyInChannel = showReplyInChannel
+ self.mentionedUsers = mentionedUsers
+ self.extraData = extraData
+ self.attachments = attachments
+ self.isSilent = isSilent
+ }
+
+ required init(from decoder: any Decoder) throws {
+ let container = try decoder.container(keyedBy: MessagePayloadsCodingKeys.self)
+ id = try container.decode(String.self, forKey: .id)
+ text = try container.decode(String.self, forKey: .text)
+ command = try container.decodeIfPresent(String.self, forKey: .command)
+ args = try container.decodeIfPresent(String.self, forKey: .args)
+ showReplyInChannel = try container.decodeIfPresent(Bool.self, forKey: .showReplyInChannel) ?? false
+ mentionedUsers = try container.decodeIfPresent([UserPayload].self, forKey: .mentionedUsers)
+ attachments = try container.decodeIfPresent([MessageAttachmentPayload].self, forKey: .attachments)
+ isSilent = try container.decodeIfPresent(Bool.self, forKey: .isSilent) ?? false
+ if var payload = try? [String: RawJSON](from: decoder) {
+ payload.removeValues(forKeys: MessagePayloadsCodingKeys.allCases.map(\.rawValue))
+ extraData = payload
+ } else {
+ extraData = [:]
+ }
+ }
+}
+
+class DraftMessageRequestBody: Encodable {
+ let id: String
+ let text: String
+ let command: String?
+ let args: String?
+ let parentId: String?
+ let showReplyInChannel: Bool
+ let isSilent: Bool
+ let quotedMessageId: String?
+ let attachments: [MessageAttachmentPayload]
+ let mentionedUserIds: [UserId]
+ let extraData: [String: RawJSON]
+
+ init(
+ id: String,
+ text: String,
+ command: String?,
+ args: String?,
+ parentId: String?,
+ showReplyInChannel: Bool,
+ isSilent: Bool,
+ quotedMessageId: String?,
+ attachments: [MessageAttachmentPayload],
+ mentionedUserIds: [UserId],
+ extraData: [String: RawJSON]
+ ) {
+ self.id = id
+ self.text = text
+ self.command = command
+ self.args = args
+ self.parentId = parentId
+ self.showReplyInChannel = showReplyInChannel
+ self.isSilent = isSilent
+ self.quotedMessageId = quotedMessageId
+ self.attachments = attachments
+ self.mentionedUserIds = mentionedUserIds
+ self.extraData = extraData
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: MessagePayloadsCodingKeys.self)
+ try container.encode(id, forKey: .id)
+ try container.encode(text, forKey: .text)
+ try container.encodeIfPresent(command, forKey: .command)
+ try container.encodeIfPresent(args, forKey: .args)
+ try container.encodeIfPresent(parentId, forKey: .parentId)
+ try container.encodeIfPresent(showReplyInChannel, forKey: .showReplyInChannel)
+ try container.encodeIfPresent(quotedMessageId, forKey: .quotedMessageId)
+ try container.encode(isSilent, forKey: .isSilent)
+
+ if !attachments.isEmpty {
+ try container.encode(attachments, forKey: .attachments)
+ }
+
+ if !mentionedUserIds.isEmpty {
+ try container.encode(mentionedUserIds, forKey: .mentionedUsers)
+ }
+
+ try extraData.encode(to: encoder)
+ }
+}
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
index 5c60ea07092..880be5f98ac 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
@@ -8,6 +8,7 @@ import Foundation
enum MessagePayloadsCodingKeys: String, CodingKey, CaseIterable {
case id
case cid
+ case channelId = "channel_cid"
case type
case user
case userId = "user_id"
@@ -22,6 +23,7 @@ enum MessagePayloadsCodingKeys: String, CodingKey, CaseIterable {
case showReplyInChannel = "show_in_channel"
case quotedMessageId = "quoted_message_id"
case quotedMessage = "quoted_message"
+ case parentMessage = "parent_message"
case mentionedUsers = "mentioned_users"
case threadParticipants = "thread_participants"
case replyCount = "reply_count"
@@ -44,11 +46,14 @@ enum MessagePayloadsCodingKeys: String, CodingKey, CaseIterable {
case moderationDetails = "moderation_details" // moderation v1 key
case moderation // moderation v2 key
case messageTextUpdatedAt = "message_text_updated_at"
+ case message
case poll
case pollId = "poll_id"
case set
case unset
case skipEnrichUrl = "skip_enrich_url"
+ case restrictedVisibility = "restricted_visibility"
+ case draft
}
extension MessagePayload {
@@ -107,6 +112,8 @@ class MessagePayload: Decodable {
var poll: PollPayload?
+ var draft: DraftPayload?
+
/// Only message payload from `getMessage` endpoint contains channel data. It's a convenience workaround for having to
/// make an extra call do get channel details.
let channel: ChannelDetailPayload?
@@ -171,6 +178,7 @@ class MessagePayload: Decodable {
moderationDetails = try container.decodeIfPresent(MessageModerationDetailsPayload.self, forKey: .moderationDetails)
messageTextUpdatedAt = try container.decodeIfPresent(Date.self, forKey: .messageTextUpdatedAt)
poll = try container.decodeIfPresent(PollPayload.self, forKey: .poll)
+ draft = try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
}
init(
@@ -210,7 +218,8 @@ class MessagePayload: Decodable {
moderation: MessageModerationDetailsPayload? = nil,
moderationDetails: MessageModerationDetailsPayload? = nil,
messageTextUpdatedAt: Date? = nil,
- poll: PollPayload? = nil
+ poll: PollPayload? = nil,
+ draft: DraftPayload? = nil
) {
self.id = id
self.cid = cid
@@ -249,6 +258,7 @@ class MessagePayload: Decodable {
self.moderationDetails = moderationDetails
self.messageTextUpdatedAt = messageTextUpdatedAt
self.poll = poll
+ self.draft = draft
}
}
@@ -272,6 +282,7 @@ struct MessageRequestBody: Encodable {
var pinned: Bool
var pinExpires: Date?
var pollId: String?
+ var restrictedVisibility: [UserId]?
let extraData: [String: RawJSON]
init(
@@ -290,6 +301,7 @@ struct MessageRequestBody: Encodable {
pinned: Bool = false,
pinExpires: Date? = nil,
pollId: String? = nil,
+ restrictedVisibility: [UserId]? = nil,
extraData: [String: RawJSON]
) {
self.id = id
@@ -307,6 +319,7 @@ struct MessageRequestBody: Encodable {
self.pinned = pinned
self.pinExpires = pinExpires
self.pollId = pollId
+ self.restrictedVisibility = restrictedVisibility
self.extraData = extraData
}
@@ -324,6 +337,7 @@ struct MessageRequestBody: Encodable {
try container.encode(isSilent, forKey: .isSilent)
try container.encodeIfPresent(pollId, forKey: .pollId)
try container.encodeIfPresent(type, forKey: .type)
+ try container.encodeIfPresent(restrictedVisibility, forKey: .restrictedVisibility)
if !attachments.isEmpty {
try container.encode(attachments, forKey: .attachments)
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ThreadListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ThreadListPayload.swift
index 0c1bb613824..2be94378d69 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ThreadListPayload.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ThreadListPayload.swift
@@ -18,6 +18,7 @@ enum ThreadCodingKeys: String, CodingKey, CaseIterable {
case title
case latestReplies = "latest_replies"
case read
+ case draft
}
struct ThreadListPayload: Decodable {
@@ -58,6 +59,7 @@ struct ThreadPayload: Decodable {
let title: String?
let latestReplies: [MessagePayload]
let read: [ThreadReadPayload]
+ let draft: DraftPayload?
let extraData: [String: RawJSON]
init(
@@ -74,6 +76,7 @@ struct ThreadPayload: Decodable {
title: String?,
latestReplies: [MessagePayload],
read: [ThreadReadPayload],
+ draft: DraftPayload?,
extraData: [String: RawJSON]
) {
self.parentMessageId = parentMessageId
@@ -89,6 +92,7 @@ struct ThreadPayload: Decodable {
self.title = title
self.latestReplies = latestReplies
self.read = read
+ self.draft = draft
self.extraData = extraData
}
@@ -109,6 +113,7 @@ struct ThreadPayload: Decodable {
createdAt = try container.decode(Date.self, forKey: ThreadCodingKeys.createdAt)
updatedAt = try container.decodeIfPresent(Date.self, forKey: ThreadCodingKeys.updatedAt)
title = try container.decodeIfPresent(String.self, forKey: ThreadCodingKeys.title)
+ draft = try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
latestReplies = try container.decodeArrayIfPresentIgnoringFailures(
[MessagePayload].self,
forKey: ThreadCodingKeys.latestReplies
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift
index 7b85a76aa9c..d1879d7aae6 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift
@@ -121,47 +121,11 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
}
open func loadAsset(from url: URL) {
- /// We are going to check if the URL requested to load, represents the currentItem that we
- /// have already loaded (if any). In this case, we will try either to resume the existing playback
- /// or restart it, if possible.
- if let currentItem = player.currentItem?.asset as? AVURLAsset,
- url == currentItem.url,
- context.assetLocation == url {
- /// If the currentItem is paused, we want to continue the playback
- /// Otherwise, no action is required
- if context.state == .paused {
- play()
- } else if context.state == .stopped {
- /// If the currentItem has stopped, we want to restart the playback. We are replacing
- /// the currentItem with the same one to trigger the player's observers on the updated
- /// currentItem.
- player.replaceCurrentItem(with: .init(asset: currentItem))
- play()
- }
-
- /// This case may be triggered if we call ``loadAsset`` on a player that is currently
- /// playing the URL we provided. In this case we will Inform the delegate about the
- /// current state.
- notifyDelegates()
-
- return
- }
-
- /// We call stop to update the currently set delegate that the playback has been stopped
- /// and then we remove the current item from the player's queue.
- stop()
- player.replaceCurrentItem(with: nil)
-
- updateContext {
- $0.state = .loading
- $0.assetLocation = url
- }
- let asset = AVURLAsset(url: url)
+ loadAsset(from: url, options: nil)
+ }
- assetPropertyLoader.loadProperties(
- [.init(\.duration)],
- of: asset
- ) { [weak self] in self?.handleDurationLoading($0) }
+ open func loadAsset(from url: URL, options: [String: Any]?) {
+ handleAssetLoading(.init(url: url, options: options))
}
open func play() {
@@ -323,6 +287,57 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
context = newContext
}
+ /// Loads an asset into the player and manages playback accordingly.
+ ///
+ /// If the asset matches the current item, playback is resumed if paused or
+ /// restarted if stopped. Otherwise, playback is stopped, the current item is
+ /// cleared, and the context is updated with the new asset. The asset's duration
+ /// is then loaded asynchronously.
+ ///
+ /// - Parameter asset: The AVURLAsset to load into the player.
+ private func handleAssetLoading(_ asset: AVURLAsset) {
+ /// We are going to check if the URL requested to load, represents the currentItem that we
+ /// have already loaded (if any). In this case, we will try either to resume the existing playback
+ /// or restart it, if possible.
+ if let currentItem = player.currentItem?.asset as? AVURLAsset,
+ asset.url == currentItem.url,
+ context.assetLocation == asset.url {
+ /// If the currentItem is paused, we want to continue the playback
+ /// Otherwise, no action is required
+ if context.state == .paused {
+ play()
+ } else if context.state == .stopped {
+ /// If the currentItem has stopped, we want to restart the playback. We are replacing
+ /// the currentItem with the same one to trigger the player's observers on the updated
+ /// currentItem.
+ player.replaceCurrentItem(with: .init(asset: currentItem))
+ play()
+ }
+
+ /// This case may be triggered if we call ``loadAsset`` on a player that is currently
+ /// playing the URL we provided. In this case we will Inform the delegate about the
+ /// current state.
+ notifyDelegates()
+
+ return
+ }
+
+ /// We call stop to update the currently set delegate that the playback has been stopped
+ /// and then we remove the current item from the player's queue.
+ stop()
+ player.replaceCurrentItem(with: nil)
+
+ updateContext {
+ $0.state = .loading
+ $0.assetLocation = asset.url
+ }
+
+ assetPropertyLoader.loadProperties(
+ [.init(\.duration)],
+ of: asset
+ ) { [weak self] in self?.handleDurationLoading($0) }
+ }
+
/// It's used by the assetPropertyLoader to handle the completion (successful or failed) of duration's
/// asynchronous loading.
private func handleDurationLoading(
diff --git a/Sources/StreamChat/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift
index 4f27fa0235b..d756842abf5 100644
--- a/Sources/StreamChat/ChatClient+Environment.swift
+++ b/Sources/StreamChat/ChatClient+Environment.swift
@@ -142,7 +142,14 @@ extension ChatClient {
) -> PollsRepository = {
PollsRepository(database: $0, apiClient: $1)
}
-
+
+ var draftMessagesRepositoryBuilder: (
+ _ database: DatabaseContainer,
+ _ apiClient: APIClient
+ ) -> DraftMessagesRepository = {
+ DraftMessagesRepository(database: $0, apiClient: $1)
+ }
+
var channelListUpdaterBuilder: (
_ database: DatabaseContainer,
_ apiClient: APIClient
diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift
index 4ab7e02050d..1505ca7d8f2 100644
--- a/Sources/StreamChat/ChatClient.swift
+++ b/Sources/StreamChat/ChatClient.swift
@@ -78,6 +78,8 @@ public class ChatClient {
let pollsRepository: PollsRepository
+ let draftMessagesRepository: DraftMessagesRepository
+
let channelListUpdater: ChannelListUpdater
func makeMessagesPaginationStateHandler() -> MessagesPaginationStateHandling {
@@ -212,6 +214,7 @@ public class ChatClient {
apiClient
)
pollsRepository = environment.pollsRepositoryBuilder(databaseContainer, apiClient)
+ draftMessagesRepository = environment.draftMessagesRepositoryBuilder(databaseContainer, apiClient)
authRepository.delegate = self
apiClientEncoder.connectionDetailsProviderDelegate = self
diff --git a/Sources/StreamChat/ChatClientFactory.swift b/Sources/StreamChat/ChatClientFactory.swift
index 24491d433f2..47634239795 100644
--- a/Sources/StreamChat/ChatClientFactory.swift
+++ b/Sources/StreamChat/ChatClientFactory.swift
@@ -125,6 +125,7 @@ class ChatClientFactory {
newProcessedMessageIds: { [weak center] in center?.newMessageIds ?? [] }
),
ThreadUpdaterMiddleware(),
+ DraftUpdaterMiddleware(),
UserTypingStateUpdaterMiddleware(),
ChannelTruncatedEventMiddleware(),
MemberEventMiddleware(),
diff --git a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
index c820a1e0f00..2b061ff3d9f 100644
--- a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
+++ b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
@@ -187,6 +187,8 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
private let pollsRepository: PollsRepository
+ private let draftsRepository: DraftMessagesRepository
+
var _basePublishers: Any?
/// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose
/// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally,
@@ -232,7 +234,8 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
client.apiClient
)
pollsRepository = client.pollsRepository
-
+ draftsRepository = client.draftMessagesRepository
+
super.init()
setChannelObserver()
@@ -747,6 +750,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - quotedMessageId: An id of the message new message quotes. (inline reply)
/// - skipPush: If true, skips sending push notification to channel members.
/// - skipEnrichUrl: If true, the url preview won't be attached to the message.
+ /// - restrictedVisibility: The list of user ids that should be able to see the message.
/// - extraData: Additional extra data of the message object.
/// - completion: Called when saving the message to the local DB finishes.
///
@@ -760,6 +764,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
quotedMessageId: MessageId? = nil,
skipPush: Bool = false,
skipEnrichUrl: Bool = false,
+ restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:],
completion: ((Result) -> Void)? = nil
) {
@@ -782,6 +787,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
quotedMessageId: quotedMessageId,
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
+ restrictedVisibility: restrictedVisibility,
extraData: transformableInfo.extraData,
poll: nil,
completion: completion
@@ -793,11 +799,13 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - text: The text of the system message.
/// - messageId: The id for the sent message. By default, it is automatically generated by Stream.
+ /// - restrictedVisibility: The list of user ids that can see the message.
/// - extraData: The extra data for the message.
/// - completion: Called when saving the message to the local DB finishes.
public func createSystemMessage(
text: String,
messageId: MessageId? = nil,
+ restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:],
completion: ((Result) -> Void)? = nil
) {
@@ -822,6 +830,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
quotedMessageId: nil,
skipPush: false,
skipEnrichUrl: false,
+ restrictedVisibility: restrictedVisibility,
poll: nil,
extraData: extraData
) { result in
@@ -834,6 +843,91 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
}
}
+ /// Updates the draft message of this channel.
+ ///
+ /// If there is no draft message, a new draft message will be created.
+ /// - Parameters:
+ /// - text: The text of the draft message.
+ /// - isSilent: A flag indicating whether the message is a silent message.
+ /// Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread.
+ /// - attachments: The attachments of the draft message.
+ /// - mentionedUserIds: The mentioned user ids of the draft message.
+ /// - quotedMessageId: The message that the draft message is quoting.
+ /// - command: The command of the draft message.
+ /// - extraData: The extra data of the draft message.
+ /// - completion: Called when the draft message is saved to the server.
+ public func updateDraftMessage(
+ text: String,
+ isSilent: Bool = false,
+ attachments: [AnyAttachmentPayload] = [],
+ mentionedUserIds: [UserId] = [],
+ quotedMessageId: MessageId? = nil,
+ command: Command? = nil,
+ extraData: [String: RawJSON] = [:],
+ completion: ((Result) -> Void)? = nil
+ ) {
+ guard let cid = cid, isChannelAlreadyCreated else {
+ channelModificationFailed { error in
+ completion?(.failure(error ?? ClientError.Unknown()))
+ }
+ return
+ }
+
+ draftsRepository.updateDraft(
+ for: cid,
+ threadId: nil,
+ text: text,
+ isSilent: isSilent,
+ showReplyInChannel: false,
+ command: command?.name,
+ arguments: command?.args,
+ attachments: attachments,
+ mentionedUserIds: mentionedUserIds,
+ quotedMessageId: quotedMessageId,
+ extraData: extraData
+ ) { result in
+ self.callback {
+ completion?(result)
+ }
+ }
+ }
+
+ /// Loads the draft message of this channel.
+ ///
+ /// It is not necessary to call this method if the channel list query was called before.
+ public func loadDraftMessage(
+ completion: ((Result) -> Void)? = nil
+ ) {
+ guard let cid = cid, isChannelAlreadyCreated else {
+ channelModificationFailed { error in
+ completion?(.failure(error ?? ClientError.Unknown()))
+ }
+ return
+ }
+
+ draftsRepository.getDraft(for: cid, threadId: nil) { result in
+ self.callback {
+ completion?(result)
+ }
+ }
+ }
+
+ /// Deletes the draft message of this channel.
+ public func deleteDraftMessage(completion: ((Error?) -> Void)? = nil) {
+ guard let cid = cid, isChannelAlreadyCreated else {
+ channelModificationFailed { error in
+ completion?(error)
+ }
+ return
+ }
+
+ draftsRepository.deleteDraft(for: cid, threadId: nil) { error in
+ self.callback {
+ completion?(error)
+ }
+ }
+ }
+
/// Creates a new poll.
///
/// - Parameters:
@@ -1432,6 +1526,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
quotedMessageId: MessageId? = nil,
skipPush: Bool = false,
skipEnrichUrl: Bool = false,
+ restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:],
poll: PollPayload?,
completion: ((Result) -> Void)? = nil
@@ -1461,6 +1556,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
quotedMessageId: quotedMessageId,
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
+ restrictedVisibility: restrictedVisibility,
poll: poll,
extraData: extraData
) { result in
@@ -1493,7 +1589,7 @@ extension ChatChannelController {
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> ChannelUpdater = ChannelUpdater.init
-
+
var memberUpdaterBuilder: (
_ database: DatabaseContainer,
_ apiClient: APIClient
diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
index 1c1a1ee3dbf..6ef29d6bd3f 100644
--- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
+++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
@@ -72,7 +72,6 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
/// The unread messages and channels count for the current user.
///
/// Returns `noUnread` if `currentUser` doesn't exist yet.
- ///
public var unreadCount: UnreadCount {
currentUser?.unreadCount ?? .noUnread
}
@@ -86,6 +85,31 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
/// The worker used to update the current user member for a given channel.
private lazy var currentMemberUpdater = createMemberUpdater()
+ /// The query used for fetching the draft messages.
+ private var draftListQuery = DraftListQuery()
+
+ /// Use for observing the current user's draft messages changes.
+ private var draftMessagesObserver: BackgroundListDatabaseObserver?
+
+ /// The repository for draft messages.
+ private var draftMessagesRepository: DraftMessagesRepository
+
+ /// The token for the next page of draft messages.
+ private var draftMessagesNextCursor: String?
+
+ /// A flag to indicate whether all draft messages have been loaded.
+ public private(set) var hasLoadedAllDrafts: Bool = false
+
+ /// The current user's draft messages.
+ public var draftMessages: [DraftMessage] {
+ if let observer = draftMessagesObserver {
+ return Array(observer.items)
+ }
+
+ let observer = createDraftMessagesObserver(query: draftListQuery)
+ return Array(observer.items)
+ }
+
/// Creates a new `CurrentUserControllerGeneric`.
///
/// - Parameters:
@@ -95,6 +119,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
init(client: ChatClient, environment: Environment = .init()) {
self.client = client
self.environment = environment
+ draftMessagesRepository = client.draftMessagesRepository
}
/// Synchronize local data with remote. Waits for the client to connect but doesn’t initiate the connection itself.
@@ -334,8 +359,79 @@ public extension CurrentChatUserController {
}
}
- private func createMemberUpdater() -> ChannelMemberUpdater {
- .init(database: client.databaseContainer, apiClient: client.apiClient)
+ /// Loads the draft messages for the current user.
+ ///
+ /// It will load the first page of drafts of the current user.
+ /// `loadMoreDraftMessages` can be used to load the next pages.
+ ///
+ /// - Parameters:
+ /// - query: The query for filtering the drafts.
+ /// - completion: Called when the API call is finished.
+ /// It is optional since it can be observed from the delegate events.
+ func loadDraftMessages(
+ query: DraftListQuery = DraftListQuery(),
+ completion: ((Result<[DraftMessage], Error>) -> Void)? = nil
+ ) {
+ draftListQuery = query
+ createDraftMessagesObserver(query: query)
+ draftMessagesRepository.loadDrafts(query: query) { result in
+ self.callback {
+ switch result {
+ case let .success(response):
+ self.draftMessagesNextCursor = response.next
+ self.hasLoadedAllDrafts = response.next == nil
+ completion?(.success(response.drafts))
+ case let .failure(error):
+ completion?(.failure(error))
+ }
+ }
+ }
+ }
+
+ /// Loads more draft messages for the current user.
+ ///
+ /// - Parameters:
+ /// - limit: The number of draft messages to load. If `nil`, the default limit will be used.
+ /// - completion: Called when the API call is finished.
+ /// It is optional since it can be observed from the delegate events.
+ func loadMoreDraftMessages(
+ limit: Int? = nil,
+ completion: ((Result<[DraftMessage], Error>) -> Void)? = nil
+ ) {
+ guard let nextCursor = draftMessagesNextCursor else {
+ completion?(.success([]))
+ return
+ }
+
+ let limit = limit ?? draftListQuery.pagination.pageSize
+ var updatedQuery = draftListQuery
+ updatedQuery.pagination = Pagination(pageSize: limit, cursor: nextCursor)
+
+ draftMessagesRepository.loadDrafts(query: updatedQuery) { result in
+ self.callback {
+ switch result {
+ case let .success(response):
+ self.draftMessagesNextCursor = response.next
+ self.hasLoadedAllDrafts = response.next == nil
+ completion?(.success(response.drafts))
+ case let .failure(error):
+ completion?(.failure(error))
+ }
+ }
+ }
+ }
+
+ /// Deletes the draft message of the given channel or thread.
+ func deleteDraftMessage(
+ for cid: ChannelId,
+ threadId: MessageId? = nil,
+ completion: ((Error?) -> Void)? = nil
+ ) {
+ draftMessagesRepository.deleteDraft(for: cid, threadId: threadId) { error in
+ self.callback {
+ completion?(error)
+ }
+ }
}
}
@@ -350,6 +446,14 @@ extension CurrentChatUserController {
_ fetchedResultsControllerType: NSFetchedResultsController.Type
) -> BackgroundEntityDatabaseObserver = BackgroundEntityDatabaseObserver.init
+ var draftMessagesObserverBuilder: (
+ _ database: DatabaseContainer,
+ _ fetchRequest: NSFetchRequest,
+ _ itemCreator: @escaping (MessageDTO) throws -> DraftMessage
+ ) -> BackgroundListDatabaseObserver = {
+ .init(database: $0, fetchRequest: $1, itemCreator: $2, itemReuseKeyPaths: (\DraftMessage.id, \MessageDTO.id))
+ }
+
var currentUserUpdaterBuilder = CurrentUserUpdater.init
}
}
@@ -378,6 +482,28 @@ private extension CurrentChatUserController {
NSFetchedResultsController.self
)
}
+
+ private func createMemberUpdater() -> ChannelMemberUpdater {
+ .init(database: client.databaseContainer, apiClient: client.apiClient)
+ }
+
+ @discardableResult
+ private func createDraftMessagesObserver(query: DraftListQuery) -> BackgroundListDatabaseObserver {
+ let observer = environment.draftMessagesObserverBuilder(
+ client.databaseContainer,
+ MessageDTO.draftMessagesFetchRequest(query: query),
+ { DraftMessage(try $0.asModel()) }
+ )
+ observer.onDidChange = { [weak self] _ in
+ guard let self = self else { return }
+ self.delegateCallback {
+ $0.currentUserController(self, didChangeDraftMessages: self.draftMessages)
+ }
+ }
+ try? observer.startObserving()
+ draftMessagesObserver = observer
+ return observer
+ }
}
// MARK: - Delegates
@@ -389,12 +515,23 @@ public protocol CurrentChatUserControllerDelegate: AnyObject {
/// The controller observed a change in the `CurrentChatUser` entity.
func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUser: EntityChange)
+
+ /// The controller observed a change in the draft messages.
+ func currentUserController(
+ _ controller: CurrentChatUserController,
+ didChangeDraftMessages draftMessages: [DraftMessage]
+ )
}
public extension CurrentChatUserControllerDelegate {
func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUserUnreadCount: UnreadCount) {}
func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUser: EntityChange) {}
+
+ func currentUserController(
+ _ controller: CurrentChatUserController,
+ didChangeDraftMessages draftMessages: [DraftMessage]
+ ) {}
}
public extension CurrentChatUserController {
diff --git a/Sources/StreamChat/Controllers/MessageController/MessageController.swift b/Sources/StreamChat/Controllers/MessageController/MessageController.swift
index cf4b883c8f5..de88b4c0f67 100644
--- a/Sources/StreamChat/Controllers/MessageController/MessageController.swift
+++ b/Sources/StreamChat/Controllers/MessageController/MessageController.swift
@@ -187,6 +187,9 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
private let replyPaginationHandler: MessagesPaginationStateHandling
private var replyPaginationState: MessagesPaginationState { replyPaginationHandler.state }
+ /// The drafts repository.
+ private let draftsRepository: DraftMessagesRepository
+
/// Creates a new `MessageControllerGeneric`.
/// - Parameters:
/// - client: The `Client` instance this controller belongs to.
@@ -206,6 +209,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
client.databaseContainer,
client.apiClient
)
+ draftsRepository = client.draftMessagesRepository
super.init()
setRepliesObserver()
@@ -259,12 +263,21 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
extraData: [String: RawJSON]? = nil,
completion: ((Error?) -> Void)? = nil
) {
+ var transformableInfo = NewMessageTransformableInfo(
+ text: text,
+ attachments: attachments,
+ extraData: extraData ?? message?.extraData ?? [:]
+ )
+ if let transformer = client.config.modelsTransformer {
+ transformableInfo = transformer.transform(newMessageInfo: transformableInfo)
+ }
+
messageUpdater.editMessage(
messageId: messageId,
- text: text,
+ text: transformableInfo.text,
skipEnrichUrl: skipEnrichUrl,
- attachments: attachments,
- extraData: extraData
+ attachments: transformableInfo.attachments,
+ extraData: transformableInfo.extraData
) { result in
self.callback {
completion?(result.error)
@@ -294,6 +307,8 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - messageId: The id for the sent message. By default, it is automatically generated by Stream..
/// - text: Text of the message.
+ /// - isSilent: A flag indicating whether the message is a silent message.
+ /// Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread.
/// - pinning: Pins the new message. `nil` if should not be pinned.
/// - attachments: An array of the attachments for the message.
/// `Note`: can be built-in types, custom attachment types conforming to `AttachmentEnvelope` protocol
@@ -843,6 +858,78 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
}
}
+
+ /// Updates the draft message for this thread.
+ ///
+ /// If there is no draft message, a new draft message will be created.
+ /// - Parameters:
+ /// - text: The text of the draft message.
+ /// - isSilent: A flag indicating whether the message is a silent message.
+ /// Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread.
+ /// - attachments: The attachments of the draft message.
+ /// - mentionedUserIds: The mentioned user ids of the draft message.
+ /// - quotedMessageId: The message that the draft message is quoting.
+ /// - showReplyInChannel: If the draft message should be shown in the channel.
+ /// - command: The command of the draft message.
+ /// - extraData: The extra data of the draft message.
+ /// - completion: Called when the draft message is saved to the server.
+ public func updateDraftReply(
+ text: String,
+ isSilent: Bool = false,
+ attachments: [AnyAttachmentPayload] = [],
+ mentionedUserIds: [UserId] = [],
+ quotedMessageId: MessageId? = nil,
+ showReplyInChannel: Bool = false,
+ command: Command? = nil,
+ extraData: [String: RawJSON] = [:],
+ completion: ((Result) -> Void)? = nil
+ ) {
+ draftsRepository.updateDraft(
+ for: cid,
+ threadId: messageId,
+ text: text,
+ isSilent: isSilent,
+ showReplyInChannel: showReplyInChannel,
+ command: command?.name,
+ arguments: command?.args,
+ attachments: attachments,
+ mentionedUserIds: mentionedUserIds,
+ quotedMessageId: quotedMessageId,
+ extraData: extraData
+ ) { result in
+ self.callback {
+ completion?(result)
+ }
+ }
+ }
+
+ /// Loads the draft message for this thread.
+ ///
+ /// It is not necessary to call this method if the thread was loaded before.
+ public func loadDraftReply(
+ completion: ((Result) -> Void)? = nil
+ ) {
+ draftsRepository.getDraft(
+ for: cid,
+ threadId: messageId
+ ) { result in
+ self.callback {
+ completion?(result)
+ }
+ }
+ }
+
+ /// Deletes the draft message for this thread.
+ public func deleteDraftReply(completion: ((Error?) -> Void)? = nil) {
+ draftsRepository.deleteDraft(
+ for: cid,
+ threadId: messageId
+ ) { error in
+ self.callback {
+ completion?(error)
+ }
+ }
+ }
}
// MARK: - Environment
diff --git a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
index 2e8b7aa99e4..af60ce40118 100644
--- a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
@@ -110,7 +110,11 @@ class AttachmentDTO: NSManagedObject {
static func pendingUploadFetchRequest() -> NSFetchRequest {
let request = NSFetchRequest(entityName: AttachmentDTO.entityName)
request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)]
- request.predicate = NSPredicate(format: "localStateRaw == %@", LocalAttachmentState.pendingUpload.rawValue)
+ request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+ NSPredicate(format: "localStateRaw == %@", LocalAttachmentState.pendingUpload.rawValue),
+ NSPredicate(format: "message.draftOfChannel == nil"),
+ NSPredicate(format: "message.draftOfThread == nil")
+ ])
return request
}
diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
index 5100e2af7d4..da4e1b40189 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
@@ -66,6 +66,7 @@ class ChannelDTO: NSManagedObject {
@NSManaged var watchers: Set
@NSManaged var memberListQueries: Set
@NSManaged var previewMessage: MessageDTO?
+ @NSManaged var draftMessage: MessageDTO?
/// If the current channel is muted by the current user, `mute` contains details.
@NSManaged var mute: ChannelMuteDTO?
@@ -328,6 +329,16 @@ extension NSManagedObjectContext {
dto.updateOldestMessageAt(payload: payload)
+ if let draftMessage = payload.draft {
+ dto.draftMessage = try saveDraftMessage(payload: draftMessage, for: payload.channel.cid, cache: nil)
+ } else {
+ /// If the payload does not contain a draft message, we should
+ /// delete the existing draft message if it exists.
+ if let draftMessage = dto.draftMessage {
+ deleteDraftMessage(in: payload.channel.cid, threadId: draftMessage.parentMessageId)
+ }
+ }
+
try payload.pinnedMessages.forEach {
_ = try saveMessage(payload: $0, channelDTO: dto, syncOwnReactions: true, cache: cache)
}
@@ -563,6 +574,7 @@ extension ChatChannel {
let membership = try dto.membership.map { try $0.asModel() }
let pinnedMessages = dto.pinnedMessages.compactMap { try? $0.relationshipAsModel(depth: depth) }
let previewMessage = try? dto.previewMessage?.relationshipAsModel(depth: depth)
+ let draftMessage = try? dto.draftMessage?.relationshipAsModel(depth: depth)
let typingUsers = Set(dto.currentlyTypingUsers.compactMap { try? $0.asModel() })
let channel = try ChatChannel(
@@ -596,7 +608,8 @@ extension ChatChannel {
lastMessageFromCurrentUser: latestMessageFromUser,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
- previewMessage: previewMessage
+ previewMessage: previewMessage,
+ draftMessage: draftMessage.map(DraftMessage.init)
)
if let transformer = clientConfig.modelsTransformer {
diff --git a/Sources/StreamChat/Database/DTOs/MessageDTO.swift b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
index a72ffd4cc42..26eb70f3369 100644
--- a/Sources/StreamChat/Database/DTOs/MessageDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
@@ -83,6 +83,11 @@ class MessageDTO: NSManagedObject {
@NSManaged var searches: Set
@NSManaged var previewOfChannel: ChannelDTO?
+ @NSManaged var draftOfChannel: ChannelDTO?
+ @NSManaged var draftOfThread: MessageDTO?
+ @NSManaged var draftReply: MessageDTO?
+ @NSManaged var isDraft: Bool
+
/// If the message is sent by the current user, this field
/// contains channel reads of other channel members (excluding the current user),
/// where `read.lastRead >= self.createdAt`.
@@ -92,6 +97,8 @@ class MessageDTO: NSManagedObject {
///
/// For messages authored NOT by the current user this field is always empty.
@NSManaged var reads: Set
+
+ @NSManaged var restrictedVisibility: Set?
@NSManaged var pinned: Bool
@NSManaged var pinnedBy: UserDTO?
@@ -170,6 +177,22 @@ class MessageDTO: NSManagedObject {
return request
}
+ /// Returns all the draft messages.
+ static func draftMessagesFetchRequest(query: DraftListQuery) -> NSFetchRequest {
+ let request = NSFetchRequest(entityName: MessageDTO.entityName)
+ MessageDTO.applyPrefetchingState(to: request)
+ request.sortDescriptors = query.sorting.compactMap { $0.sortDescriptor() }
+ let isPartOfChannelOrThreadPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [
+ NSPredicate(format: "draftOfChannel != nil"),
+ NSPredicate(format: "draftOfThread != nil")
+ ])
+ request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+ isPartOfChannelOrThreadPredicate,
+ NSPredicate(format: "isDraft == YES")
+ ])
+ return request
+ }
+
static func allAttachmentsAreUploadedOrEmptyPredicate() -> NSCompoundPredicate {
NSCompoundPredicate(orPredicateWithSubpredicates: [
.init(
@@ -228,6 +251,10 @@ class MessageDTO: NSManagedObject {
.init(format: "deletedAt == nil")
}
+ private static func ignoreDraftMessagesPredicate() -> NSPredicate {
+ .init(format: "isDraft == NO")
+ }
+
private static func channelPredicate(with cid: String) -> NSPredicate {
.init(format: "channel.cid == %@", cid)
}
@@ -293,7 +320,8 @@ class MessageDTO: NSManagedObject {
messageTypePredicate,
nonTruncatedMessagesPredicate(),
ignoreOlderMessagesPredicate,
- deletedMessagesPredicate(deletedMessagesVisibility: deletedMessagesVisibility)
+ deletedMessagesPredicate(deletedMessagesVisibility: deletedMessagesVisibility),
+ ignoreDraftMessagesPredicate()
]
if filterNewerMessages {
@@ -329,6 +357,7 @@ class MessageDTO: NSManagedObject {
])
var subpredicates = [
+ ignoreDraftMessagesPredicate(),
replyMessage,
shouldShowInsideThread,
ignoreNewerRepliesPredicate,
@@ -349,7 +378,8 @@ class MessageDTO: NSManagedObject {
channelPredicate(with: cid),
messageSentPredicate(),
nonTruncatedMessagesPredicate(),
- nonDeletedMessagesPredicate()
+ nonDeletedMessagesPredicate(),
+ ignoreDraftMessagesPredicate()
])
}
@@ -400,7 +430,8 @@ class MessageDTO: NSManagedObject {
MessageDTO.applyPrefetchingState(to: request)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "ANY searches.filterHash == %@", query.filterHash),
- NSPredicate(format: "isHardDeleted == NO")
+ NSPredicate(format: "isHardDeleted == NO"),
+ ignoreDraftMessagesPredicate()
])
let sortDescriptors = query.sort.compactMap { $0.key.sortDescriptor(isAscending: $0.isAscending) }
request.sortDescriptors = sortDescriptors.isEmpty ? [MessageSearchSortingKey.defaultSortDescriptor] : sortDescriptors
@@ -647,6 +678,7 @@ extension NSManagedObjectContext: MessageDatabaseSession {
skipPush: Bool,
skipEnrichUrl: Bool,
poll: PollPayload?,
+ restrictedVisibility: [UserId],
extraData: [String: RawJSON]
) throws -> MessageDTO {
guard let currentUserDTO = currentUser else {
@@ -686,6 +718,7 @@ extension NSManagedObjectContext: MessageDatabaseSession {
message.reactionScores = [:]
message.reactionCounts = [:]
message.reactionGroups = []
+ message.restrictedVisibility = Set(restrictedVisibility)
// Message type
if parentMessageId != nil {
@@ -737,10 +770,92 @@ extension NSManagedObjectContext: MessageDatabaseSession {
return message
}
+ func createNewDraftMessage(
+ in cid: ChannelId,
+ text: String,
+ command: String?,
+ arguments: String?,
+ parentMessageId: MessageId?,
+ attachments: [AnyAttachmentPayload],
+ mentionedUserIds: [UserId],
+ showReplyInChannel: Bool,
+ isSilent: Bool,
+ quotedMessageId: MessageId?,
+ extraData: [String: RawJSON]
+ ) throws -> MessageDTO {
+ guard let currentUserDTO = currentUser else {
+ throw ClientError.CurrentUserDoesNotExist()
+ }
+
+ guard let channelDTO = ChannelDTO.load(cid: cid, context: self) else {
+ throw ClientError.ChannelDoesNotExist(cid: cid)
+ }
+
+ /// Makes sure to delete the existing draft message if it exists.
+ deleteDraftMessage(in: cid, threadId: parentMessageId)
+
+ let createdAt = Date()
+ let message = MessageDTO.loadOrCreate(id: .newUniqueId, context: self, cache: nil)
+ message.isDraft = true
+ message.locallyCreatedAt = createdAt.bridgeDate
+ message.createdAt = createdAt.bridgeDate
+ message.updatedAt = createdAt.bridgeDate
+ message.cid = cid.rawValue
+ message.text = text
+ message.command = command
+ message.args = arguments
+ message.parentMessageId = parentMessageId
+ message.extraData = try JSONEncoder.default.encode(extraData)
+ message.isSilent = isSilent
+ message.skipPush = false
+ message.skipEnrichUrl = false
+ message.reactionScores = [:]
+ message.reactionCounts = [:]
+ message.reactionGroups = []
+ message.mentionedUserIds = mentionedUserIds
+ message.showReplyInChannel = showReplyInChannel
+ message.quotedMessage = quotedMessageId.flatMap { MessageDTO.load(id: $0, context: self) }
+ message.user = currentUserDTO.user
+ message.channel = channelDTO
+ message.attachments = Set(
+ try attachments.enumerated().map { index, attachment in
+ let id = AttachmentId(cid: cid, messageId: message.id, index: index)
+ return try createNewAttachment(attachment: attachment, id: id)
+ }
+ )
+
+ if parentMessageId != nil {
+ message.type = MessageType.reply.rawValue
+ } else {
+ message.type = MessageType.regular.rawValue
+ }
+
+ if let threadId = parentMessageId {
+ let parentMessageDTO = self.message(id: threadId)
+ message.draftOfThread = parentMessageDTO
+ let threadDTO = thread(parentMessageId: threadId, cache: nil)
+ threadDTO?.parentMessageId = threadId
+ } else {
+ message.channel?.draftMessage = message
+ }
+
+ return message
+ }
+
+ /// Saves a message into the local DB.
+ /// - Parameters:
+ /// - payload: The message payload
+ /// - channelDTO: The channel dto.
+ /// - syncOwnReactions: Whether to sync own reactions. It should be set to `true` when the payload comes from an API response and `false` when the payload is received via WS events. For performance reasons the API
+ /// does not populate the `message.own_reactions` when sending events
+ /// - skipDraftUpdate: Whether to skip draft update. This is used when saving quoted and parent messages from
+ /// saveDraftMessage function to avoid an infinite loop since saving the draft would be called again.
+ /// - cache: The pre-warmed cache.
func saveMessage(
payload: MessagePayload,
channelDTO: ChannelDTO,
syncOwnReactions: Bool,
+ skipDraftUpdate: Bool = false,
cache: PreWarmedCache?
) throws -> MessageDTO {
let cid = try ChannelId(cid: channelDTO.cid)
@@ -813,12 +928,24 @@ extension NSManagedObjectContext: MessageDatabaseSession {
payload: quotedMessage,
channelDTO: channelDTO,
syncOwnReactions: false,
+ skipDraftUpdate: false,
cache: cache
)
} else {
dto.quotedMessage = nil
}
+ if let draft = payload.draft, skipDraftUpdate == false {
+ dto.draftReply = try saveDraftMessage(payload: draft, for: cid, cache: cache)
+ } else if skipDraftUpdate == false {
+ /// If the payload does not contain a draft reply, we should
+ /// delete the existing draft reply if it exists.
+ if let draft = dto.draftReply {
+ deleteDraftMessage(in: cid, threadId: draft.parentMessageId)
+ dto.draftReply = nil
+ }
+ }
+
let user = try saveUser(payload: payload.user)
dto.user = user
@@ -928,10 +1055,20 @@ extension NSManagedObjectContext: MessageDatabaseSession {
return dto
}
- func saveMessages(messagesPayload: MessageListPayload, for cid: ChannelId?, syncOwnReactions: Bool = true) -> [MessageDTO] {
+ func saveMessages(
+ messagesPayload: MessageListPayload,
+ for cid: ChannelId?,
+ syncOwnReactions: Bool = true
+ ) -> [MessageDTO] {
let cache = messagesPayload.getPayloadToModelIdMappings(context: self)
return messagesPayload.messages.compactMapLoggingError {
- try saveMessage(payload: $0, for: cid, syncOwnReactions: syncOwnReactions, cache: cache)
+ try saveMessage(
+ payload: $0,
+ for: cid,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: false,
+ cache: cache
+ )
}
}
@@ -939,6 +1076,7 @@ extension NSManagedObjectContext: MessageDatabaseSession {
payload: MessagePayload,
for cid: ChannelId?,
syncOwnReactions: Bool = true,
+ skipDraftUpdate: Bool = false,
cache: PreWarmedCache?
) throws -> MessageDTO {
guard payload.channel != nil || cid != nil else {
@@ -971,7 +1109,116 @@ extension NSManagedObjectContext: MessageDatabaseSession {
throw ClientError.MessagePayloadSavingFailure(description)
}
- return try saveMessage(payload: payload, channelDTO: channel, syncOwnReactions: syncOwnReactions, cache: cache)
+ return try saveMessage(
+ payload: payload,
+ channelDTO: channel,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: skipDraftUpdate,
+ cache: cache
+ )
+ }
+
+ @discardableResult
+ func saveDraftMessage(
+ payload: DraftPayload,
+ for cid: ChannelId,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO {
+ let draftDetailsPayload = payload.message
+ let channelDTO: ChannelDTO?
+ if let channelPayload = payload.channelPayload {
+ channelDTO = try saveChannel(payload: channelPayload, query: nil, cache: cache)
+ } else {
+ channelDTO = ChannelDTO.load(cid: cid, context: self)
+ }
+ guard let channelDTO = channelDTO else {
+ throw ClientError.ChannelDoesNotExist(cid: cid)
+ }
+ guard let user = currentUser?.user else {
+ throw ClientError.CurrentUserDoesNotExist()
+ }
+
+ // Delete existing draft message if it exists.
+ deleteDraftMessage(in: cid, threadId: payload.parentId)
+
+ let dto = MessageDTO.loadOrCreate(id: draftDetailsPayload.id, context: self, cache: cache)
+ dto.cid = cid.rawValue
+ dto.text = draftDetailsPayload.text
+ dto.createdAt = payload.createdAt.bridgeDate
+ dto.updatedAt = payload.createdAt.bridgeDate
+ dto.reactionScores = [:]
+ dto.reactionCounts = [:]
+ dto.type = MessageType.regular.rawValue
+ dto.command = draftDetailsPayload.command
+ dto.args = draftDetailsPayload.args
+ dto.parentMessageId = payload.parentId
+ dto.showReplyInChannel = draftDetailsPayload.showReplyInChannel
+ dto.isSilent = draftDetailsPayload.isSilent
+ dto.user = user
+ dto.channel = channelDTO
+ dto.isDraft = true
+
+ if let threadId = payload.parentId {
+ let threadDTO = thread(parentMessageId: threadId, cache: cache)
+ threadDTO?.parentMessageId = threadId
+ }
+
+ if let parentMessage = payload.parentMessage {
+ dto.parentMessage = try saveMessage(
+ payload: parentMessage,
+ channelDTO: channelDTO,
+ syncOwnReactions: false,
+ skipDraftUpdate: true,
+ cache: cache
+ )
+ dto.draftOfThread = dto.parentMessage
+ } else if let parentMessageId = payload.parentId,
+ let parentMessage = message(id: parentMessageId) {
+ dto.parentMessage = parentMessage
+ dto.draftOfThread = parentMessage
+ } else {
+ dto.parentMessage = nil
+ dto.draftOfThread = nil
+ channelDTO.draftMessage = dto
+ }
+
+ if let quotedMessage = payload.quotedMessage {
+ dto.quotedMessage = try saveMessage(
+ payload: quotedMessage,
+ channelDTO: channelDTO,
+ syncOwnReactions: false,
+ skipDraftUpdate: true,
+ cache: cache
+ )
+ } else {
+ dto.quotedMessage = nil
+ }
+
+ if let mentionedUsers = draftDetailsPayload.mentionedUsers {
+ dto.mentionedUsers = try Set(mentionedUsers.map { try saveUser(payload: $0) })
+ dto.mentionedUserIds = mentionedUsers.map(\.id)
+ }
+
+ if let attachments = draftDetailsPayload.attachments {
+ dto.attachments = Set(
+ try attachments.enumerated().map { index, attachment in
+ let id = AttachmentId(cid: cid, messageId: draftDetailsPayload.id, index: index)
+ return try saveAttachment(payload: attachment, id: id)
+ }
+ )
+ }
+
+ do {
+ dto.extraData = try JSONEncoder.default.encode(draftDetailsPayload.extraData)
+ } catch {
+ log.error(
+ "Failed to decode extra payload for Message with id: <\(dto.id)>, using default value instead. "
+ + "Error: \(error)"
+ )
+ dto.extraData = Data()
+ }
+
+ return dto
}
func saveMessage(payload: MessagePayload, for query: MessageSearchQuery, cache: PreWarmedCache?) throws -> MessageDTO {
@@ -980,6 +1227,21 @@ extension NSManagedObjectContext: MessageDatabaseSession {
return messageDTO
}
+ func deleteDraftMessage(in cid: ChannelId, threadId: MessageId?) {
+ if let threadId = threadId, let parentMessage = message(id: threadId) {
+ parentMessage.draftReply.map {
+ delete($0)
+ }
+ // Trigger thread update
+ let thread = thread(parentMessageId: threadId, cache: nil)
+ thread?.parentMessageId = threadId
+ } else if let channel = channel(cid: cid) {
+ channel.draftMessage.map {
+ delete($0)
+ }
+ }
+ }
+
func message(id: MessageId) -> MessageDTO? { .load(id: id, context: self) }
func messageExists(id: MessageId) -> Bool {
@@ -1301,6 +1563,11 @@ extension MessageDTO {
// At the moment, we only provide the type for system messages when creating a message.
let systemType = type == MessageType.system.rawValue ? type : nil
+
+ var restrictedVisibilityArray: [UserId]?
+ if let restrictedVisibility {
+ restrictedVisibilityArray = Array(restrictedVisibility)
+ }
return .init(
id: id,
@@ -1318,6 +1585,36 @@ extension MessageDTO {
pinned: pinned,
pinExpires: pinExpires?.bridgeDate,
pollId: poll?.id,
+ restrictedVisibility: restrictedVisibilityArray,
+ extraData: extraData
+ )
+ }
+
+ func asDraftRequestBody() -> DraftMessageRequestBody {
+ let extraData: [String: RawJSON]
+ do {
+ extraData = try JSONDecoder.stream.decodeRawJSON(from: self.extraData)
+ } catch {
+ log.assertionFailure("Failed decoding saved extra data with error: \(error). This should never happen because the extra data must be a valid JSON to be saved.")
+ extraData = [:]
+ }
+
+ let uploadedAttachments: [MessageAttachmentPayload] = attachments
+ .filter { $0.localState == .uploaded || $0.localState == nil }
+ .sorted { ($0.attachmentID?.index ?? 0) < ($1.attachmentID?.index ?? 0) }
+ .compactMap { $0.asRequestPayload() }
+
+ return .init(
+ id: id,
+ text: text,
+ command: command,
+ args: args,
+ parentId: parentMessageId,
+ showReplyInChannel: showReplyInChannel,
+ isSilent: isSilent,
+ quotedMessageId: quotedMessage?.id,
+ attachments: uploadedAttachments,
+ mentionedUserIds: mentionedUserIds,
extraData: extraData
)
}
@@ -1445,6 +1742,8 @@ private extension ChatMessage {
let quotedMessage = try? dto.quotedMessage?.relationshipAsModel(depth: depth)
+ let draftReply = try? dto.draftReply?.relationshipAsModel(depth: depth)
+
let readBy = Set(dto.reads.compactMap { try? $0.user.asModel() })
let message = ChatMessage(
@@ -1485,7 +1784,8 @@ private extension ChatMessage {
moderationDetails: moderationDetails,
readBy: readBy,
poll: poll,
- textUpdatedAt: textUpdatedAt
+ textUpdatedAt: textUpdatedAt,
+ draftReply: draftReply.map(DraftMessage.init)
)
if let transformer = chatClientConfig?.modelsTransformer {
diff --git a/Sources/StreamChat/Database/DTOs/ThreadDTO.swift b/Sources/StreamChat/Database/DTOs/ThreadDTO.swift
index e007c4e8bcd..caf27cf04f8 100644
--- a/Sources/StreamChat/Database/DTOs/ThreadDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ThreadDTO.swift
@@ -259,6 +259,17 @@ extension NSManagedObjectContext {
currentUserUnreadCount = currentUserRead?.unreadMessagesCount ?? 0
}
+ if let draft = payload.draft {
+ parentMessageDTO.draftReply = try saveDraftMessage(payload: draft, for: payload.channel.cid, cache: cache)
+ } else {
+ /// If the payload does not contain a draft reply, we should
+ /// delete the existing draft reply if it exists.
+ if let draft = parentMessageDTO.draftReply {
+ deleteDraftMessage(in: payload.channel.cid, threadId: draft.parentMessageId)
+ parentMessageDTO.draftReply = nil
+ }
+ }
+
threadDTO.fill(
parentMessage: parentMessageDTO,
title: payload.title,
diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift
index fe0579fa954..78206e75f91 100644
--- a/Sources/StreamChat/Database/DatabaseSession.swift
+++ b/Sources/StreamChat/Database/DatabaseSession.swift
@@ -104,6 +104,22 @@ protocol MessageDatabaseSession {
skipPush: Bool,
skipEnrichUrl: Bool,
poll: PollPayload?,
+ restrictedVisibility: [UserId],
+ extraData: [String: RawJSON]
+ ) throws -> MessageDTO
+
+ /// Creates a draft message in the database.
+ func createNewDraftMessage(
+ in cid: ChannelId,
+ text: String,
+ command: String?,
+ arguments: String?,
+ parentMessageId: MessageId?,
+ attachments: [AnyAttachmentPayload],
+ mentionedUserIds: [UserId],
+ showReplyInChannel: Bool,
+ isSilent: Bool,
+ quotedMessageId: MessageId?,
extraData: [String: RawJSON]
) throws -> MessageDTO
@@ -116,30 +132,48 @@ protocol MessageDatabaseSession {
@discardableResult
func saveMessages(messagesPayload: MessageListPayload, for cid: ChannelId?, syncOwnReactions: Bool) -> [MessageDTO]
- /// Saves the provided message payload to the DB. Return's the matching `MessageDTO` if the save was successful.
- /// Throws an error if the save fails.
- ///
- /// You must either provide `cid` or `payload.channel` value must not be `nil`.
- /// The `syncOwnReactions` should be set to `true` when the payload comes from an API response and `false` when the payload
- /// is received via WS events. For performance reasons the API does not populate the `message.own_reactions` when sending events
+ /// Saves a message into the local DB.
+ /// - Parameters:
+ /// - payload: The message payload
+ /// - cid: The channel ID.
+ /// - syncOwnReactions: Whether to sync own reactions. It should be set to `true` when the payload comes from an API response and `false` when the payload is received via WS events. For performance reasons the API
+ /// does not populate the `message.own_reactions` when sending events
+ /// - skipDraftUpdate: Whether to skip draft update. This is used when saving quoted and parent messages from
+ /// saveDraftMessage function to avoid an infinite loop since saving the draft would be called again.
+ /// - cache: The pre-warmed cache.
@discardableResult
func saveMessage(
payload: MessagePayload,
for cid: ChannelId?,
syncOwnReactions: Bool,
+ skipDraftUpdate: Bool,
cache: PreWarmedCache?
) throws -> MessageDTO
- /// Saves the provided message payload to the DB. Return's the matching `MessageDTO` if the save was successful.
+ /// Saves the provided draft message payload to the DB. Return's the matching `MessageDTO` if the save was successful.
/// Throws an error if the save fails.
- ///
- /// The `syncOwnReactions` should be set to `true` when the payload comes from an API response and `false` when the payload
- /// is received via WS events. For performance reasons the API does not populate the `message.own_reactions` when sending events
+ @discardableResult
+ func saveDraftMessage(
+ payload: DraftPayload,
+ for cid: ChannelId,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO
+
+ /// Saves a message into the local DB.
+ /// - Parameters:
+ /// - payload: The message payload
+ /// - channelDTO: The channel dto.
+ /// - syncOwnReactions: Whether to sync own reactions. It should be set to `true` when the payload comes from an API response and `false` when the payload is received via WS events. For performance reasons the API
+ /// does not populate the `message.own_reactions` when sending events
+ /// - skipDraftUpdate: Whether to skip draft update. This is used when saving quoted and parent messages from
+ /// saveDraftMessage function to avoid an infinite loop since saving the draft would be called again.
+ /// - cache: The pre-warmed cache.
@discardableResult
func saveMessage(
payload: MessagePayload,
channelDTO: ChannelDTO,
syncOwnReactions: Bool,
+ skipDraftUpdate: Bool,
cache: PreWarmedCache?
) throws -> MessageDTO
@@ -246,6 +280,7 @@ extension MessageDatabaseSession {
attachments: [AnyAttachmentPayload] = [],
mentionedUserIds: [UserId] = [],
pollPayload: PollPayload? = nil,
+ restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:]
) throws -> MessageDTO {
try createNewMessage(
@@ -266,6 +301,7 @@ extension MessageDatabaseSession {
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
poll: pollPayload,
+ restrictedVisibility: restrictedVisibility,
extraData: extraData
)
}
@@ -320,6 +356,9 @@ protocol ChannelDatabaseSession {
/// Removes a list of channels based on their id
func removeChannels(cids: Set)
+
+ /// Delete the draft message.
+ func deleteDraftMessage(in cid: ChannelId, threadId: MessageId?)
}
protocol ChannelReadDatabaseSession {
@@ -748,6 +787,7 @@ extension DatabaseSession {
payload: messagePayload,
channelDTO: channelDTO,
syncOwnReactions: false,
+ skipDraftUpdate: false,
cache: nil
)
diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
index 98617a53291..1a43f6d4654 100644
--- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
+++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
@@ -63,6 +63,7 @@
+
@@ -209,6 +210,7 @@
+
@@ -227,6 +229,7 @@
+
@@ -239,6 +242,9 @@
+
+
+
diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
index f2576a43223..01b7f83bd28 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.72.0"
+ public static let version: String = "4.73.0"
}
diff --git a/Sources/StreamChat/Info.plist b/Sources/StreamChat/Info.plist
index 52cf65ec6e6..8caf4c3312b 100644
--- a/Sources/StreamChat/Info.plist
+++ b/Sources/StreamChat/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.72.0
+ 4.73.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift
index cdd0c4430ab..c69bb793409 100644
--- a/Sources/StreamChat/Models/Channel.swift
+++ b/Sources/StreamChat/Models/Channel.swift
@@ -157,6 +157,9 @@ public struct ChatChannel {
/// because the preview message is the last `non-deleted` message sent to the channel.
public let previewMessage: ChatMessage?
+ /// The draft message in the channel.
+ public let draftMessage: DraftMessage?
+
// MARK: - Internal
var hasUnread: Bool {
@@ -194,7 +197,8 @@ public struct ChatChannel {
lastMessageFromCurrentUser: ChatMessage?,
pinnedMessages: [ChatMessage],
muteDetails: MuteDetails?,
- previewMessage: ChatMessage?
+ previewMessage: ChatMessage?,
+ draftMessage: DraftMessage?
) {
self.cid = cid
self.name = name
@@ -227,6 +231,7 @@ public struct ChatChannel {
self.pinnedMessages = pinnedMessages
self.muteDetails = muteDetails
self.previewMessage = previewMessage
+ self.draftMessage = draftMessage
}
/// Returns a new `ChatChannel` with the provided data replaced.
@@ -266,7 +271,8 @@ public struct ChatChannel {
lastMessageFromCurrentUser: lastMessageFromCurrentUser,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
- previewMessage: previewMessage
+ previewMessage: previewMessage,
+ draftMessage: draftMessage
)
}
}
@@ -315,6 +321,7 @@ extension ChatChannel: Hashable {
guard lhs.team == rhs.team else { return false }
guard lhs.truncatedAt == rhs.truncatedAt else { return false }
guard lhs.ownCapabilities == rhs.ownCapabilities else { return false }
+ guard lhs.draftMessage == rhs.draftMessage else { return false }
return true
}
diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift
index ee0e6cc1d82..f75be99d7f1 100644
--- a/Sources/StreamChat/Models/ChatMessage.swift
+++ b/Sources/StreamChat/Models/ChatMessage.swift
@@ -64,6 +64,9 @@ public struct ChatMessage {
public var quotedMessage: ChatMessage? { _quotedMessage() }
let _quotedMessage: () -> ChatMessage?
+ /// The draft reply to this message. Applies only for the messages of the current user.
+ public let draftReply: DraftMessage?
+
/// A flag indicating whether the message was bounced due to moderation.
public let isBounced: Bool
@@ -213,7 +216,8 @@ public struct ChatMessage {
moderationDetails: MessageModerationDetails?,
readBy: Set,
poll: Poll?,
- textUpdatedAt: Date?
+ textUpdatedAt: Date?,
+ draftReply: DraftMessage?
) {
self.id = id
self.cid = cid
@@ -254,6 +258,7 @@ public struct ChatMessage {
self.readBy = readBy
_attachments = attachments
_quotedMessage = { quotedMessage }
+ self.draftReply = draftReply
}
/// Returns a new `ChatMessage` with the provided data replaced.
@@ -300,7 +305,8 @@ public struct ChatMessage {
moderationDetails: moderationDetails,
readBy: readBy,
poll: poll,
- textUpdatedAt: textUpdatedAt
+ textUpdatedAt: textUpdatedAt,
+ draftReply: draftReply
)
}
}
@@ -431,6 +437,7 @@ extension ChatMessage: Hashable {
guard lhs.quotedMessage == rhs.quotedMessage else { return false }
guard lhs.translations == rhs.translations else { return false }
guard lhs.type == rhs.type else { return false }
+ guard lhs.draftReply == rhs.draftReply else { return false }
return true
}
diff --git a/Sources/StreamChat/Models/DraftMessage.swift b/Sources/StreamChat/Models/DraftMessage.swift
new file mode 100644
index 00000000000..8742794a357
--- /dev/null
+++ b/Sources/StreamChat/Models/DraftMessage.swift
@@ -0,0 +1,166 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+public struct DraftMessage {
+ /// A unique identifier of the message.
+ public let id: MessageId
+
+ /// The ChannelId this message belongs to.
+ public let cid: ChannelId?
+
+ /// The ID of the parent message, if the message is a reply.
+ public let threadId: MessageId?
+
+ /// The text of the message.
+ public let text: String
+
+ /// A flag indicating whether the message is a silent message.
+ ///
+ /// Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread.
+ public let isSilent: Bool
+
+ /// If the message was created by a specific `/` command, the command is saved in this variable.
+ public let command: String?
+
+ /// The date when the draft was created.
+ public let createdAt: Date
+
+ /// If the message was created by a specific `/` command, the arguments of the command are stored in this variable.
+ public let arguments: String?
+
+ /// If the message is a reply and this flag is `true`, the message should be also shown in the channel, not only in the
+ /// reply thread.
+ public let showReplyInChannel: Bool
+
+ /// Additional data associated with the message.
+ public let extraData: [String: RawJSON]
+
+ /// Quoted message.
+ ///
+ /// If message is inline reply this property will contain the message quoted by this reply.
+ public var quotedMessage: ChatMessage? { _quotedMessage() }
+ let _quotedMessage: () -> ChatMessage?
+
+ /// A list of users that are mentioned in this message.
+ public let mentionedUsers: Set
+
+ /// A list of attachments of the message.
+ public let attachments: [AnyChatMessageAttachment]
+
+ /// This property is used to make it easier to convert to a regular message.
+ internal let currentUser: ChatUser
+
+ init(
+ id: MessageId,
+ cid: ChannelId?,
+ threadId: MessageId?,
+ text: String,
+ isSilent: Bool,
+ command: String?,
+ createdAt: Date,
+ arguments: String?,
+ showReplyInChannel: Bool,
+ extraData: [String: RawJSON],
+ currentUser: ChatUser,
+ quotedMessage: @escaping () -> ChatMessage?,
+ mentionedUsers: Set,
+ attachments: [AnyChatMessageAttachment]
+ ) {
+ self.id = id
+ self.cid = cid
+ self.threadId = threadId
+ self.text = text
+ self.isSilent = isSilent
+ self.command = command
+ self.createdAt = createdAt
+ self.arguments = arguments
+ self.showReplyInChannel = showReplyInChannel
+ self.extraData = extraData
+ _quotedMessage = quotedMessage
+ self.mentionedUsers = mentionedUsers
+ self.attachments = attachments
+ self.currentUser = currentUser
+ }
+
+ init(_ message: ChatMessage) {
+ id = message.id
+ cid = message.cid
+ text = message.text
+ isSilent = message.isSilent
+ command = message.command
+ createdAt = message.createdAt
+ arguments = message.arguments
+ threadId = message.parentMessageId
+ showReplyInChannel = message.showReplyInChannel
+ extraData = message.extraData
+ _quotedMessage = { message.quotedMessage }
+ mentionedUsers = message.mentionedUsers
+ attachments = message.allAttachments
+ currentUser = message.author
+ }
+}
+
+extension DraftMessage: Equatable {
+ public static func == (lhs: DraftMessage, rhs: DraftMessage) -> Bool {
+ lhs.text == rhs.text
+ && lhs.id == rhs.id
+ && lhs.cid == rhs.cid
+ && lhs.isSilent == rhs.isSilent
+ && lhs.command == rhs.command
+ && lhs.createdAt == rhs.createdAt
+ && lhs.createdAt.timeIntervalSince1970 == rhs.createdAt.timeIntervalSince1970
+ && lhs.arguments == rhs.arguments
+ && lhs.threadId == rhs.threadId
+ && lhs.quotedMessage == rhs.quotedMessage
+ && lhs.attachments == rhs.attachments
+ }
+}
+
+extension ChatMessage {
+ /// Converts the draft message to a regular message so that it
+ /// can be easily used in existing UI components.
+ public init(_ draft: DraftMessage) {
+ id = draft.id
+ cid = draft.cid
+ text = draft.text
+ type = .regular
+ command = draft.command
+ createdAt = draft.createdAt
+ locallyCreatedAt = draft.createdAt
+ updatedAt = draft.createdAt
+ deletedAt = nil
+ arguments = draft.arguments
+ parentMessageId = draft.threadId
+ showReplyInChannel = draft.showReplyInChannel
+ replyCount = 0
+ extraData = draft.extraData
+ _quotedMessage = { draft.quotedMessage }
+ isBounced = false
+ isSilent = false
+ isShadowed = false
+ reactionScores = [:]
+ reactionCounts = [:]
+ reactionGroups = [:]
+ author = draft.currentUser
+ mentionedUsers = draft.mentionedUsers
+ threadParticipants = []
+ _attachments = draft.attachments
+ latestReplies = []
+ localState = nil
+ isFlaggedByCurrentUser = false
+ latestReactions = []
+ currentUserReactions = []
+ isSentByCurrentUser = true
+ pinDetails = nil
+ translations = nil
+ originalLanguage = nil
+ moderationDetails = nil
+ readBy = []
+ poll = nil
+ textUpdatedAt = nil
+ draftReply = nil
+ }
+}
diff --git a/Sources/StreamChat/Query/DraftListQuery.swift b/Sources/StreamChat/Query/DraftListQuery.swift
new file mode 100644
index 00000000000..1c2367f219f
--- /dev/null
+++ b/Sources/StreamChat/Query/DraftListQuery.swift
@@ -0,0 +1,60 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// A query used to fetch the drafts of the current user.
+public struct DraftListQuery: Encodable {
+ /// The pagination information to query the votes.
+ public var pagination: Pagination
+ /// The sorting parameter. By default drafts are sorted by newest first.
+ public var sorting: [Sorting]
+
+ public init(
+ pagination: Pagination = .init(pageSize: 25, offset: 0),
+ sorting: [Sorting] = [.init(key: .createdAt, isAscending: false)]
+ ) {
+ self.pagination = pagination
+ self.sorting = sorting
+ }
+
+ enum CodingKeys: CodingKey {
+ case pagination
+ case sort
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ if !sorting.isEmpty {
+ try container.encode(sorting, forKey: .sort)
+ }
+ try pagination.encode(to: encoder)
+ }
+}
+
+/// The type describing a value that can be used as a sorting when paginating all the drafts of the current user.
+public struct DraftListSortingKey: RawRepresentable, Hashable, SortingKey {
+ public let rawValue: String
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+}
+
+/// The supported sorting keys.
+public extension DraftListSortingKey {
+ /// Sorts drafts by `created_at` field.
+ static let createdAt = Self(rawValue: MessagePayloadsCodingKeys.createdAt.rawValue)
+}
+
+extension Sorting where Key == DraftListSortingKey {
+ func sortDescriptor() -> NSSortDescriptor? {
+ switch key {
+ case .createdAt:
+ return .init(keyPath: \MessageDTO.createdAt, ascending: isAscending)
+ default:
+ return nil
+ }
+ }
+}
diff --git a/Sources/StreamChat/Query/PollVoteListQuery.swift b/Sources/StreamChat/Query/PollVoteListQuery.swift
index 91d76298eee..7f7d75c0962 100644
--- a/Sources/StreamChat/Query/PollVoteListQuery.swift
+++ b/Sources/StreamChat/Query/PollVoteListQuery.swift
@@ -12,7 +12,7 @@ public struct PollVoteListQuery: Encodable {
public var optionId: String?
/// The pagination information to query the votes.
public var pagination: Pagination
- // The sorting parameter. By default votes are sorted by newest first.
+ /// The sorting parameter. By default votes are sorted by newest first.
public var sorting: [Sorting]
/// The filter details to query the votes.
public var filter: Filter?
diff --git a/Sources/StreamChat/Repositories/DraftMessagesRepository.swift b/Sources/StreamChat/Repositories/DraftMessagesRepository.swift
new file mode 100644
index 00000000000..a54bdf08f79
--- /dev/null
+++ b/Sources/StreamChat/Repositories/DraftMessagesRepository.swift
@@ -0,0 +1,160 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import CoreData
+
+struct DraftListResponse {
+ var drafts: [DraftMessage]
+ var next: String?
+}
+
+class DraftMessagesRepository {
+ private let database: DatabaseContainer
+ private let apiClient: APIClient
+
+ init(database: DatabaseContainer, apiClient: APIClient) {
+ self.database = database
+ self.apiClient = apiClient
+ }
+
+ func loadDrafts(
+ query: DraftListQuery,
+ completion: @escaping (Result) -> Void
+ ) {
+ apiClient.request(endpoint: .drafts(query: query)) { [weak self] result in
+ switch result {
+ case .success(let response):
+ var drafts: [DraftMessage] = []
+ self?.database.write({ session in
+ drafts = try response.drafts.compactMap {
+ guard let channelId = $0.channelPayload?.cid else {
+ return nil
+ }
+ return DraftMessage(try session
+ .saveDraftMessage(payload: $0, for: channelId, cache: nil)
+ .asModel())
+ }
+ }, completion: { error in
+ if let error {
+ completion(.failure(error))
+ return
+ }
+ completion(.success(DraftListResponse(drafts: drafts, next: response.next)))
+ })
+ case .failure(let error):
+ completion(.failure(error))
+ }
+ }
+ }
+
+ func updateDraft(
+ for cid: ChannelId,
+ threadId: MessageId?,
+ text: String,
+ isSilent: Bool,
+ showReplyInChannel: Bool,
+ command: String?,
+ arguments: String?,
+ attachments: [AnyAttachmentPayload],
+ mentionedUserIds: [UserId],
+ quotedMessageId: MessageId?,
+ extraData: [String: RawJSON],
+ completion: ((Result) -> Void)?
+ ) {
+ var draftRequestBody: DraftMessageRequestBody?
+ database.write({ (session) in
+ let newMessageDTO = try session.createNewDraftMessage(
+ in: cid,
+ text: text,
+ command: command,
+ arguments: arguments,
+ parentMessageId: threadId,
+ attachments: attachments,
+ mentionedUserIds: mentionedUserIds,
+ showReplyInChannel: showReplyInChannel,
+ isSilent: isSilent,
+ quotedMessageId: quotedMessageId,
+ extraData: extraData
+ )
+ draftRequestBody = newMessageDTO.asDraftRequestBody()
+ }) { error in
+ guard let requestBody = draftRequestBody, error == nil else {
+ completion?(.failure(error ?? ClientError.Unknown()))
+ return
+ }
+
+ self.apiClient.request(
+ endpoint: .updateDraftMessage(channelId: cid, requestBody: requestBody)
+ ) { [weak self] result in
+ switch result {
+ case .success(let response):
+ var draft: ChatMessage?
+ self?.database.write({ session in
+ let draftPayload = response.draft
+ let messageDTO = try session.saveDraftMessage(
+ payload: draftPayload,
+ for: cid,
+ cache: nil
+ )
+ draft = try messageDTO.asModel()
+ }, completion: { error in
+ if let draft {
+ completion?(.success(DraftMessage(draft)))
+ } else if let error {
+ completion?(.failure(error))
+ }
+ })
+ case .failure(let error):
+ completion?(.failure(error))
+ }
+ }
+ }
+ }
+
+ func getDraft(
+ for cid: ChannelId,
+ threadId: MessageId?,
+ completion: ((Result) -> Void)?
+ ) {
+ apiClient.request(
+ endpoint: .getDraftMessage(channelId: cid, threadId: threadId)
+ ) { [weak self] result in
+ switch result {
+ case .success(let response):
+ var draft: ChatMessage?
+ self?.database.write({ session in
+ let messageDTO = try session.saveDraftMessage(
+ payload: response.draft,
+ for: cid,
+ cache: nil
+ )
+ draft = try messageDTO.asModel()
+ }) { error in
+ if let draft {
+ completion?(.success(DraftMessage(draft)))
+ } else if let error {
+ completion?(.failure(error))
+ }
+ }
+ case .failure(let error):
+ completion?(.failure(error))
+ }
+ }
+ }
+
+ func deleteDraft(
+ for cid: ChannelId,
+ threadId: MessageId?,
+ completion: @escaping (Error?) -> Void
+ ) {
+ database.write { session in
+ session.deleteDraftMessage(in: cid, threadId: threadId)
+ }
+ apiClient.request(
+ endpoint: .deleteDraftMessage(channelId: cid, threadId: threadId)
+ ) { result in
+ completion(result.error)
+ }
+ }
+}
diff --git a/Sources/StreamChat/Repositories/MessageRepository.swift b/Sources/StreamChat/Repositories/MessageRepository.swift
index 3002062d983..80192a84684 100644
--- a/Sources/StreamChat/Repositories/MessageRepository.swift
+++ b/Sources/StreamChat/Repositories/MessageRepository.swift
@@ -137,7 +137,13 @@ class MessageRepository {
) {
var messageModel: ChatMessage!
database.write({
- let messageDTO = try $0.saveMessage(payload: message, for: cid, syncOwnReactions: false, cache: nil)
+ let messageDTO = try $0.saveMessage(
+ payload: message,
+ for: cid,
+ syncOwnReactions: false,
+ skipDraftUpdate: false,
+ cache: nil
+ )
if messageDTO.localMessageState == .sending || messageDTO.localMessageState == .sendingFailed {
messageDTO.markMessageAsSent()
}
@@ -210,6 +216,7 @@ class MessageRepository {
payload: message,
for: ChannelId(cid: cid),
syncOwnReactions: false,
+ skipDraftUpdate: false,
cache: nil
)
deletedMessage.localMessageState = nil
@@ -238,7 +245,13 @@ class MessageRepository {
case let .success(boxed):
var message: ChatMessage?
self.database.write({ session in
- message = try session.saveMessage(payload: boxed.message, for: cid, syncOwnReactions: true, cache: nil).asModel()
+ message = try session.saveMessage(
+ payload: boxed.message,
+ for: cid,
+ syncOwnReactions: true,
+ skipDraftUpdate: false,
+ cache: nil
+ ).asModel()
if !store {
// Force load attachments before discarding changes
_ = message?.attachmentCounts
diff --git a/Sources/StreamChat/StateLayer/Chat.swift b/Sources/StreamChat/StateLayer/Chat.swift
index 4be633ba652..efce5af5158 100644
--- a/Sources/StreamChat/StateLayer/Chat.swift
+++ b/Sources/StreamChat/StateLayer/Chat.swift
@@ -487,6 +487,7 @@ public class Chat {
/// - skipPushNotification: If true, skips sending push notification to channel members.
/// - skipEnrichURL: If true, the url preview won't be attached to the message.
/// - messageId: A custom id for the sent message. By default, it is automatically generated by Stream.
+ /// - restrictedVisibility: The list of user ids that should be able to see the message.
///
/// - Throws: An error while sending a message to the Stream API.
/// - Returns: An instance of `ChatMessage` which was delivered to the channel.
@@ -501,7 +502,8 @@ public class Chat {
silent: Bool = false,
skipPushNotification: Bool = false,
skipEnrichURL: Bool = false,
- messageId: MessageId? = nil
+ messageId: MessageId? = nil,
+ restrictedVisibility: [UserId] = []
) async throws -> ChatMessage {
Task { try await stopTyping() } // errors explicitly ignored
let localMessage = try await channelUpdater.createNewMessage(
@@ -518,6 +520,7 @@ public class Chat {
quotedMessageId: quotedMessageId,
skipPush: skipPushNotification,
skipEnrichUrl: skipEnrichURL,
+ restrictedVisibility: restrictedVisibility,
extraData: extraData
)
// Important to set up the waiter immediately
@@ -531,11 +534,13 @@ public class Chat {
/// - Parameters:
/// - text: Text of the message.
/// - messageId: A custom id for the sent message. By default, it is automatically generated by Stream.
+ /// - restrictedVisibility: The list of user ids that should be able to see the message.
/// - extraData: Additional extra data of the message object.
@discardableResult
public func sendSystemMessage(
with text: String,
messageId: MessageId? = nil,
+ restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:]
) async throws -> ChatMessage {
let localMessage = try await channelUpdater.createNewMessage(
@@ -552,6 +557,7 @@ public class Chat {
quotedMessageId: nil,
skipPush: false,
skipEnrichUrl: false,
+ restrictedVisibility: restrictedVisibility,
extraData: extraData
)
// Important to set up the waiter immediately
diff --git a/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift b/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift
index 0424ae13e12..77b3e29eaac 100644
--- a/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift
+++ b/Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift
@@ -179,7 +179,15 @@ class FetchCache {
extension NSManagedObjectContext {
func fetch(_ request: NSFetchRequest, using cache: FetchCache) throws -> [T] where T: NSFetchRequestResult {
- if let objectIds = cache.get(request), !objectIds.contains(where: { $0.isTemporaryID }) {
+ func canUseCachedIds(_ objectIds: [NSManagedObjectID]) -> Bool {
+ // Ignore cache when inserted (but not yet saved) object id is present
+ guard !objectIds.contains(where: { $0.isTemporaryID }) else { return false }
+ // Context has pending inserted or deleted objects of this type (can affect ids returned by the fetch request)
+ guard !insertedObjects.contains(where: { $0 is T }) && !deletedObjects.contains(where: { $0 is T }) else { return false }
+ return true
+ }
+
+ if let objectIds = cache.get(request), canUseCachedIds(objectIds) {
return try objectIds.compactMap { try existingObject(with: $0) as? T }
}
diff --git a/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift b/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift
index b450816ef3e..ffa403065e8 100644
--- a/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift
+++ b/Sources/StreamChat/Utils/SystemEnvironment+XStreamClient.swift
@@ -12,7 +12,7 @@ import IOKit
extension SystemEnvironment {
static let xStreamClientHeader: String = {
- "stream-chat-\(sdkIdentifier)-client-v\(version)|app=\(appName)|app_version=\(appVersion)|os=\(os) \(osVersion)|device_model=\(model)|device_screen_ratio=\(scale)"
+ "stream-chat-\(sdkIdentifier)-v\(version)|app=\(appName)|app_version=\(appVersion)|os=\(os) \(osVersion)|device_model=\(model)"
}()
private static var sdkIdentifier: String {
@@ -76,12 +76,4 @@ extension SystemEnvironment {
return "MacOS"
#endif
}
-
- private static var scale: String {
- #if os(iOS)
- return String(format: "%0.2f", UIScreen.main.scale)
- #elseif os(macOS)
- return "1.00"
- #endif
- }
}
diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/DraftUpdaterMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/DraftUpdaterMiddleware.swift
new file mode 100644
index 00000000000..797b1c80272
--- /dev/null
+++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/DraftUpdaterMiddleware.swift
@@ -0,0 +1,25 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+struct DraftUpdaterMiddleware: EventMiddleware {
+ func handle(event: Event, session: DatabaseSession) -> Event? {
+ switch event {
+ case let event as DraftUpdatedEventDTO:
+ guard let draft = event.payload.draft else { break }
+ do {
+ try session.saveDraftMessage(payload: draft, for: event.cid, cache: nil)
+ } catch {
+ log.error("Failed to save draft message: \(error)")
+ }
+ case let event as DraftDeletedEventDTO:
+ let threadId = event.payload.draft?.parentId
+ session.deleteDraftMessage(in: event.cid, threadId: threadId)
+ default:
+ break
+ }
+ return event
+ }
+}
diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ThreadUpdaterMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ThreadUpdaterMiddleware.swift
index a330e2bdcb2..157305c1f39 100644
--- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ThreadUpdaterMiddleware.swift
+++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ThreadUpdaterMiddleware.swift
@@ -62,6 +62,7 @@ struct ThreadUpdaterMiddleware: EventMiddleware {
payload: messagePayload,
channelDTO: channelDTO,
syncOwnReactions: false,
+ skipDraftUpdate: false,
cache: nil
)
else {
diff --git a/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift b/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift
new file mode 100644
index 00000000000..69263ba6087
--- /dev/null
+++ b/Sources/StreamChat/WebSocketClient/Events/DraftEvents.swift
@@ -0,0 +1,95 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// Triggered when a draft message is updated or created.
+public class DraftUpdatedEvent: Event {
+ /// The channel identifier of the draft.
+ public let cid: ChannelId
+
+ /// The channel object of the draft.
+ public let channel: ChatChannel
+
+ /// The draft message.
+ public let draftMessage: DraftMessage
+
+ /// The event timestamp.
+ public let createdAt: Date
+
+ init(cid: ChannelId, channel: ChatChannel, draftMessage: DraftMessage, createdAt: Date) {
+ self.cid = cid
+ self.channel = channel
+ self.draftMessage = draftMessage
+ self.createdAt = createdAt
+ }
+}
+
+class DraftUpdatedEventDTO: EventDTO {
+ let cid: ChannelId
+ let draft: DraftPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ cid = try response.value(at: \.cid)
+ draft = try response.value(at: \.draft)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: any DatabaseSession) -> Event? {
+ guard
+ let messageDTO = session.message(id: draft.message.id),
+ let channelDTO = session.channel(cid: cid) else {
+ return nil
+ }
+ return try? DraftUpdatedEvent(
+ cid: cid,
+ channel: channelDTO.asModel(),
+ draftMessage: DraftMessage(messageDTO.asModel()),
+ createdAt: createdAt
+ )
+ }
+}
+
+/// Triggered when a draft message is deleted.
+public class DraftDeletedEvent: Event {
+ /// The channel identifier of the draft.
+ public let cid: ChannelId
+
+ /// The thread identifier of the draft.
+ public let threadId: MessageId?
+
+ /// The event timestamp.
+ public let createdAt: Date
+
+ init(cid: ChannelId, threadId: MessageId?, createdAt: Date) {
+ self.cid = cid
+ self.threadId = threadId
+ self.createdAt = createdAt
+ }
+}
+
+class DraftDeletedEventDTO: EventDTO {
+ let cid: ChannelId
+ let draft: DraftPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ cid = try response.value(at: \.cid)
+ draft = try response.value(at: \.draft)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: any DatabaseSession) -> Event? {
+ DraftDeletedEvent(
+ cid: cid,
+ threadId: draft.parentId,
+ createdAt: createdAt
+ )
+ }
+}
diff --git a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
index 3700a3506a6..c760f6358d4 100644
--- a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
@@ -39,6 +39,7 @@ class EventPayload: Decodable {
case aiState = "ai_state"
case messageId = "message_id"
case aiMessage = "ai_message"
+ case draft
}
let eventType: EventType
@@ -75,6 +76,7 @@ class EventPayload: Decodable {
let aiState: String?
let messageId: String?
let aiMessage: String?
+ let draft: DraftPayload?
init(
eventType: EventType,
@@ -106,7 +108,8 @@ class EventPayload: Decodable {
vote: PollVotePayload? = nil,
aiState: String? = nil,
messageId: String? = nil,
- aiMessage: String? = nil
+ aiMessage: String? = nil,
+ draft: DraftPayload? = nil
) {
self.eventType = eventType
self.connectionId = connectionId
@@ -138,6 +141,7 @@ class EventPayload: Decodable {
self.aiState = aiState
self.messageId = messageId
self.aiMessage = aiMessage
+ self.draft = draft
}
required init(from decoder: Decoder) throws {
@@ -174,6 +178,7 @@ class EventPayload: Decodable {
aiState = try container.decodeIfPresent(String.self, forKey: .aiState)
messageId = try container.decodeIfPresent(String.self, forKey: .messageId)
aiMessage = try container.decodeIfPresent(String.self, forKey: .aiMessage)
+ draft = try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
}
func event() throws -> Event {
diff --git a/Sources/StreamChat/WebSocketClient/Events/EventType.swift b/Sources/StreamChat/WebSocketClient/Events/EventType.swift
index a00676b38d4..1268e2b955e 100644
--- a/Sources/StreamChat/WebSocketClient/Events/EventType.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/EventType.swift
@@ -154,6 +154,14 @@ public extension EventType {
// When an AI typing indicator has been stopped.
static let aiTypingIndicatorStop: Self = "ai_indicator.stop"
+
+ // MARK: Drafts
+
+ /// When a draft was updated.
+ static let draftUpdated: Self = "draft.updated"
+
+ /// When a draft was deleted.
+ static let draftDeleted: Self = "draft.deleted"
}
extension EventType {
@@ -222,6 +230,8 @@ extension EventType {
case .aiTypingIndicatorChanged: return try AIIndicatorUpdateEventDTO(from: response)
case .aiTypingIndicatorClear: return try AIIndicatorClearEventDTO(from: response)
case .aiTypingIndicatorStop: return try AIIndicatorStopEventDTO(from: response)
+ case .draftUpdated: return try DraftUpdatedEventDTO(from: response)
+ case .draftDeleted: return try DraftDeletedEventDTO(from: response)
default:
if response.cid == nil {
throw ClientError.UnknownUserEvent(response.eventType)
diff --git a/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift b/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift
index 3eb9225ac05..bb85df84b8d 100644
--- a/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift
@@ -293,7 +293,8 @@ private extension MessagePayload {
moderationDetails: nil,
readBy: [],
poll: nil,
- textUpdatedAt: messageTextUpdatedAt
+ textUpdatedAt: messageTextUpdatedAt,
+ draftReply: nil
)
}
}
diff --git a/Sources/StreamChat/Workers/ChannelUpdater.swift b/Sources/StreamChat/Workers/ChannelUpdater.swift
index 37a02fc32df..fe62d9da076 100644
--- a/Sources/StreamChat/Workers/ChannelUpdater.swift
+++ b/Sources/StreamChat/Workers/ChannelUpdater.swift
@@ -308,6 +308,7 @@ class ChannelUpdater: Worker {
quotedMessageId: MessageId?,
skipPush: Bool,
skipEnrichUrl: Bool,
+ restrictedVisibility: [UserId] = [],
poll: PollPayload? = nil,
extraData: [String: RawJSON],
completion: ((Result) -> Void)? = nil
@@ -332,6 +333,7 @@ class ChannelUpdater: Worker {
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
poll: poll,
+ restrictedVisibility: restrictedVisibility,
extraData: extraData
)
if quotedMessageId != nil {
@@ -743,6 +745,7 @@ extension ChannelUpdater {
quotedMessageId: MessageId?,
skipPush: Bool,
skipEnrichUrl: Bool,
+ restrictedVisibility: [UserId],
extraData: [String: RawJSON]
) async throws -> ChatMessage {
try await withCheckedThrowingContinuation { continuation in
@@ -760,6 +763,7 @@ extension ChannelUpdater {
quotedMessageId: quotedMessageId,
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
+ restrictedVisibility: restrictedVisibility,
extraData: extraData
) { result in
continuation.resume(with: result)
diff --git a/Sources/StreamChat/Workers/MessageUpdater.swift b/Sources/StreamChat/Workers/MessageUpdater.swift
index 986017a474b..cc38c233cd0 100644
--- a/Sources/StreamChat/Workers/MessageUpdater.swift
+++ b/Sources/StreamChat/Workers/MessageUpdater.swift
@@ -236,6 +236,7 @@ class MessageUpdater: Worker {
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
poll: nil,
+ restrictedVisibility: [],
extraData: extraData
)
@@ -764,7 +765,13 @@ class MessageUpdater: Worker {
switch $0 {
case let .success(payload):
self.database.write({ session in
- try session.saveMessage(payload: payload.message, for: cid, syncOwnReactions: true, cache: nil)
+ try session.saveMessage(
+ payload: payload.message,
+ for: cid,
+ syncOwnReactions: true,
+ skipDraftUpdate: true,
+ cache: nil
+ )
}, completion: { error in
completion?(error)
})
@@ -824,6 +831,7 @@ class MessageUpdater: Worker {
payload: boxedMessage.message,
for: boxedMessage.message.cid,
syncOwnReactions: false,
+ skipDraftUpdate: true,
cache: nil
)
if completion != nil {
diff --git a/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift b/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
index e50c0de7c0e..ecda5d0d85c 100644
--- a/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
+++ b/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
@@ -226,6 +226,14 @@ open class ChatChannelVC: _ViewController,
}
}
+ override open func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ if let draftMessage = channelController.channel?.draftMessage {
+ messageComposerVC.content.draftMessage(draftMessage)
+ }
+ }
+
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
@@ -573,6 +581,12 @@ open class ChatChannelVC: _ViewController,
}
debugPrint("New Message Error: \(error) Message: \(message)")
}
+
+ if let draftUpdatedEvent = event as? DraftUpdatedEvent,
+ let draft = channelController.channel?.draftMessage,
+ draftUpdatedEvent.cid == channelController.cid {
+ messageComposerVC.content.draftMessage(draft)
+ }
}
// MARK: - AudioQueuePlayerDatasource
diff --git a/Sources/StreamChatUI/ChatChannelList/ChatChannelListItemView.swift b/Sources/StreamChatUI/ChatChannelList/ChatChannelListItemView.swift
index 7417748ccae..215f236c801 100644
--- a/Sources/StreamChatUI/ChatChannelList/ChatChannelListItemView.swift
+++ b/Sources/StreamChatUI/ChatChannelList/ChatChannelListItemView.swift
@@ -124,6 +124,11 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
.withoutAutoresizingMaskConstraints
.withAccessibilityIdentifier(identifier: "unreadCountView")
+ /// The prefix text for the draft message.
+ open var draftPrefixText: String {
+ "\(L10n.Message.Preview.draft):"
+ }
+
/// Text of `titleLabel` which contains the channel name.
open var titleText: String? {
if let searchedMessage = content?.searchedMessage {
@@ -149,38 +154,34 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
return typingUsersInfo
}
- if let previewMessage = content.channel.previewMessage {
- if let pollPreviewText = pollAttachmentPreviewText(for: previewMessage) {
- return pollPreviewText
- }
-
- if isLastMessageVoiceRecording {
- return previewMessageForAudioRecordingMessage(messageText: previewMessage.text)
- }
+ if let dratMessage = content.channel.draftMessage.map(ChatMessage.init), components.isDraftMessagesEnabled {
+ let previewText = previewMessageContextText(previewMessage: dratMessage)
+ return previewMessageTextForDraft(messageText: previewText)
+ }
+ if let previewMessage = content.channel.previewMessage {
if previewMessage.type == .system {
return previewMessageTextForSystemMessage(messageText: previewMessage.text)
}
- var text = previewMessage.textContent ?? previewMessage.text
-
- if let translatedText = translatedPreviewText(for: previewMessage, messageText: text) {
- text = translatedText
+ if let pollPreviewText = pollAttachmentPreviewText(for: previewMessage) {
+ return pollPreviewText
}
- if let attachmentText = attachmentPreviewText(for: previewMessage, messageText: text) {
- text = attachmentText
+ let previewText = previewMessageContextText(previewMessage: previewMessage)
+ if isLastMessageVoiceRecording {
+ return previewText
}
if previewMessage.isSentByCurrentUser {
- return previewMessageTextForCurrentUser(messageText: text)
+ return previewMessageTextForCurrentUser(messageText: previewText)
}
if content.channel.memberCount == 2 {
- return previewMessageTextFor1on1Channel(messageText: text)
+ return previewMessageTextFor1on1Channel(messageText: previewText)
}
- return previewMessageTextFromAnotherUser(previewMessage.author, messageText: text)
+ return previewMessageTextFromAnotherUser(previewMessage.author, messageText: previewText)
}
return previewMessageTextForEmptyMessage()
@@ -353,6 +354,14 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
at: status == .pending || status == .failed ? 0 : 1
)
}
+
+ if content?.channel.draftMessage != nil {
+ let highlightOptions = TextHighlightOptions(
+ color: appearance.colorPalette.accentPrimary,
+ font: appearance.fonts.footnoteBold
+ )
+ subtitleLabel.highlightText(draftPrefixText, options: highlightOptions)
+ }
}
// MARK: - Channel title rendering
@@ -375,6 +384,26 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
// MARK: - Preview message text rendering
+ /// The message preview text based solely on the message content.
+ /// It does not account if it is sent by the current user or it is a draft etc.
+ ///
+ /// - Parameter previewMessage: The preview message of the channel.
+ /// - Returns: A string representing the message preview text.
+ open func previewMessageContextText(previewMessage: ChatMessage) -> String {
+ if previewMessage.voiceRecordingAttachments.isEmpty == false {
+ return previewMessageForAudioRecordingMessage(messageText: previewMessage.text)
+ }
+
+ var text = previewMessage.textContent ?? previewMessage.text
+ if let translatedText = translatedPreviewText(for: previewMessage, messageText: text) {
+ text = translatedText
+ }
+ if let attachmentText = attachmentPreviewText(for: previewMessage, messageText: text) {
+ text = attachmentText
+ }
+ return text
+ }
+
/// The message preview text in case the message is empty.
/// - Returns: A string representing the message preview text.
open func previewMessageTextForEmptyMessage() -> String {
@@ -416,6 +445,13 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
messageText
}
+ /// The message preview text in case the message is a draft.
+ /// - Parameter messageText: The current text of the message.
+ /// - Returns: A string representing the message preview text.
+ open func previewMessageTextForDraft(messageText: String) -> String {
+ "\(draftPrefixText) \(messageText)"
+ }
+
/// The message preview text in case the message is from another user and it is not a 1on1 channel.
/// - Parameter messageText: The current text of the message.
/// - Returns: A string representing the message preview text.
@@ -516,6 +552,10 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
extension ChatChannelListItemView {
var isLastMessageVoiceRecording: Bool {
- content?.channel.previewMessage?.voiceRecordingAttachments.isEmpty == false && typingUserString == nil
+ let previewMessage = content?.channel.previewMessage
+ let previewHasVoiceRecording = previewMessage?.voiceRecordingAttachments.isEmpty == false
+ let doesNotHaveDraft = content?.channel.draftMessage == nil
+ let noTypingUsers = typingUserString == nil
+ return previewHasVoiceRecording && noTypingUsers && doesNotHaveDraft
}
}
diff --git a/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift b/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift
index 54230124fc7..f76727b92ac 100644
--- a/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift
+++ b/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift
@@ -175,6 +175,14 @@ open class ChatThreadVC: _ViewController,
navigationItem.largeTitleDisplayMode = .never
}
+ override open func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ if let draftMessage = messageController.message?.draftReply {
+ messageComposerVC.content.draftMessage(draftMessage)
+ }
+ }
+
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@@ -481,6 +489,10 @@ open class ChatThreadVC: _ViewController,
if !isFirstPageLoaded && newMessage.isSentByCurrentUser && newMessage.isPartOfThread {
messageController.loadFirstPage()
}
+ case let event as DraftUpdatedEvent where event.draftMessage.threadId == messageController.messageId:
+ if let draft = messageController.message?.draftReply {
+ messageComposerVC.content.draftMessage(draft)
+ }
default:
break
}
diff --git a/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift b/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift
index be04307a3aa..32d4737f594 100644
--- a/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift
+++ b/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift
@@ -252,6 +252,14 @@ open class ChatThreadListItemView: _View, ThemeProvider {
replyAuthorAvatarView.content = latestReply?.author
replyTitleLabel.text = latestReply?.author.name
threadUnreadCountView.content = unreadReplies
+
+ if thread.parentMessage.draftReply != nil {
+ let highlightOptions = TextHighlightOptions(
+ color: appearance.colorPalette.accentPrimary,
+ font: appearance.fonts.footnoteBold
+ )
+ replyDescriptionLabel.highlightText(draftPrefixText, options: highlightOptions)
+ }
}
/// The timestamp text formatted.
@@ -294,18 +302,33 @@ open class ChatThreadListItemView: _View, ThemeProvider {
/// The reply preview text.
open var replyPreviewText: String? {
// TODO: On v5 the logic in ChatChannelItemView.subtitleText should be extracted to `Appearance.formatters` and shared with the `ChatThreadListItemView`
+
+ if let draftReply = content?.thread.parentMessage.draftReply.map(ChatMessage.init), components.isDraftMessagesEnabled {
+ let previewText = previewText(for: draftReply)
+ return "\(draftPrefixText) \(previewText)"
+ }
+
guard let latestReply = content?.thread.latestReplies.last else {
return nil
}
-
- if latestReply.text.isEmpty {
- return latestReply.allAttachments.first?.type.rawValue
+
+ return previewText(for: latestReply)
+ }
+
+ /// The prefix text for the draft message.
+ open var draftPrefixText: String {
+ "\(L10n.Message.Preview.draft):"
+ }
+
+ private func previewText(for message: ChatMessage) -> String {
+ if message.text.isEmpty {
+ return message.allAttachments.first?.type.rawValue.capitalized ?? ""
}
- if latestReply.isDeleted {
+ if message.isDeleted {
return L10n.Message.Item.deleted
}
- return latestReply.text
+ return message.text
}
}
diff --git a/Sources/StreamChatUI/Components.swift b/Sources/StreamChatUI/Components.swift
index 17b0d86021d..b8c75ef75ea 100644
--- a/Sources/StreamChatUI/Components.swift
+++ b/Sources/StreamChatUI/Components.swift
@@ -43,6 +43,11 @@ public struct Components {
/// A boolean value that determines whether the link preview should show when typing a message with links.
public var isComposerLinkPreviewEnabled = false
+ /// A boolean value indicating if draft messages should be enabled.
+ ///
+ /// If enabled, the SDK will save the message content as a draft when the user navigates away from the composer.
+ public var isDraftMessagesEnabled: Bool = false
+
/// A view that displays a quoted message.
public var quotedMessageView: QuotedChatMessageView.Type = QuotedChatMessageView.self
diff --git a/Sources/StreamChatUI/Composer/ComposerVC.swift b/Sources/StreamChatUI/Composer/ComposerVC.swift
index af61f979eb3..90fb63869db 100644
--- a/Sources/StreamChatUI/Composer/ComposerVC.swift
+++ b/Sources/StreamChatUI/Composer/ComposerVC.swift
@@ -79,6 +79,9 @@ open class ComposerVC: _ViewController,
public var cooldownTime: Int
/// A boolean value indicating if the message url enrichment should be skipped.
public var skipEnrichUrl: Bool
+ /// A boolean value indicating if the message should be shown in the channel.
+ /// If the provided value is nil, it won't change the current checkbox state.
+ public var showReplyInChannel: Bool?
/// A boolean that checks if the message contains any content.
public var isEmpty: Bool {
@@ -90,6 +93,14 @@ open class ComposerVC: _ViewController,
return text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && attachments.isEmpty
}
+ /// The text that should be sent to the backend considering the command.
+ public var inputText: String {
+ if let command = command {
+ return "/\(command.name) " + text
+ }
+ return text.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+
/// A boolean that checks if the composer is replying in a thread
public var isInsideThread: Bool { threadMessage != nil }
/// A boolean that checks if the composer recognised already a command.
@@ -123,7 +134,8 @@ open class ComposerVC: _ViewController,
command: Command?,
extraData: [String: RawJSON] = [:],
cooldownTime: Int = 0,
- skipEnrichUrl: Bool = false
+ skipEnrichUrl: Bool = false,
+ showReplyInChannel: Bool? = nil
) {
self.text = text
self.state = state
@@ -136,6 +148,7 @@ open class ComposerVC: _ViewController,
self.extraData = extraData
self.cooldownTime = cooldownTime
self.skipEnrichUrl = skipEnrichUrl
+ self.showReplyInChannel = showReplyInChannel
}
/// Creates a new content struct with all empty data.
@@ -172,6 +185,26 @@ open class ComposerVC: _ViewController,
)
}
+ /// Sets the content state to new message from a saved draft.
+ ///
+ /// - Parameter message: The message which was saved as a draft.
+ public mutating func draftMessage(_ message: DraftMessage) {
+ self = .init(
+ text: message.text,
+ state: message.quotedMessage != nil ? .quote : .new,
+ editingMessage: nil,
+ quotingMessage: message.quotedMessage,
+ threadMessage: threadMessage,
+ attachments: message.attachments.toAnyAttachmentPayload(),
+ mentionedUsers: message.mentionedUsers,
+ command: message.command.map { Command(name: $0, args: message.text) },
+ extraData: message.extraData,
+ cooldownTime: cooldownTime,
+ skipEnrichUrl: skipEnrichUrl,
+ showReplyInChannel: message.showReplyInChannel
+ )
+ }
+
/// Sets the content state to editing a message.
///
/// - Parameter message: The message that the composer will edit.
@@ -484,6 +517,12 @@ open class ComposerVC: _ViewController,
composerView.pin(to: view)
}
+ override open func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+
+ updateDraftMessageIfNeeded()
+ }
+
open func setupAttachmentsView() {
addChildViewController(attachmentsVC, embedIn: composerView.inputMessageView.attachmentsViewContainer)
attachmentsVC.didTapRemoveItemButton = { [weak self] index in
@@ -684,6 +723,10 @@ open class ComposerVC: _ViewController,
} else {
composerView.checkboxControl.label.text = L10n.Composer.Checkmark.channelReply
}
+
+ if let showReplyInChannel = content.showReplyInChannel {
+ composerView.checkboxControl.isSelected = showReplyInChannel
+ }
}
}
@@ -773,12 +816,9 @@ open class ComposerVC: _ViewController,
return
}
- let text: String
- if let command = content.command {
- text = "/\(command.name) " + content.text
- } else {
- text = content.text.trimmingCharacters(in: .whitespacesAndNewlines)
- }
+ deleteDraftMessageIfNeeded()
+
+ let text = content.inputText
if let editingMessage = content.editingMessage {
editMessage(withId: editingMessage.id, newText: text)
@@ -913,14 +953,7 @@ open class ComposerVC: _ViewController,
open func createNewMessage(text: String) {
guard let cid = channelController?.cid else { return }
- // If the user included some mentions via suggestions,
- // but then removed them from text, we should remove them from
- // the content we'll send
- for user in content.mentionedUsers {
- if !text.contains(mentionText(for: user)) {
- content.mentionedUsers.remove(user)
- }
- }
+ removeMentionUserIfNotIncluded(in: text)
if let threadParentMessageId = content.threadMessage?.id {
let messageController = channelController?.client.messageController(
@@ -952,6 +985,67 @@ open class ComposerVC: _ViewController,
)
}
+ /// Updates the draft message in the channel or thread if draft messages are enabled.
+ ///
+ /// The draft is created from the current content in the composer.
+ private func updateDraftMessageIfNeeded() {
+ guard components.isDraftMessagesEnabled else {
+ return
+ }
+
+ let text = content.command != nil ? content.text : content.inputText
+ if content.isEmpty {
+ return
+ }
+
+ removeMentionUserIfNotIncluded(in: text)
+
+ if let threadParentMessageId = content.threadMessage?.id, let cid = channelController?.cid {
+ let messageController = channelController?.client.messageController(
+ cid: cid,
+ messageId: threadParentMessageId
+ )
+
+ messageController?.updateDraftReply(
+ text: text,
+ attachments: content.attachments,
+ mentionedUserIds: content.mentionedUsers.map(\.id),
+ quotedMessageId: content.quotingMessage?.id,
+ showReplyInChannel: composerView.checkboxControl.isSelected,
+ command: content.command,
+ extraData: content.extraData
+ )
+ return
+ }
+
+ channelController?.updateDraftMessage(
+ text: text,
+ attachments: content.attachments,
+ mentionedUserIds: content.mentionedUsers.map(\.id),
+ quotedMessageId: content.quotingMessage?.id,
+ command: content.command,
+ extraData: content.extraData
+ )
+ }
+
+ /// Deletes the draft message in the channel or thread if there is one.
+ private func deleteDraftMessageIfNeeded() {
+ if let threadParentMessageId = content.threadMessage?.id, let cid = channelController?.cid {
+ let messageController = channelController?.client.messageController(
+ cid: cid,
+ messageId: threadParentMessageId
+ )
+ if messageController?.message?.draftReply != nil {
+ messageController?.deleteDraftReply()
+ }
+ return
+ }
+
+ if channelController?.channel?.draftMessage != nil {
+ channelController?.deleteDraftMessage()
+ }
+ }
+
/// Updates an existing message.
/// - Parameters:
/// - id: The id of the editing message.
@@ -1450,6 +1544,11 @@ open class ComposerVC: _ViewController,
guard textView.text != content.text else { return }
content.text = textView.text
+
+ /// If the input message was erased and there is a draft message, the draft message should be deleted.
+ if content.isEmpty {
+ deleteDraftMessageIfNeeded()
+ }
}
open func textView(
@@ -1663,6 +1762,17 @@ open class ComposerVC: _ViewController,
present(alert, animated: true)
}
+
+ private func removeMentionUserIfNotIncluded(in currentText: String) {
+ // If the user included some mentions via suggestions,
+ // but then removed them from text, we should remove them from
+ // the content we'll send
+ for user in content.mentionedUsers {
+ if !currentText.contains(mentionText(for: user)) {
+ content.mentionedUsers.remove(user)
+ }
+ }
+ }
}
/// searchUsers does an autocomplete search on a list of ChatUser and returns users with `id` or `name` containing the search string
diff --git a/Sources/StreamChatUI/Generated/L10n.swift b/Sources/StreamChatUI/Generated/L10n.swift
index 6e476ad05ac..cd80a31b8dc 100644
--- a/Sources/StreamChatUI/Generated/L10n.swift
+++ b/Sources/StreamChatUI/Generated/L10n.swift
@@ -296,6 +296,8 @@ internal enum L10n {
internal static var title: String { L10n.tr("Localizable", "message.moderation.title") }
}
internal enum Preview {
+ /// Draft
+ internal static var draft: String { L10n.tr("Localizable", "message.preview.draft") }
/// %@ created:
internal static func pollSomeoneCreated(_ p1: Any) -> String {
return L10n.tr("Localizable", "message.preview.poll-someone-created", String(describing: p1))
diff --git a/Sources/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist
index 52cf65ec6e6..8caf4c3312b 100644
--- a/Sources/StreamChatUI/Info.plist
+++ b/Sources/StreamChatUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.72.0
+ 4.73.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/Sources/StreamChatUI/Resources/en.lproj/Localizable.strings b/Sources/StreamChatUI/Resources/en.lproj/Localizable.strings
index 3d41b3249d8..9e0b35d1254 100644
--- a/Sources/StreamChatUI/Resources/en.lproj/Localizable.strings
+++ b/Sources/StreamChatUI/Resources/en.lproj/Localizable.strings
@@ -306,6 +306,9 @@
/// Shown in the channel list or thread list message preview in case the current user created a poll.
"message.preview.poll-you-created" = "You created:";
+/// Shown in the channel list or thread list message preview in case there is a draft.
+"message.preview.draft" = "Draft";
+
/// Alert title when closing a poll.
"alert.poll.end-title" = "Nobody will be able to vote in this poll anymore.";
/// Alert title when adding a comment to a poll.
diff --git a/Sources/StreamChatUI/Utils/UILabel+highlightText.swift b/Sources/StreamChatUI/Utils/UILabel+highlightText.swift
new file mode 100644
index 00000000000..a78d1e0b6a4
--- /dev/null
+++ b/Sources/StreamChatUI/Utils/UILabel+highlightText.swift
@@ -0,0 +1,41 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import UIKit
+
+struct TextHighlightOptions {
+ var color: UIColor
+ var font: UIFont?
+}
+
+extension UILabel {
+ func highlightText(_ text: String, options: TextHighlightOptions) {
+ let currentText = self.text ?? ""
+ guard text.isEmpty == false && currentText.isEmpty == false else {
+ return
+ }
+ let currentTextString = currentText as NSString
+ let fullRange = NSRange(location: 0, length: currentTextString.length)
+
+ let attributedString = NSMutableAttributedString(string: currentText)
+ if let currentFont = font {
+ attributedString.addAttribute(.font, value: currentFont, range: fullRange)
+ }
+
+ let highlightRange = currentTextString.range(of: text)
+ attributedString.addAttribute(
+ .foregroundColor,
+ value: options.color,
+ range: highlightRange
+ )
+ if let font = options.font {
+ attributedString.addAttribute(
+ .font,
+ value: font,
+ range: highlightRange
+ )
+ }
+ attributedText = attributedString
+ }
+}
diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec
index 81feccf3138..cf8f9a3d8cd 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.72.0"
+ spec.version = "4.73.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 e5d00a80524..2f19de80b3f 100644
--- a/StreamChat.podspec
+++ b/StreamChat.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChat"
- spec.version = "4.72.0"
+ spec.version = "4.73.0"
spec.summary = "StreamChat iOS Chat Client"
spec.description = "stream-chat-swift is the official Swift client for Stream Chat, a service for building chat applications."
diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj
index b34f37d920d..0f721f59b4c 100644
--- a/StreamChat.xcodeproj/project.pbxproj
+++ b/StreamChat.xcodeproj/project.pbxproj
@@ -228,10 +228,6 @@
437FCA1626D79A910000223C /* ChatRemoteNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437FCA1526D79A910000223C /* ChatRemoteNotificationHandler.swift */; };
437FCA1926D906B20000223C /* ChatPushNotificationContent_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437FCA1826D906B20000223C /* ChatPushNotificationContent_Tests.swift */; };
43ABF8B526C2CD900034BD62 /* ComposerVC_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ABF8B326C2B7140034BD62 /* ComposerVC_Tests.swift */; };
- 43D3F0F62841052600B74921 /* CallPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D3F0F52841052600B74921 /* CallPayloads.swift */; };
- 43D3F0F72841052600B74921 /* CallPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D3F0F52841052600B74921 /* CallPayloads.swift */; };
- 43D3F0F9284106EE00B74921 /* CallEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D3F0F8284106EE00B74921 /* CallEndpoints.swift */; };
- 43D3F0FA284106EE00B74921 /* CallEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D3F0F8284106EE00B74921 /* CallEndpoints.swift */; };
43D3F0FC28410A0200B74921 /* CreateCallRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D3F0FB28410A0200B74921 /* CreateCallRequestBody.swift */; };
43D3F0FD28410A0200B74921 /* CreateCallRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D3F0FB28410A0200B74921 /* CreateCallRequestBody.swift */; };
43EB3AE22671718200954323 /* AttachmentViewCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EB3AE12671718200954323 /* AttachmentViewCatalog.swift */; };
@@ -1338,8 +1334,6 @@
A3F65E3827EB716A003F6256 /* TypingStartCleanupMiddleware_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A0E9B82498C31300E9BD50 /* TypingStartCleanupMiddleware_Tests.swift */; };
A3F65E3A27EB72F6003F6256 /* Event+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D15D9127EA0125006B34D7 /* Event+Equatable.swift */; };
AC1E16FF269C70530040548B /* String+Extensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDB5412269C6F2A007CD465 /* String+Extensions_Tests.swift */; };
- AC82033A28C60B100002EFDD /* CallEndpoints_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC82033928C60B100002EFDD /* CallEndpoints_Tests.swift */; };
- AC82033C28C61F640002EFDD /* CreateCallPayload_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC82033B28C61F640002EFDD /* CreateCallPayload_Tests.swift */; };
AC82033F28C6598C0002EFDD /* CallRequestBody_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC82033E28C6598C0002EFDD /* CallRequestBody_Tests.swift */; };
AC908384268B115F00ACFB8E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC908383268B115F00ACFB8E /* AppDelegate.swift */; };
AC90838D268B116000ACFB8E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC90838C268B116000ACFB8E /* Assets.xcassets */; };
@@ -1459,6 +1453,8 @@
AD3D0CC226A88E5100A6D813 /* MessengerChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3D0CC126A88E5100A6D813 /* MessengerChatChannelHeaderView.swift */; };
AD3D0CC426A89E6300A6D813 /* iMessageChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3D0CC326A89E6300A6D813 /* iMessageChatChannelHeaderView.swift */; };
AD3EE5442832921400ACEFD9 /* VirtualTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D15D8527E9D4B5006B34D7 /* VirtualTime.swift */; };
+ AD4118832D5E1368000EF88E /* UILabel+highlightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */; };
+ AD4118842D5E1368000EF88E /* UILabel+highlightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */; };
AD43DE6D2A712B0F0040C0FD /* ChatChannelListSearchVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD43DE6C2A712B0F0040C0FD /* ChatChannelListSearchVC.swift */; };
AD43DE6E2A712B0F0040C0FD /* ChatChannelListSearchVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD43DE6C2A712B0F0040C0FD /* ChatChannelListSearchVC.swift */; };
AD43F90926153BAD00F2D4BB /* QuotedChatMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD43F90826153BAD00F2D4BB /* QuotedChatMessageView.swift */; };
@@ -1509,6 +1505,31 @@
AD52A21C2804851600D0157E /* CommandDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD52A21B2804851600D0157E /* CommandDTO.swift */; };
AD52A21D2804851600D0157E /* CommandDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD52A21B2804851600D0157E /* CommandDTO.swift */; };
AD540AE2260CECA10082D802 /* QuotedChatMessageView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD540AE1260CECA10082D802 /* QuotedChatMessageView_Tests.swift */; };
+ AD545E602D523CB0008FD399 /* DraftPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E5F2D523CA8008FD399 /* DraftPayloads.swift */; };
+ AD545E612D523CB0008FD399 /* DraftPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E5F2D523CA8008FD399 /* DraftPayloads.swift */; };
+ AD545E632D52827B008FD399 /* DraftListQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E622D528271008FD399 /* DraftListQuery.swift */; };
+ AD545E642D52827B008FD399 /* DraftListQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E622D528271008FD399 /* DraftListQuery.swift */; };
+ AD545E662D53C271008FD399 /* DraftMessagesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E652D53C26B008FD399 /* DraftMessagesRepository.swift */; };
+ AD545E672D53C271008FD399 /* DraftMessagesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E652D53C26B008FD399 /* DraftMessagesRepository.swift */; };
+ AD545E692D5531BA008FD399 /* DemoDraftMessageListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E682D5531B9008FD399 /* DemoDraftMessageListVC.swift */; };
+ AD545E6B2D5650B5008FD399 /* DraftPayloads_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E6A2D5650B5008FD399 /* DraftPayloads_Tests.swift */; };
+ AD545E6D2D565316008FD399 /* DraftMessage.json in Resources */ = {isa = PBXBuildFile; fileRef = AD545E6C2D565316008FD399 /* DraftMessage.json */; };
+ AD545E712D5A7463008FD399 /* DraftEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E702D5A745F008FD399 /* DraftEvents.swift */; };
+ AD545E722D5A7463008FD399 /* DraftEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E702D5A745F008FD399 /* DraftEvents.swift */; };
+ AD545E742D5A79DA008FD399 /* DraftUpdaterMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E732D5A79B5008FD399 /* DraftUpdaterMiddleware.swift */; };
+ AD545E752D5A79DA008FD399 /* DraftUpdaterMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E732D5A79B5008FD399 /* DraftUpdaterMiddleware.swift */; };
+ AD545E772D5BB3E0008FD399 /* DraftEndpoints_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E762D5BB3D6008FD399 /* DraftEndpoints_Tests.swift */; };
+ AD545E792D5BC14E008FD399 /* DraftMessagesRepository_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E782D5BC14E008FD399 /* DraftMessagesRepository_Tests.swift */; };
+ AD545E7B2D5BC1DC008FD399 /* DraftPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E7A2D5BC1DC008FD399 /* DraftPayload.swift */; };
+ AD545E7D2D5CFC15008FD399 /* ChannelController+Drafts_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E7C2D5CFC15008FD399 /* ChannelController+Drafts_Tests.swift */; };
+ AD545E7F2D5CFC36008FD399 /* DraftMessagesRepository_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E7E2D5CFC36008FD399 /* DraftMessagesRepository_Mock.swift */; };
+ AD545E812D5D0006008FD399 /* MessageController+Drafts_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E802D5D0006008FD399 /* MessageController+Drafts_Tests.swift */; };
+ AD545E832D5D0389008FD399 /* CurrentUserController+Drafts_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E822D5D0389008FD399 /* CurrentUserController+Drafts_Tests.swift */; };
+ AD545E852D5D7591008FD399 /* DraftListQuery_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E842D5D7591008FD399 /* DraftListQuery_Tests.swift */; };
+ AD545E872D5D805A008FD399 /* DraftEvents_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E862D5D805A008FD399 /* DraftEvents_Tests.swift */; };
+ AD545E8B2D5D8095008FD399 /* DraftUpdated.json in Resources */ = {isa = PBXBuildFile; fileRef = AD545E892D5D8095008FD399 /* DraftUpdated.json */; };
+ AD545E8C2D5D8095008FD399 /* DraftDeleted.json in Resources */ = {isa = PBXBuildFile; fileRef = AD545E882D5D8095008FD399 /* DraftDeleted.json */; };
+ AD545E8E2D5D827B008FD399 /* DraftUpdaterMiddleware_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD545E8D2D5D827B008FD399 /* DraftUpdaterMiddleware_Tests.swift */; };
AD552E0128F46CE700199A6F /* ImageLoaderOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD552E0028F46CE700199A6F /* ImageLoaderOptions.swift */; };
AD552E0228F46CE700199A6F /* ImageLoaderOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD552E0028F46CE700199A6F /* ImageLoaderOptions.swift */; };
AD57979E2978C4F7006CC435 /* UploadedAttachmentPostProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD57979D2978C4F7006CC435 /* UploadedAttachmentPostProcessor.swift */; };
@@ -1660,6 +1681,9 @@
AD99C909279B0E9D009DD9C5 /* MessageDateSeparatorFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD99C906279B0C9B009DD9C5 /* MessageDateSeparatorFormatter.swift */; };
AD99C90C279B136B009DD9C5 /* UserLastActivityFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD99C90A279B1363009DD9C5 /* UserLastActivityFormatter.swift */; };
AD99C90D279B136D009DD9C5 /* UserLastActivityFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD99C90A279B1363009DD9C5 /* UserLastActivityFormatter.swift */; };
+ ADA03A222D64EFE900DFE048 /* DraftMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA03A212D64EFE900DFE048 /* DraftMessage.swift */; };
+ ADA03A232D64EFE900DFE048 /* DraftMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA03A212D64EFE900DFE048 /* DraftMessage.swift */; };
+ ADA03A252D65041B00DFE048 /* DraftMessage_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA03A242D65041300DFE048 /* DraftMessage_Mock.swift */; };
ADA2D64A2C46B66E001D2B44 /* DemoChatChannelListErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA2D6492C46B66E001D2B44 /* DemoChatChannelListErrorView.swift */; };
ADA3572F269C807A004AD8E9 /* ChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA3572D269C562A004AD8E9 /* ChatChannelHeaderView.swift */; };
ADA5A0F8276790C100E1C465 /* ChatMessageListDateSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA5A0F7276790C100E1C465 /* ChatMessageListDateSeparatorView.swift */; };
@@ -1818,6 +1842,8 @@
ADF617692A09927000E70307 /* MessagesPaginationStateHandler_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF617672A09926900E70307 /* MessagesPaginationStateHandler_Tests.swift */; };
ADF9E1F72A03E7E400109108 /* MessagesPaginationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF9E1F62A03E7E400109108 /* MessagesPaginationState.swift */; };
ADFA09C926A99E0A002A6EFA /* ChatThreadHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFA09C726A99C71002A6EFA /* ChatThreadHeaderView.swift */; };
+ ADFD391D2D47D07C00F8E1B1 /* DraftEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFD391C2D47D06E00F8E1B1 /* DraftEndpoints.swift */; };
+ ADFD391E2D47D07C00F8E1B1 /* DraftEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFD391C2D47D06E00F8E1B1 /* DraftEndpoints.swift */; };
BCE4831434E78C9538FA73F8 /* JSONDecoder_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE48068C1C02C0689BEB64E /* JSONDecoder_Tests.swift */; };
BCE484BA1EE03FF336034250 /* FilterEncoding_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE483AC99F58A9034EA2ECE /* FilterEncoding_Tests.swift */; };
BCE48639FD7B6B05CD63A6AF /* FilterDecoding_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE4862E2C4943998F0DCBD9 /* FilterDecoding_Tests.swift */; };
@@ -3179,8 +3205,6 @@
43ABF8B626C513D20034BD62 /* CurrentUserCustomRole.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CurrentUserCustomRole.json; sourceTree = ""; };
43BAAD472664F59600323D8E /* UserStopTypingThread.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UserStopTypingThread.json; sourceTree = ""; };
43BAAD482664F59600323D8E /* UserStartTypingThread.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UserStartTypingThread.json; sourceTree = ""; };
- 43D3F0F52841052600B74921 /* CallPayloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallPayloads.swift; sourceTree = ""; };
- 43D3F0F8284106EE00B74921 /* CallEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallEndpoints.swift; sourceTree = ""; };
43D3F0FB28410A0200B74921 /* CreateCallRequestBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateCallRequestBody.swift; sourceTree = ""; };
43EB3AE12671718200954323 /* AttachmentViewCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewCatalog.swift; sourceTree = ""; };
43F4750B26F4E4FF0009487D /* ChatMessageReactionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageReactionItemView.swift; sourceTree = ""; };
@@ -4139,8 +4163,6 @@
A3EA3327276C904700C84A52 /* ObjcAssociatedWeakObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcAssociatedWeakObject.swift; sourceTree = ""; };
A3F65E3227EB6F63003F6256 /* AssertNetworkRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssertNetworkRequest.swift; sourceTree = ""; };
AC73783926A6AF1C002ED7B4 /* AttachmentPayloadLinkWithoutImagePreview.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = AttachmentPayloadLinkWithoutImagePreview.json; sourceTree = ""; };
- AC82033928C60B100002EFDD /* CallEndpoints_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallEndpoints_Tests.swift; sourceTree = ""; };
- AC82033B28C61F640002EFDD /* CreateCallPayload_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateCallPayload_Tests.swift; sourceTree = ""; };
AC82033E28C6598C0002EFDD /* CallRequestBody_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRequestBody_Tests.swift; sourceTree = ""; };
AC908381268B115F00ACFB8E /* YouTube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = YouTube.app; sourceTree = BUILT_PRODUCTS_DIR; };
AC908383268B115F00ACFB8E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
@@ -4234,6 +4256,7 @@
AD3D0CBF26A8727800A6D813 /* SlackChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlackChatChannelHeaderView.swift; sourceTree = ""; };
AD3D0CC126A88E5100A6D813 /* MessengerChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessengerChatChannelHeaderView.swift; sourceTree = ""; };
AD3D0CC326A89E6300A6D813 /* iMessageChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iMessageChatChannelHeaderView.swift; sourceTree = ""; };
+ AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+highlightText.swift"; sourceTree = ""; };
AD43DE6C2A712B0F0040C0FD /* ChatChannelListSearchVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListSearchVC.swift; sourceTree = ""; };
AD43F90826153BAD00F2D4BB /* QuotedChatMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedChatMessageView.swift; sourceTree = ""; };
AD447376263ABC5C0030E583 /* ChatCommandSuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCommandSuggestionCollectionViewCell.swift; sourceTree = ""; };
@@ -4267,6 +4290,26 @@
AD52A21B2804851600D0157E /* CommandDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandDTO.swift; sourceTree = ""; };
AD53DCDE27271D850019290C /* MessageReactionsPayload.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = MessageReactionsPayload.json; sourceTree = ""; };
AD540AE1260CECA10082D802 /* QuotedChatMessageView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedChatMessageView_Tests.swift; sourceTree = ""; };
+ AD545E5F2D523CA8008FD399 /* DraftPayloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftPayloads.swift; sourceTree = ""; };
+ AD545E622D528271008FD399 /* DraftListQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftListQuery.swift; sourceTree = ""; };
+ AD545E652D53C26B008FD399 /* DraftMessagesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftMessagesRepository.swift; sourceTree = ""; };
+ AD545E682D5531B9008FD399 /* DemoDraftMessageListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoDraftMessageListVC.swift; sourceTree = ""; };
+ AD545E6A2D5650B5008FD399 /* DraftPayloads_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftPayloads_Tests.swift; sourceTree = ""; };
+ AD545E6C2D565316008FD399 /* DraftMessage.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DraftMessage.json; sourceTree = ""; };
+ AD545E702D5A745F008FD399 /* DraftEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftEvents.swift; sourceTree = ""; };
+ AD545E732D5A79B5008FD399 /* DraftUpdaterMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftUpdaterMiddleware.swift; sourceTree = ""; };
+ AD545E762D5BB3D6008FD399 /* DraftEndpoints_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftEndpoints_Tests.swift; sourceTree = ""; };
+ AD545E782D5BC14E008FD399 /* DraftMessagesRepository_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftMessagesRepository_Tests.swift; sourceTree = ""; };
+ AD545E7A2D5BC1DC008FD399 /* DraftPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftPayload.swift; sourceTree = ""; };
+ AD545E7C2D5CFC15008FD399 /* ChannelController+Drafts_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelController+Drafts_Tests.swift"; sourceTree = ""; };
+ AD545E7E2D5CFC36008FD399 /* DraftMessagesRepository_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftMessagesRepository_Mock.swift; sourceTree = ""; };
+ AD545E802D5D0006008FD399 /* MessageController+Drafts_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageController+Drafts_Tests.swift"; sourceTree = ""; };
+ AD545E822D5D0389008FD399 /* CurrentUserController+Drafts_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUserController+Drafts_Tests.swift"; sourceTree = ""; };
+ AD545E842D5D7591008FD399 /* DraftListQuery_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftListQuery_Tests.swift; sourceTree = ""; };
+ AD545E862D5D805A008FD399 /* DraftEvents_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftEvents_Tests.swift; sourceTree = ""; };
+ AD545E882D5D8095008FD399 /* DraftDeleted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DraftDeleted.json; sourceTree = ""; };
+ AD545E892D5D8095008FD399 /* DraftUpdated.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DraftUpdated.json; sourceTree = ""; };
+ AD545E8D2D5D827B008FD399 /* DraftUpdaterMiddleware_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftUpdaterMiddleware_Tests.swift; sourceTree = ""; };
AD552E0028F46CE700199A6F /* ImageLoaderOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoaderOptions.swift; sourceTree = ""; };
AD57979D2978C4F7006CC435 /* UploadedAttachmentPostProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadedAttachmentPostProcessor.swift; sourceTree = ""; };
AD57DE752A77D5A2005408B6 /* ChannelListSearchStrategy_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListSearchStrategy_Tests.swift; sourceTree = ""; };
@@ -4365,6 +4408,8 @@
AD99C906279B0C9B009DD9C5 /* MessageDateSeparatorFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDateSeparatorFormatter.swift; sourceTree = ""; };
AD99C90A279B1363009DD9C5 /* UserLastActivityFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLastActivityFormatter.swift; sourceTree = ""; };
AD9BE32526680E4200A6D284 /* Stream.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Stream.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ ADA03A212D64EFE900DFE048 /* DraftMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftMessage.swift; sourceTree = ""; };
+ ADA03A242D65041300DFE048 /* DraftMessage_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftMessage_Mock.swift; sourceTree = ""; };
ADA2D6492C46B66E001D2B44 /* DemoChatChannelListErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChatChannelListErrorView.swift; sourceTree = ""; };
ADA3572D269C562A004AD8E9 /* ChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelHeaderView.swift; sourceTree = ""; };
ADA5A0F7276790C100E1C465 /* ChatMessageListDateSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageListDateSeparatorView.swift; sourceTree = ""; };
@@ -4472,6 +4517,7 @@
ADF617672A09926900E70307 /* MessagesPaginationStateHandler_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesPaginationStateHandler_Tests.swift; sourceTree = ""; };
ADF9E1F62A03E7E400109108 /* MessagesPaginationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesPaginationState.swift; sourceTree = ""; };
ADFA09C726A99C71002A6EFA /* ChatThreadHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadHeaderView.swift; sourceTree = ""; };
+ ADFD391C2D47D06E00F8E1B1 /* DraftEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftEndpoints.swift; sourceTree = ""; };
BCE48068C1C02C0689BEB64E /* JSONDecoder_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONDecoder_Tests.swift; sourceTree = ""; };
BCE483AC99F58A9034EA2ECE /* FilterEncoding_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterEncoding_Tests.swift; sourceTree = ""; };
BCE4862E2C4943998F0DCBD9 /* FilterDecoding_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterDecoding_Tests.swift; sourceTree = ""; };
@@ -5536,6 +5582,7 @@
841BAA0F2BCEADAC000C73E4 /* PollsEvents.swift */,
AD7BE1692C209888000A5756 /* ThreadEvents.swift */,
848849B52CEE01070010E7CA /* AITypingEvents.swift */,
+ AD545E702D5A745F008FD399 /* DraftEvents.swift */,
);
path = Events;
sourceTree = "";
@@ -5651,6 +5698,7 @@
79C5CBF025F66E9700D98001 /* ChannelWatcherListQuery.swift */,
AD6E32A02BBC50110073831B /* ThreadListQuery.swift */,
AD6E32A32BBC502D0073831B /* ThreadQuery.swift */,
+ AD545E622D528271008FD399 /* DraftListQuery.swift */,
AD0CC0112BDBC1BF005E2C66 /* ReactionListQuery.swift */,
7978FBB926E15A58002CA2DF /* MessageSearchQuery.swift */,
792A4F442480107A00EAF71D /* Pagination.swift */,
@@ -5724,6 +5772,7 @@
796610B7248E64EC00761629 /* EventMiddlewares */ = {
isa = PBXGroup;
children = (
+ AD545E732D5A79B5008FD399 /* DraftUpdaterMiddleware.swift */,
79896D63250A62EE00BA8F1C /* ChannelReadUpdaterMiddleware.swift */,
AD9632E02C0A43630073B814 /* ThreadUpdaterMiddleware.swift */,
79158CF325F133FB00186102 /* ChannelTruncatedEventMiddleware.swift */,
@@ -5745,7 +5794,6 @@
isa = PBXGroup;
children = (
ADF2BBEA2B9B622B0069D467 /* AppSettingsPayload.swift */,
- 43D3F0F52841052600B74921 /* CallPayloads.swift */,
DA9985ED24E175AA000E9885 /* ChannelCodingKeys.swift */,
DAFAD6A224DD8E1A0043ED06 /* ChannelEditDetailPayload.swift */,
79682C4924BF37970071578E /* ChannelListPayload.swift */,
@@ -5762,6 +5810,7 @@
793728292498FFD300E13FE5 /* MemberPayload.swift */,
4F6B840F2D008D5F005645B0 /* MemberUpdatePayload.swift */,
DABC6ABB2546FD0100A8FC78 /* MessageAttachmentPayload.swift */,
+ AD545E5F2D523CA8008FD399 /* DraftPayloads.swift */,
79682C4824BF37650071578E /* MessagePayloads.swift */,
8899BC3E2542FFA1003CB98B /* MessageReactionPayload.swift */,
ADEEB7F12BD1368900C76602 /* MessageReactionGroupPayload.swift */,
@@ -5807,9 +5856,9 @@
children = (
88E26D7C2580F95300F55AB5 /* AttachmentEndpoints.swift */,
82C18FDB2C10C8E600C5283C /* BlockedUserPayload.swift */,
- 43D3F0F8284106EE00B74921 /* CallEndpoints.swift */,
79877A132498E4EE00015F8B /* ChannelEndpoints.swift */,
F6FF1DA924FD23D300151735 /* MessageEndpoints.swift */,
+ ADFD391C2D47D06E00F8E1B1 /* DraftEndpoints.swift */,
AD0CC01B2BDBD22D005E2C66 /* ReactionEndpoints.swift */,
AD84377A2BB482CF000F3826 /* ThreadEndpoints.swift */,
C143788F27BC03EE00E23965 /* EndpointPath.swift */,
@@ -5907,6 +5956,7 @@
4F877D392D019E0400CB66EC /* ChannelPinningScope.swift */,
79896D5D25065E6900BA8F1C /* ChannelRead.swift */,
79877A042498E4BB00015F8B /* ChannelType.swift */,
+ ADA03A212D64EFE900DFE048 /* DraftMessage.swift */,
799C9431247D2FB9001F1104 /* ChatMessage.swift */,
79877A052498E4BC00015F8B /* CurrentUser.swift */,
79877A022498E4BB00015F8B /* Device.swift */,
@@ -6343,6 +6393,7 @@
79F691B12604C10A000AE89B /* SystemEnvironment.swift */,
CF7B2A2528BEAA93006BE124 /* TextViewMentionedUsersHandler.swift */,
C12297D22AC57A3200C5FF04 /* Throttler.swift */,
+ AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */,
AD169DEC2C9B112B00F58FAC /* KeyboardHandler */,
AD95FD0F28F9B72200DBDF41 /* Extensions */,
ACCA772826C40C7A007AE2ED /* ImageLoading */,
@@ -6578,6 +6629,7 @@
8A62705F24BE31B20040BFD6 /* Events */ = {
isa = PBXGroup;
children = (
+ AD545E8A2D5D8095008FD399 /* Draft */,
84E46A332CFA1B73000CBDDE /* AIIndicator */,
ADE57B802C3C5C4600DD6B88 /* Thread */,
8A0C3BCA24C1C38C00CAFD19 /* Channel */,
@@ -6760,6 +6812,7 @@
A3227ECA284A607D00EBE6CC /* Screens */ = {
isa = PBXGroup;
children = (
+ AD545E682D5531B9008FD399 /* DemoDraftMessageListVC.swift */,
ADD328592C04DD8300BAD0E9 /* DemoAppTabBarController.swift */,
C10B5C712A1F794A006A5BCB /* MembersViewController.swift */,
C1CEF9062A1BC4E800414931 /* UserProfileViewController.swift */,
@@ -6796,6 +6849,7 @@
A344074F27D753530044F150 /* Models + Extensions */ = {
isa = PBXGroup;
children = (
+ ADA03A242D65041300DFE048 /* DraftMessage_Mock.swift */,
A344075027D753530044F150 /* ChannelUnreadCount_Mock.swift */,
A344075127D753530044F150 /* ChatChannel_Mock.swift */,
A344075D27D753530044F150 /* ChatChannelMember_Mock.swift */,
@@ -6828,6 +6882,7 @@
A344076127D753530044F150 /* DummyData */ = {
isa = PBXGroup;
children = (
+ AD545E7A2D5BC1DC008FD399 /* DraftPayload.swift */,
AD94905B2BF630D200E69224 /* ThreadPayload.swift */,
84C11BDE27FB2B4600000A9E /* ChannelPayload.swift */,
79D7A1CD2593A40900D3C2BF /* ChannelDetailPayload.swift */,
@@ -6967,6 +7022,7 @@
A364D08D27D0BD8E0029857A /* EventMiddlewares */ = {
isa = PBXGroup;
children = (
+ AD545E8D2D5D827B008FD399 /* DraftUpdaterMiddleware_Tests.swift */,
79896D65250A6D1500BA8F1C /* ChannelReadUpdaterMiddleware_Tests.swift */,
AD7BE16C2C20CC02000A5756 /* ThreadUpdaterMiddlware_Tests.swift */,
79158CEA25F0EADF00186102 /* ChannelTruncatedEventMiddleware_Tests.swift */,
@@ -6987,6 +7043,7 @@
A364D08E27D0BDB20029857A /* Events */ = {
isa = PBXGroup;
children = (
+ AD545E862D5D805A008FD399 /* DraftEvents_Tests.swift */,
8A62706B24BF3DBC0040BFD6 /* ChannelEvents_Tests.swift */,
84A1D2EF26AB10DB00014712 /* EventDecoder_Tests.swift */,
794927F0249E3DE6009D7EB7 /* EventPayload_Tests.swift */,
@@ -7039,9 +7096,9 @@
A364D09327D0BF330029857A /* Endpoints */ = {
isa = PBXGroup;
children = (
+ AD545E762D5BB3D6008FD399 /* DraftEndpoints_Tests.swift */,
AD8C7C652BA46A4A00260715 /* AppEndpoints_Tests.swift */,
88381E8625825A240047A6A3 /* AttachmentEndpoints_Tests.swift */,
- AC82033928C60B100002EFDD /* CallEndpoints_Tests.swift */,
DAEAF4B724DC026C0015FB28 /* ChannelEndpoints_Tests.swift */,
AD6E32AC2BBC86950073831B /* ThreadEndpoint_Tests.swift */,
790A4C47252DDD1A001F4A23 /* DeviceEndpoints_Tests.swift */,
@@ -7066,13 +7123,13 @@
A364D09427D0BF3A0029857A /* Payloads */ = {
isa = PBXGroup;
children = (
+ AD545E6A2D5650B5008FD399 /* DraftPayloads_Tests.swift */,
AD8C7C622BA464E600260715 /* AppSettingsPayload_Tests.swift */,
AD6E32952BBB10890073831B /* ThreadListPayload_Tests.swift */,
DA7229E224E140260074503A /* ChannelEditDetailPayload_Tests.swift */,
8A0D64AA24E57BF20017A3C0 /* ChannelListPayload_Tests.swift */,
C122B8802A02645200D27F41 /* ChannelReadPayload_Tests.swift */,
882C574D252C76A300E60C44 /* ChannelMemberListPayload_Tests.swift */,
- AC82033B28C61F640002EFDD /* CreateCallPayload_Tests.swift */,
79B5517624E595DA00CE9FEC /* CurrentUserPayloads_Tests.swift */,
430156DB26B1862C0006E7EA /* CustomDataHashMap_Tests.swift */,
790A4C4D252E0901001F4A23 /* DevicePayloads_Tests.swift */,
@@ -7287,6 +7344,7 @@
A364D0A327D126490029857A /* Repositories */ = {
isa = PBXGroup;
children = (
+ AD545E782D5BC14E008FD399 /* DraftMessagesRepository_Tests.swift */,
C11BAA4C2907EC7B004C5EA4 /* AuthenticationRepository_Tests.swift */,
C18514FC292E34E10033387E /* ConnectionRepository_Tests.swift */,
C1C5345B29AFE4C9006F9AF4 /* ChannelRepository_Tests.swift */,
@@ -7342,6 +7400,7 @@
A364D0A827D128650029857A /* ChannelController */ = {
isa = PBXGroup;
children = (
+ AD545E7C2D5CFC15008FD399 /* ChannelController+Drafts_Tests.swift */,
7952B3B224D314B100AC53D4 /* ChannelController_Tests.swift */,
DA4AA3B32502719700FAAF6E /* ChannelController+Combine_Tests.swift */,
DAE566E824FFD24000E39431 /* ChannelController+SwiftUI_Tests.swift */,
@@ -7382,6 +7441,7 @@
A364D0AD27D1291E0029857A /* CurrentUserController */ = {
isa = PBXGroup;
children = (
+ AD545E822D5D0389008FD399 /* CurrentUserController+Drafts_Tests.swift */,
F69E7F7C24ED7562000F5252 /* CurrentUserController_Tests.swift */,
DA4AA3B5250271B100FAAF6E /* CurrentUserController+Combine_Tests.swift */,
DAE566EC24FFD27500E39431 /* CurrentUserController+SwiftUI_Tests.swift */,
@@ -7431,6 +7491,7 @@
A364D0B227D129A90029857A /* MessageController */ = {
isa = PBXGroup;
children = (
+ AD545E802D5D0006008FD399 /* MessageController+Drafts_Tests.swift */,
F649B2362500F785008F98C8 /* MessageController_Tests.swift */,
DAF1BED625066128003CEDC0 /* MessageController+Combine_Tests.swift */,
DAF1BED225066107003CEDC0 /* MessageController+SwiftUI_Tests.swift */,
@@ -7480,6 +7541,7 @@
A364D0B727D12A520029857A /* Query */ = {
isa = PBXGroup;
children = (
+ AD545E842D5D7591008FD399 /* DraftListQuery_Tests.swift */,
A3C7BAD027E4E02700BBF4FA /* ChannelListFilterScope_Tests.swift */,
799F611A2530B62C007F218C /* ChannelListQuery_Tests.swift */,
AD0CC0142BDBC68E005E2C66 /* ReactionListQuery_Tests.swift */,
@@ -8130,6 +8192,7 @@
A3C729552840BA4800FFE8B4 /* JSONs */ = {
isa = PBXGroup;
children = (
+ AD545E6C2D565316008FD399 /* DraftMessage.json */,
798779F72498E47700015F8B /* Channel.json */,
AD6E32972BBB13650073831B /* Thread.json */,
AD6E32992BBB139D0073831B /* ThreadList.json */,
@@ -8572,6 +8635,15 @@
path = PollResultsVoteListVC;
sourceTree = "";
};
+ AD545E8A2D5D8095008FD399 /* Draft */ = {
+ isa = PBXGroup;
+ children = (
+ AD545E882D5D8095008FD399 /* DraftDeleted.json */,
+ AD545E892D5D8095008FD399 /* DraftUpdated.json */,
+ );
+ path = Draft;
+ sourceTree = "";
+ };
AD57DE742A77D566005408B6 /* Search */ = {
isa = PBXGroup;
children = (
@@ -9116,6 +9188,7 @@
C12D0A5E28FD58CE0099895A /* Repositories */ = {
isa = PBXGroup;
children = (
+ AD545E7E2D5CFC36008FD399 /* DraftMessagesRepository_Mock.swift */,
C12D0A5F28FD59B60099895A /* AuthenticationRepository_Mock.swift */,
A344074E27D753530044F150 /* ConnectionRepository_Mock.swift */,
C1C5345929AFDDAE006F9AF4 /* ChannelRepository_Mock.swift */,
@@ -9258,6 +9331,7 @@
C1E8AD55278C8A440041B775 /* SyncRepository.swift */,
8451C48C2BD671A400849955 /* PollsRepository.swift */,
AD0E278D2BF789630037554F /* ThreadsRepository.swift */,
+ AD545E652D53C26B008FD399 /* DraftMessagesRepository.swift */,
);
path = Repositories;
sourceTree = "";
@@ -10194,6 +10268,8 @@
A311B3F527E8B99D00CFCF6D /* HealthCheck.json in Resources */,
A311B40727E8B9AD00CFCF6D /* NotificationRemovedFromChannel.json in Resources */,
A311B41727E8B9B900CFCF6D /* UserStartTypingThread.json in Resources */,
+ AD545E8B2D5D8095008FD399 /* DraftUpdated.json in Resources */,
+ AD545E8C2D5D8095008FD399 /* DraftDeleted.json in Resources */,
799EC85F2853B3BE00F18770 /* BigChannelListPayload.json in Resources */,
A311B40127E8B9AD00CFCF6D /* NotificationMarkRead.json in Resources */,
A311B42927E8B9D800CFCF6D /* UserUpdateResponse.json in Resources */,
@@ -10279,6 +10355,7 @@
A311B3E227E8B98C00CFCF6D /* UsersQuery.json in Resources */,
A311B3FE27E8B9A800CFCF6D /* MessageDeleted.json in Resources */,
A311B3D927E8B98C00CFCF6D /* ChannelPayloadWithCustom.json in Resources */,
+ AD545E6D2D565316008FD399 /* DraftMessage.json in Resources */,
A311B42027E8B9C400CFCF6D /* FlagUserPayload+NoExtraData.json in Resources */,
ADF3EEF62C00FC7B00DB36D6 /* NotificationMarkUnread+MissingFields.json in Resources */,
A311B3E627E8B99200CFCF6D /* AttachmentPayloadImage.json in Resources */,
@@ -10608,6 +10685,7 @@
C1FC2F8627416E150062530F /* Allocations.swift in Sources */,
C1FC2F8C27416E1F0062530F /* UIImage+SwiftyGif.swift in Sources */,
AD9632DC2C09E0350073B814 /* ChatThreadListRouter.swift in Sources */,
+ AD4118842D5E1368000EF88E /* UILabel+highlightText.swift in Sources */,
40FA4DE52A12A45400DA21D2 /* VoiceRecordingAttachmentComposerPreview.swift in Sources */,
ADC4AAB02788C8850004BB35 /* Appearance+Formatters.swift in Sources */,
AD6F531927175FDB00D428B4 /* ChatMessageGiphyView+GiphyBadge.swift in Sources */,
@@ -11090,6 +11168,7 @@
AD053B9D2B3358E2003612B6 /* LocationAttachmentPayload.swift in Sources */,
AD053BA12B3359DD003612B6 /* DemoAttachmentViewCatalog.swift in Sources */,
AD053B9F2B335929003612B6 /* LocationAttachmentViewInjector.swift in Sources */,
+ AD545E692D5531BA008FD399 /* DemoDraftMessageListVC.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -11121,6 +11200,7 @@
A3C3BC6927E8AA4300224761 /* TestBuilder.swift in Sources */,
A3C3BC6A27E8AA4300224761 /* TestMemberEvent.swift in Sources */,
40D4840A2A1264F1009E4134 /* MockAVPlayer.swift in Sources */,
+ AD545E7B2D5BC1DC008FD399 /* DraftPayload.swift in Sources */,
A344078127D753530044F150 /* ChatUser_Mock.swift in Sources */,
84C85B452BF2B2D1008A7AA5 /* Poll+Unique.swift in Sources */,
A3C3BC3327E87F2900224761 /* OfflineRequestsRepository_Mock.swift in Sources */,
@@ -11261,6 +11341,7 @@
C163ED012992B378006D6124 /* NotificationExtensionLifecycle_Mock.swift in Sources */,
82E655352B06751D00D64906 /* QueueAwareDelegate.swift in Sources */,
A3C3BC6D27E8AA4300224761 /* TestManagedObject.swift in Sources */,
+ ADA03A252D65041B00DFE048 /* DraftMessage_Mock.swift in Sources */,
8263464C2B0BACF600122D0E /* Difference.swift in Sources */,
A3C3BC2427E87F1800224761 /* ChatChannelController_Spy.swift in Sources */,
A3C3BC1927E87EFE00224761 /* ConnectionRepository_Mock.swift in Sources */,
@@ -11283,6 +11364,7 @@
A3C3BC8527E8AB6200224761 /* Array+Subscript.swift in Sources */,
84196FA32805892500185E99 /* LocalMessageState+Extensions.swift in Sources */,
82E655452B067CAE00D64906 /* AssertResult.swift in Sources */,
+ AD545E7F2D5CFC36008FD399 /* DraftMessagesRepository_Mock.swift in Sources */,
82E655432B067C3600D64906 /* AssertAsync.swift in Sources */,
84C7CB152BC1F7EC0088890C /* MessageSearch_Mock.swift in Sources */,
A34ECB5C27F5D0BF00A804C1 /* TestDataModel2.xcdatamodeld in Sources */,
@@ -11337,6 +11419,7 @@
84DCB853269F569A006CDF32 /* EventsController+SwiftUI.swift in Sources */,
4FF9B2692C6F697300A3B711 /* AttachmentDownloader.swift in Sources */,
40789D3129F6AC500018C2BB /* AudioRecording.swift in Sources */,
+ AD545E662D53C271008FD399 /* DraftMessagesRepository.swift in Sources */,
841BAA4B2BD1CCC0000C73E4 /* PollVoteDTO.swift in Sources */,
88381E65258258C20047A6A3 /* FileUploadPayload.swift in Sources */,
7978FBBF26E1667C002CA2DF /* MessageSearchController.swift in Sources */,
@@ -11385,6 +11468,7 @@
8A0D649724E579A50017A3C0 /* GuestEndpoints.swift in Sources */,
841BAA102BCEADAC000C73E4 /* PollsEvents.swift in Sources */,
8899BC47254305F8003CB98B /* MessageReactionRequestPayload.swift in Sources */,
+ AD545E722D5A7463008FD399 /* DraftEvents.swift in Sources */,
8A62705024B867190040BFD6 /* EventPayload.swift in Sources */,
AD70DC362ADEC0F600CFC3B7 /* MessageModerationDetailsPayload.swift in Sources */,
AD0E278E2BF789630037554F /* ThreadsRepository.swift in Sources */,
@@ -11434,10 +11518,10 @@
799C9445247D3DD2001F1104 /* WebSocketClient.swift in Sources */,
AD6E32A12BBC50110073831B /* ThreadListQuery.swift in Sources */,
AD8258A32BD2939500B9ED74 /* MessageReactionGroup.swift in Sources */,
- 43D3F0F9284106EE00B74921 /* CallEndpoints.swift in Sources */,
C15C8838286C7BF300E6A72C /* BackgroundListDatabaseObserver.swift in Sources */,
DA640FBB2535CF8500D32944 /* ChannelListSortingKey.swift in Sources */,
8899BC3F2542FFA1003CB98B /* MessageReactionPayload.swift in Sources */,
+ ADFD391E2D47D07C00F8E1B1 /* DraftEndpoints.swift in Sources */,
797A756624814EF8003CF16D /* SystemEnvironment.swift in Sources */,
799C9438247D2FB9001F1104 /* ChatClientConfig.swift in Sources */,
7964F3B6249A314D002A09EC /* PrefixLogFormatter.swift in Sources */,
@@ -11502,6 +11586,7 @@
DA4AA3B8250271BD00FAAF6E /* CurrentUserController+Combine.swift in Sources */,
79280F712487CD2B00CDEB89 /* Atomic.swift in Sources */,
AD7AC99B260A9572004AADA5 /* MessagePinning.swift in Sources */,
+ ADA03A232D64EFE900DFE048 /* DraftMessage.swift in Sources */,
88DA57642631CF1F00FA8C53 /* MuteDetails.swift in Sources */,
7978FBBA26E15A58002CA2DF /* MessageSearchQuery.swift in Sources */,
4F8CA69C2CB665EB00EBEA2D /* EphemeralValuesContainer.swift in Sources */,
@@ -11572,6 +11657,7 @@
DABC6ABC2546FD0100A8FC78 /* MessageAttachmentPayload.swift in Sources */,
4F427F662BA2F43200D92238 /* ConnectedUser.swift in Sources */,
4F73F39E2B91C7BF00563CD9 /* MessageState+Observer.swift in Sources */,
+ AD545E752D5A79DA008FD399 /* DraftUpdaterMiddleware.swift in Sources */,
C1E8AD5E278EF5F30041B775 /* AsyncOperation.swift in Sources */,
88D85DA7252F3C1D00AE1030 /* MemberListController.swift in Sources */,
79280F4B248523C000CDEB89 /* ConnectionEvents.swift in Sources */,
@@ -11579,6 +11665,7 @@
882C574A252C767E00E60C44 /* ChannelMemberListPayload.swift in Sources */,
DAFAD6A324DD8E1A0043ED06 /* ChannelEditDetailPayload.swift in Sources */,
84A43CB326A9A54700302763 /* EventSender.swift in Sources */,
+ AD545E612D523CB0008FD399 /* DraftPayloads.swift in Sources */,
88BEBCD62536FDBF00D9E8B7 /* MemberListController+SwiftUI.swift in Sources */,
A3B0CFA227BBF52600F352F9 /* ChannelTruncateRequestPayload.swift in Sources */,
4FE6E1AD2BAC7A1B00C80AF1 /* UserListState+Observer.swift in Sources */,
@@ -11618,7 +11705,6 @@
4F45802E2BEE0B4B0099F540 /* ChannelListLinker.swift in Sources */,
AD0CC02E2BDC08E9005E2C66 /* ChatClient+ReactionListController.swift in Sources */,
841BA9F52BCE8089000C73E4 /* PollsEndpoints.swift in Sources */,
- 43D3F0F62841052600B74921 /* CallPayloads.swift in Sources */,
A30C3F20276B428F00DA5968 /* UnknownUserEvent.swift in Sources */,
40789D2129F6AC500018C2BB /* AppStateObserving.swift in Sources */,
4F427F6C2BA2F53200D92238 /* ConnectedUserState+Observer.swift in Sources */,
@@ -11644,6 +11730,7 @@
AD0CC0342BDC4A6B005E2C66 /* ReactionListController+Combine.swift in Sources */,
88206FC425B18C88009D086A /* ConnectionRepository.swift in Sources */,
79682C4B24BF37CB0071578E /* ChannelListPayload.swift in Sources */,
+ AD545E642D52827B008FD399 /* DraftListQuery.swift in Sources */,
79D6CE3725F7C84600BE2EEC /* ChatChannelWatcherListController+SwiftUI.swift in Sources */,
4042968929FACA6A0089126D /* AudioValuePercentageNormaliser.swift in Sources */,
DA9985EE24E175AA000E9885 /* ChannelCodingKeys.swift in Sources */,
@@ -11759,6 +11846,7 @@
A34ECB4827F5C9FA00A804C1 /* UserEvents_IntegrationTests.swift in Sources */,
AD6E32962BBB10890073831B /* ThreadListPayload_Tests.swift in Sources */,
8A0175F425013B6400570345 /* TypingEventSender_Tests.swift in Sources */,
+ AD545E872D5D805A008FD399 /* DraftEvents_Tests.swift in Sources */,
4F3554982C9C0F7200479229 /* StreamJSONDecoder_Tests.swift in Sources */,
C149B744282A61FF00F25BED /* NSManagedObject+Validation_Tests.swift in Sources */,
AD94905A2BF5702700E69224 /* ThreadsRepository_Tests.swift in Sources */,
@@ -11773,16 +11861,17 @@
4042969229FBF84B0089126D /* AudioSamplesProcessor_Tests.swift in Sources */,
A3F65E3627EB70E0003F6256 /* EventLogger.swift in Sources */,
DA8407332526003D005A0F62 /* UserListUpdater_Tests.swift in Sources */,
+ AD545E772D5BB3E0008FD399 /* DraftEndpoints_Tests.swift in Sources */,
C12DBE5C2A614F310045D9F0 /* ListDatabaseObserver+Sorting_Tests.swift in Sources */,
F69E7F7D24ED7562000F5252 /* CurrentUserController_Tests.swift in Sources */,
A30C3F22276B4F8800DA5968 /* UnknownUserEvent_Tests.swift in Sources */,
DA84074025260CA3005A0F62 /* UserListController_Tests.swift in Sources */,
+ AD545E852D5D7591008FD399 /* DraftListQuery_Tests.swift in Sources */,
C1EE53A727BA53F300B1A6CA /* Endpoint_Tests.swift in Sources */,
84A1D2F426AB221E00014712 /* ChannelEventsController_Tests.swift in Sources */,
88381E6E258259310047A6A3 /* FileUploadPayload_Tests.swift in Sources */,
AD6355162CE801AD009E498F /* MessageModerationDetailsDTO_Tests.swift in Sources */,
64F70D4B26257FD400C9F979 /* Error+InternetNotAvailable_Tests.swift in Sources */,
- AC82033A28C60B100002EFDD /* CallEndpoints_Tests.swift in Sources */,
4FB4AB9F2BAD6DBD00712C4E /* Chat_Tests.swift in Sources */,
84FD350827FD8BE300D68D85 /* ChatChannel_Tests.swift in Sources */,
F63CC37124E591990052844D /* EventObserver_Tests.swift in Sources */,
@@ -11839,6 +11928,7 @@
84D5BC71277B61B900A65C75 /* PinnedMessagesSortingKey_Tests.swift in Sources */,
88381E8725825A240047A6A3 /* AttachmentEndpoints_Tests.swift in Sources */,
AD142ACA2C739D6600ABCC1F /* Poll_Tests.swift in Sources */,
+ AD545E6B2D5650B5008FD399 /* DraftPayloads_Tests.swift in Sources */,
84DCB855269F56A7006CDF32 /* EventsController+SwiftUI_Tests.swift in Sources */,
79B5517724E595DA00CE9FEC /* CurrentUserPayloads_Tests.swift in Sources */,
40B345F829C46AE500B96027 /* AudioPlaybackRate_Tests.swift in Sources */,
@@ -11863,6 +11953,7 @@
88EA9AE825471EF4007EE76B /* MessageReactionRequestPayload_Tests.swift in Sources */,
64C80615262EDA9600B1F7AD /* ChatMessage_Tests.swift in Sources */,
F63CC37524E592DD0052844D /* MemberEventObserver_Tests.swift in Sources */,
+ AD545E832D5D0389008FD399 /* CurrentUserController+Drafts_Tests.swift in Sources */,
ADA9DB892BCEF06B00C4AE3B /* ThreadReadDTO_Tests.swift in Sources */,
792FCB4724A33CC2000290C7 /* EventDataProcessorMiddleware_Tests.swift in Sources */,
797EEA4A24FFC37600C81203 /* ConnectionStatus_Tests.swift in Sources */,
@@ -11902,10 +11993,11 @@
79B5517C24E6A1CA00CE9FEC /* MessagePayloads_Tests.swift in Sources */,
F6D61D9D2510B57F00EB0624 /* NSManagedObject_Tests.swift in Sources */,
79D6CE9525F7D72E00BE2EEC /* ChatChannelWatcherListController_Tests.swift in Sources */,
+ AD545E792D5BC14E008FD399 /* DraftMessagesRepository_Tests.swift in Sources */,
AD5BCCC92AB22A6600456CD9 /* Logger_Tests.swift in Sources */,
A34ECB5027F5CAF200A804C1 /* PinnedMessagesQuery_IntegrationTests.swift in Sources */,
79896D66250A6D1800BA8F1C /* ChannelReadUpdaterMiddleware_Tests.swift in Sources */,
- AC82033C28C61F640002EFDD /* CreateCallPayload_Tests.swift in Sources */,
+ AD545E8E2D5D827B008FD399 /* DraftUpdaterMiddleware_Tests.swift in Sources */,
796CBC6525FBAD12003299B0 /* Member_Tests.swift in Sources */,
A34ECB4E27F5CABD00A804C1 /* MemberEvents_IntegrationTests.swift in Sources */,
79DDF812249CD5AC002F4412 /* APIClient_Tests.swift in Sources */,
@@ -11971,6 +12063,7 @@
79CD959424F9381700E87377 /* MulticastDelegate_Tests.swift in Sources */,
C1C5345D29AFE526006F9AF4 /* ChannelRepository_Tests.swift in Sources */,
C11BAA4D2907EC7B004C5EA4 /* AuthenticationRepository_Tests.swift in Sources */,
+ AD545E7D2D5CFC15008FD399 /* ChannelController+Drafts_Tests.swift in Sources */,
C186BFA627A7F4E10099CCA6 /* AsyncOperation_Tests.swift in Sources */,
A3C7BAD127E4E02700BBF4FA /* ChannelListFilterScope_Tests.swift in Sources */,
DAD5C836250278AD0045117A /* ChannelController+Combine_Tests.swift in Sources */,
@@ -12022,6 +12115,7 @@
8459C9F42BFB929600F0D235 /* PollsRepository_Tests.swift in Sources */,
4F5151982BC407ED001B7152 /* UserList_Tests.swift in Sources */,
84CC56EC267B3F6B00DF2784 /* AnyAttachmentPayload_Tests.swift in Sources */,
+ AD545E812D5D0006008FD399 /* MessageController+Drafts_Tests.swift in Sources */,
88F7692B25837EE600BD36B0 /* AttachmentQueueUploader_Tests.swift in Sources */,
8819DFDE252622D900FD1A50 /* ModerationEndpoints_Tests.swift in Sources */,
4042969829FE92320089126D /* AudioAnalysisEngine_Tests.swift in Sources */,
@@ -12240,6 +12334,7 @@
C121E814274544AD00023E4C /* ChannelVisibilityEventMiddleware.swift in Sources */,
C121E815274544AD00023E4C /* EventDTOConverterMiddleware.swift in Sources */,
C121E816274544AD00023E4C /* EventType.swift in Sources */,
+ AD545E712D5A7463008FD399 /* DraftEvents.swift in Sources */,
AD70DC3A2ADEC3C400CFC3B7 /* MessageModerationDetailsDTO.swift in Sources */,
AD0CC0242BDBF715005E2C66 /* ReactionListUpdater.swift in Sources */,
4F8CA69B2CB665EB00EBEA2D /* EphemeralValuesContainer.swift in Sources */,
@@ -12294,6 +12389,7 @@
AD0E278F2BF789630037554F /* ThreadsRepository.swift in Sources */,
4FF2A80E2B8E011000941A64 /* ChatState+Observer.swift in Sources */,
C121E833274544AD00023E4C /* UserListPayload.swift in Sources */,
+ ADA03A222D64EFE900DFE048 /* DraftMessage.swift in Sources */,
C121E834274544AD00023E4C /* UserPayloads.swift in Sources */,
C121E835274544AD00023E4C /* CurrentUserPayloads.swift in Sources */,
C121E836274544AD00023E4C /* ChannelCodingKeys.swift in Sources */,
@@ -12313,6 +12409,7 @@
C121E840274544AE00023E4C /* MessageAttachmentPayload.swift in Sources */,
C121E841274544AE00023E4C /* FileUploadPayload.swift in Sources */,
C121E842274544AE00023E4C /* MutedChannelPayload.swift in Sources */,
+ AD545E742D5A79DA008FD399 /* DraftUpdaterMiddleware.swift in Sources */,
C121E843274544AE00023E4C /* RawJSON.swift in Sources */,
4F6B84112D008D6E005645B0 /* MemberUpdatePayload.swift in Sources */,
4F97F2782BA87E30001C4D66 /* MessageSearchState.swift in Sources */,
@@ -12413,6 +12510,7 @@
C189D7792AEBC6CD00D4B966 /* BackgroundDatabaseObserver.swift in Sources */,
40789D1A29F6AC500018C2BB /* AudioPlayerObserving.swift in Sources */,
4042969329FBF84B0089126D /* AudioSamplesProcessor_Tests.swift in Sources */,
+ AD545E632D52827B008FD399 /* DraftListQuery.swift in Sources */,
C173538F27D9F804008AC412 /* KeyedDecodingContainer+Array.swift in Sources */,
C121E879274544AF00023E4C /* ChannelReadDTO.swift in Sources */,
CFA41B6827DA952300427602 /* SystemEnvironment+XStreamClient.swift in Sources */,
@@ -12424,6 +12522,7 @@
C1E8AD5F278EF5F40041B775 /* AsyncOperation.swift in Sources */,
8413D2F62BDDAAFF005ADA4E /* PollVoteListController+SwiftUI.swift in Sources */,
AD78F9EE28EC718700BC0FCE /* URL+EnrichedURL.swift in Sources */,
+ ADFD391D2D47D07C00F8E1B1 /* DraftEndpoints.swift in Sources */,
ADEEB7F62BD168D500C76602 /* MessageReactionGroupDTO.swift in Sources */,
C121E87D274544AF00023E4C /* ChannelMemberListQueryDTO.swift in Sources */,
C121E87E274544AF00023E4C /* DeviceDTO.swift in Sources */,
@@ -12480,6 +12579,7 @@
4F910C6D2BEE1BDC00214EB9 /* UnreadMessageLookup.swift in Sources */,
404296DE2A0114900089126D /* StreamAudioQueuePlayer_Tests.swift in Sources */,
C121E89C274544B000023E4C /* DataController.swift in Sources */,
+ AD545E602D523CB0008FD399 /* DraftPayloads.swift in Sources */,
AD17CDFA27E4DB2700E0D092 /* PushProvider.swift in Sources */,
C121E89D274544B000023E4C /* UserSearchController.swift in Sources */,
C121E89E274544B000023E4C /* MessageSearchController.swift in Sources */,
@@ -12603,7 +12703,6 @@
C121E8E5274544B200023E4C /* Timers.swift in Sources */,
C121E8E6274544B200023E4C /* SystemEnvironment.swift in Sources */,
4F97F2712BA86491001C4D66 /* UserSearchState.swift in Sources */,
- 43D3F0F72841052600B74921 /* CallPayloads.swift in Sources */,
C121E8E7274544B200023E4C /* Bundle+Extensions.swift in Sources */,
C121E8E8274544B200023E4C /* OptionSet+Extensions.swift in Sources */,
40789D2029F6AC500018C2BB /* AudioPlaying.swift in Sources */,
@@ -12617,11 +12716,11 @@
C121E8EF274544B200023E4C /* MainQueue+Synchronous.swift in Sources */,
C121E8F0274544B200023E4C /* Dictionary+Extensions.swift in Sources */,
C121E8F1274544B200023E4C /* MultipartFormData.swift in Sources */,
+ AD545E672D53C271008FD399 /* DraftMessagesRepository.swift in Sources */,
C121E8F2274544B200023E4C /* SystemEnvironment+Version.swift in Sources */,
AD78F9EF28EC718D00BC0FCE /* EventBatcher.swift in Sources */,
4F8E53162B7F58BE008C0F9F /* Chat.swift in Sources */,
404296DB2A0112D00089126D /* AudioQueuePlayer.swift in Sources */,
- 43D3F0FA284106EE00B74921 /* CallEndpoints.swift in Sources */,
40A458EE2A03AC7C00C198F7 /* AVAsset+TotalAudioSamples.swift in Sources */,
AD0CC0132BDBC1BF005E2C66 /* ReactionListQuery.swift in Sources */,
4F427F672BA2F43200D92238 /* ConnectedUser.swift in Sources */,
@@ -12798,6 +12897,7 @@
C121EBC02746A1E900023E4C /* ZoomAnimator.swift in Sources */,
40824D362A1271D7003B61FD /* PillButton_Tests.swift in Sources */,
ADD328692C06B3AE00BAD0E9 /* ChatThreadListItemView.swift in Sources */,
+ AD4118832D5E1368000EF88E /* UILabel+highlightText.swift in Sources */,
40824D1E2A1271B9003B61FD /* PillButton.swift in Sources */,
AD78F9FD28EC735700BC0FCE /* SwiftyScanner.swift in Sources */,
C121EBC12746A1E900023E4C /* VideoPlaybackControlView.swift in Sources */,
diff --git a/StreamChatArtifacts.json b/StreamChatArtifacts.json
index de31592b387..3403bdf56c7 100644
--- a/StreamChatArtifacts.json
+++ b/StreamChatArtifacts.json
@@ -1 +1 @@
-{"4.7.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.7.0/StreamChat-All.zip","4.8.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.8.0/StreamChat-All.zip","4.9.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.9.0/StreamChat-All.zip","4.10.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.0/StreamChat-All.zip","4.10.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.1/StreamChat-All.zip","4.11.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.11.0/StreamChat-All.zip","4.12.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.12.0/StreamChat-All.zip","4.13.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.0/StreamChat-All.zip","4.13.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.1/StreamChat-All.zip","4.14.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.14.0/StreamChat-All.zip","4.15.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.0/StreamChat-All.zip","4.15.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.1/StreamChat-All.zip","4.16.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.16.0/StreamChat-All.zip","4.17.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.17.0/StreamChat-All.zip","4.18.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.18.0/StreamChat-All.zip","4.19.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.19.0/StreamChat-All.zip","4.20.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.20.0/StreamChat-All.zip","4.21.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.0/StreamChat-All.zip","4.21.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.1/StreamChat-All.zip","4.21.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.2/StreamChat-All.zip","4.22.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.22.0/StreamChat-All.zip","4.23.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.23.0/StreamChat-All.zip","4.24.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.0/StreamChat-All.zip","4.24.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.1/StreamChat-All.zip","4.25.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.0/StreamChat-All.zip","4.25.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.1/StreamChat-All.zip","4.26.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.26.0/StreamChat-All.zip","4.27.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.0/StreamChat-All.zip","4.27.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.1/StreamChat-All.zip","4.28.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.28.0/StreamChat-All.zip","4.29.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.29.0/StreamChat-All.zip","4.30.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.30.0/StreamChat-All.zip","4.31.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.31.0/StreamChat-All.zip","4.32.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.32.0/StreamChat-All.zip","4.33.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.33.0/StreamChat-All.zip","4.34.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.34.0/StreamChat-All.zip","4.35.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.0/StreamChat-All.zip","4.35.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.1/StreamChat-All.zip","4.35.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.2/StreamChat-All.zip","4.36.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.36.0/StreamChat-All.zip","4.37.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.0/StreamChat-All.zip","4.37.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.1/StreamChat-All.zip","4.38.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.38.0/StreamChat-All.zip","4.39.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.39.0/StreamChat-All.zip","4.40.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.40.0/StreamChat-All.zip","4.41.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.41.0/StreamChat-All.zip","4.42.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.42.0/StreamChat-All.zip","4.43.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.43.0/StreamChat-All.zip","4.44.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.44.0/StreamChat-All.zip","4.45.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.45.0/StreamChat-All.zip","4.46.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.46.0/StreamChat-All.zip","4.47.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.0/StreamChat-All.zip","4.47.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.1/StreamChat-All.zip","4.48.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.0/StreamChat-All.zip","4.48.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.1/StreamChat-All.zip","4.49.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.49.0/StreamChat-All.zip","4.50.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.50.0/StreamChat-All.zip","4.51.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.51.0/StreamChat-All.zip","4.52.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.52.0/StreamChat-All.zip","4.53.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.53.0/StreamChat-All.zip","4.54.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.54.0/StreamChat-All.zip","4.55.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.55.0/StreamChat-All.zip","4.56.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.0/StreamChat-All.zip","4.56.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.1/StreamChat-All.zip","4.57.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.57.0/StreamChat-All.zip","4.58.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.58.0/StreamChat-All.zip","4.59.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.59.0/StreamChat-All.zip","4.60.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.60.0/StreamChat-All.zip","4.61.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.61.0/StreamChat-All.zip","4.62.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.62.0/StreamChat-All.zip","4.63.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.63.0/StreamChat-All.zip","4.64.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.64.0/StreamChat-All.zip","4.65.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.65.0/StreamChat-All.zip","4.66.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.66.0/StreamChat-All.zip","4.67.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.67.0/StreamChat-All.zip","4.68.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.68.0/StreamChat-All.zip","4.69.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.69.0/StreamChat-All.zip","4.70.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.70.0/StreamChat-All.zip","4.71.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.71.0/StreamChat-All.zip","4.72.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.72.0/StreamChat-All.zip"}
\ No newline at end of file
+{"4.7.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.7.0/StreamChat-All.zip","4.8.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.8.0/StreamChat-All.zip","4.9.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.9.0/StreamChat-All.zip","4.10.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.0/StreamChat-All.zip","4.10.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.1/StreamChat-All.zip","4.11.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.11.0/StreamChat-All.zip","4.12.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.12.0/StreamChat-All.zip","4.13.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.0/StreamChat-All.zip","4.13.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.1/StreamChat-All.zip","4.14.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.14.0/StreamChat-All.zip","4.15.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.0/StreamChat-All.zip","4.15.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.1/StreamChat-All.zip","4.16.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.16.0/StreamChat-All.zip","4.17.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.17.0/StreamChat-All.zip","4.18.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.18.0/StreamChat-All.zip","4.19.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.19.0/StreamChat-All.zip","4.20.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.20.0/StreamChat-All.zip","4.21.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.0/StreamChat-All.zip","4.21.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.1/StreamChat-All.zip","4.21.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.2/StreamChat-All.zip","4.22.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.22.0/StreamChat-All.zip","4.23.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.23.0/StreamChat-All.zip","4.24.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.0/StreamChat-All.zip","4.24.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.1/StreamChat-All.zip","4.25.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.0/StreamChat-All.zip","4.25.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.1/StreamChat-All.zip","4.26.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.26.0/StreamChat-All.zip","4.27.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.0/StreamChat-All.zip","4.27.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.1/StreamChat-All.zip","4.28.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.28.0/StreamChat-All.zip","4.29.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.29.0/StreamChat-All.zip","4.30.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.30.0/StreamChat-All.zip","4.31.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.31.0/StreamChat-All.zip","4.32.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.32.0/StreamChat-All.zip","4.33.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.33.0/StreamChat-All.zip","4.34.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.34.0/StreamChat-All.zip","4.35.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.0/StreamChat-All.zip","4.35.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.1/StreamChat-All.zip","4.35.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.2/StreamChat-All.zip","4.36.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.36.0/StreamChat-All.zip","4.37.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.0/StreamChat-All.zip","4.37.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.1/StreamChat-All.zip","4.38.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.38.0/StreamChat-All.zip","4.39.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.39.0/StreamChat-All.zip","4.40.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.40.0/StreamChat-All.zip","4.41.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.41.0/StreamChat-All.zip","4.42.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.42.0/StreamChat-All.zip","4.43.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.43.0/StreamChat-All.zip","4.44.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.44.0/StreamChat-All.zip","4.45.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.45.0/StreamChat-All.zip","4.46.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.46.0/StreamChat-All.zip","4.47.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.0/StreamChat-All.zip","4.47.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.1/StreamChat-All.zip","4.48.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.0/StreamChat-All.zip","4.48.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.1/StreamChat-All.zip","4.49.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.49.0/StreamChat-All.zip","4.50.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.50.0/StreamChat-All.zip","4.51.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.51.0/StreamChat-All.zip","4.52.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.52.0/StreamChat-All.zip","4.53.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.53.0/StreamChat-All.zip","4.54.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.54.0/StreamChat-All.zip","4.55.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.55.0/StreamChat-All.zip","4.56.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.0/StreamChat-All.zip","4.56.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.1/StreamChat-All.zip","4.57.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.57.0/StreamChat-All.zip","4.58.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.58.0/StreamChat-All.zip","4.59.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.59.0/StreamChat-All.zip","4.60.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.60.0/StreamChat-All.zip","4.61.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.61.0/StreamChat-All.zip","4.62.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.62.0/StreamChat-All.zip","4.63.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.63.0/StreamChat-All.zip","4.64.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.64.0/StreamChat-All.zip","4.65.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.65.0/StreamChat-All.zip","4.66.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.66.0/StreamChat-All.zip","4.67.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.67.0/StreamChat-All.zip","4.68.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.68.0/StreamChat-All.zip","4.69.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.69.0/StreamChat-All.zip","4.70.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.70.0/StreamChat-All.zip","4.71.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.71.0/StreamChat-All.zip","4.72.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.72.0/StreamChat-All.zip","4.73.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.73.0/StreamChat-All.zip"}
\ No newline at end of file
diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec
index d05acfdd9d6..888b626c40f 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.72.0"
+ spec.version = "4.73.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 c8103e99eab..82a96c19958 100644
--- a/StreamChatUI.podspec
+++ b/StreamChatUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChatUI"
- spec.version = "4.72.0"
+ spec.version = "4.73.0"
spec.summary = "StreamChat UI Components"
spec.description = "StreamChatUI SDK offers flexible UI components able to display data provided by StreamChat SDK."
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json
index 74b886d501f..51037ce2fd3 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json
@@ -1,11 +1,11 @@
{
"channel": {
- "id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"type": "messaging",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "last_message_at": "2025-01-31T16:18:37.094228Z",
- "created_at": "2025-01-31T16:18:35.560277Z",
- "updated_at": "2025-01-31T16:18:35.560277Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "last_message_at": "2025-02-15T00:14:54.796665Z",
+ "created_at": "2025-02-15T00:14:50.645951Z",
+ "updated_at": "2025-02-15T00:14:50.645951Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -19,21 +19,21 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
"frozen": false,
"disabled": false,
"member_count": 4,
"config": {
"created_at": "2021-03-01T19:26:18.406502Z",
- "updated_at": "2024-11-13T11:49:47.368244Z",
+ "updated_at": "2025-02-13T14:46:16.795117Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
@@ -103,6 +103,7 @@
"send-poll",
"send-reaction",
"send-reply",
+ "send-restricted-visibility-message",
"send-typing-events",
"set-channel-cooldown",
"skip-slow-mode",
@@ -133,15 +134,15 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -163,15 +164,15 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -193,18 +194,18 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -226,23 +227,23 @@
"updated_at": "2025-01-16T12:34:56.765249Z",
"banned": false,
"online": false,
- "last_active": "2025-01-29T16:35:31.022088Z",
+ "last_active": "2025-02-14T13:12:46.241933Z",
"blocked_user_ids": [
],
"birthland": "Polis Massa",
"private_settings": {
- "readReceipts": {
+ "typingIndicators": {
"enabled": false
},
- "typingIndicators": {
+ "readReceipts": {
"enabled": false
}
}
},
"status": "member",
- "created_at": "2025-01-31T16:18:37.521245Z",
- "updated_at": "2025-01-31T16:18:37.521245Z",
+ "created_at": "2025-02-15T00:14:56.183774Z",
+ "updated_at": "2025-02-15T00:14:56.183774Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -250,5 +251,5 @@
"notifications_muted": false
}
],
- "duration": "48.95ms"
+ "duration": "41.93ms"
}
\ No newline at end of file
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json
index 87e398ab0bf..9eeca2efef8 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_attachment.json
@@ -1,4 +1,4 @@
{
- "file": "https://frankfurt.stream-io-cdn.com/102399/images/da9d0ac2-77f1-47ae-b369-8a80dde644be.yoda.jpg?Key-Pair-Id=APKAIHG36VEWPDULE23Q&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9mcmFua2Z1cnQuc3RyZWFtLWlvLWNkbi5jb20vMTAyMzk5L2ltYWdlcy9kYTlkMGFjMi03N2YxLTQ3YWUtYjM2OS04YTgwZGRlNjQ0YmUueW9kYS5qcGc~Km9oPTAqb3c9MCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3Mzk1NDk5MTd9fX1dfQ__&Signature=Vn6bN5uCpQcw9TrIHzsG0IVOPezLsg8paeE3HQwK4SkUdqUWLQaAMDiEOcW9xsNbTLW3WZArI9buSklsJSXbD9Q1vjML9L3zXBJswGaYTHDrD9OOn4MRNpeHz~t8rCx55fMNwmAp7wX2evCdwVHS315GnWHsM6yl8i8CzsFXSxUggZI0Y207SEvBzc0ZUOxEUiFfYuGEYpwVmX9q2S2azEiqVB4Hprt6i4guXme-ESsikDF4mvy3cC7XKm5kNJw7IXgXOzyU9pPlQMyS-QvdfXtCWvBgQezwBnXFQwyrCOphB4GsyQ9jOIq4wL86LkwLex2ia8WN1qaWt1Ik2qK6mA__&oh=0&ow=0",
- "duration": "115.34ms"
+ "file": "https://frankfurt.stream-io-cdn.com/102399/images/089374d3-ab0d-498b-8d95-49dc5b7b53e8.yoda.jpg?Key-Pair-Id=APKAIHG36VEWPDULE23Q&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9mcmFua2Z1cnQuc3RyZWFtLWlvLWNkbi5jb20vMTAyMzk5L2ltYWdlcy8wODkzNzRkMy1hYjBkLTQ5OGItOGQ5NS00OWRjNWI3YjUzZTgueW9kYS5qcGc~Km9oPTAqb3c9MCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3NDA3ODgwOTZ9fX1dfQ__&Signature=UuBQWQmVcSzQM94oxRbgpxaQxfn0nMmv69b3-ziRM27g38RANQubgSq~9a~o86R4NbSZmIejBo8Hmb53uK39N57HzXuWPAAmW6M2uZE4Ytg7UVDvLQAch-Jwa6R50g4RvvpJbDHHZKz3itjl46q3~WmRfmwq-EDsei8qSON-ZW-bYNcZ67yQ3pKP7QxAK57cXyTFzl7TjlSgJ16K2wIcz~q24cSwNVsC5JeiqofY7b6DU69zNch7p0qF61YzY-7CAaRl9o9FP6kcEHsItd5czY9DIuOdwsDLSYUBwcxjnddDd-yS6HTXKPZskKPoH5Ygz1nJybCd7OfDJWc69y61Ag__&oh=0&ow=0",
+ "duration": "130.61ms"
}
\ 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 676ae1ce815..aa43bef1d5c 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_creation.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_creation.json
@@ -1,10 +1,10 @@
{
"channel": {
- "id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"type": "messaging",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:35.560277Z",
- "updated_at": "2025-01-31T16:18:35.560277Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:50.645951Z",
+ "updated_at": "2025-02-15T00:14:50.645951Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -18,21 +18,21 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
"team": "test",
- "type": "team"
+ "type": "team",
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine"
},
"frozen": false,
"disabled": false,
"member_count": 3,
"config": {
"created_at": "2021-03-01T19:26:18.406502Z",
- "updated_at": "2024-11-13T11:49:47.368244Z",
+ "updated_at": "2025-02-13T14:46:16.795117Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
@@ -102,6 +102,7 @@
"send-poll",
"send-reaction",
"send-reply",
+ "send-restricted-visibility-message",
"send-typing-events",
"set-channel-cooldown",
"skip-slow-mode",
@@ -139,13 +140,13 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
- "last_read": "2025-01-31T16:18:35.584603445Z",
+ "last_read": "2025-02-15T00:14:50.728264225Z",
"unread_messages": 0
},
{
@@ -162,7 +163,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -171,7 +172,7 @@
"birthland": "Tatooine",
"team": "test"
},
- "last_read": "2025-01-31T16:18:35.584603445Z",
+ "last_read": "2025-02-15T00:14:50.728264225Z",
"unread_messages": 0
}
],
@@ -191,15 +192,15 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -221,15 +222,15 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -251,18 +252,18 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
- "team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -284,7 +285,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -294,8 +295,8 @@
"team": "test"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -305,5 +306,5 @@
"threads": [
],
- "duration": "65.79ms"
+ "duration": "134.36ms"
}
\ No newline at end of file
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json
index dd8f1ad1fa5..dc1b2e92a05 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json
@@ -1,12 +1,12 @@
{
- "duration": "30.73ms",
+ "duration": "79.06ms",
"channel": {
- "id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"type": "messaging",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:35.560277Z",
- "updated_at": "2025-01-31T16:18:39.67828Z",
- "deleted_at": "2025-01-31T16:18:39.924361Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:50.645951Z",
+ "updated_at": "2025-02-15T00:15:02.086477Z",
+ "deleted_at": "2025-02-15T00:15:02.914267Z",
"created_by": null,
"frozen": false,
"disabled": false,
@@ -26,15 +26,15 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -56,15 +56,15 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -86,18 +86,18 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
+ "team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
- "team": "test"
+ "birthland": "Tatooine"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -119,10 +119,11 @@
"updated_at": "2025-01-16T12:34:56.765249Z",
"banned": false,
"online": false,
- "last_active": "2025-01-29T16:35:31.022088Z",
+ "last_active": "2025-02-14T13:12:46.241933Z",
"blocked_user_ids": [
],
+ "birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
"enabled": false
@@ -130,12 +131,11 @@
"typingIndicators": {
"enabled": false
}
- },
- "birthland": "Polis Massa"
+ }
},
"status": "member",
- "created_at": "2025-01-31T16:18:37.521245Z",
- "updated_at": "2025-01-31T16:18:37.521245Z",
+ "created_at": "2025-02-15T00:14:56.183774Z",
+ "updated_at": "2025-02-15T00:14:56.183774Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -145,7 +145,7 @@
],
"config": {
"created_at": "2021-03-01T19:26:18.406502Z",
- "updated_at": "2024-11-13T11:49:47.368244Z",
+ "updated_at": "2025-02-13T14:46:16.795117Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
@@ -192,7 +192,7 @@
}
]
},
- "truncated_at": "2025-01-31T16:18:39.924361Z",
+ "truncated_at": "2025-02-15T00:15:02.914267Z",
"truncated_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -206,14 +206,14 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
}
}
}
\ 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 9fca117f4a2..d874ab9f87f 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json
@@ -2,11 +2,11 @@
"channels": [
{
"channel": {
- "id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"type": "messaging",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:35.560277Z",
- "updated_at": "2025-01-31T16:18:35.560277Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:50.645951Z",
+ "updated_at": "2025-02-15T00:14:50.645951Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -20,21 +20,21 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
"frozen": false,
"disabled": false,
"member_count": 3,
"config": {
"created_at": "2021-03-01T19:26:18.406502Z",
- "updated_at": "2024-11-13T11:49:47.368244Z",
+ "updated_at": "2025-02-13T14:46:16.795117Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
@@ -104,6 +104,7 @@
"send-poll",
"send-reaction",
"send-reply",
+ "send-restricted-visibility-message",
"send-typing-events",
"set-channel-cooldown",
"skip-slow-mode",
@@ -141,14 +142,14 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
"unread_messages": 0,
- "last_read": "2025-01-31T16:18:35Z"
+ "last_read": "2025-02-15T00:14:51Z"
},
{
"user": {
@@ -164,14 +165,14 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
"birthland": "Corellia"
},
"unread_messages": 0,
- "last_read": "2025-01-31T16:18:35Z"
+ "last_read": "2025-02-15T00:14:51Z"
},
{
"user": {
@@ -187,17 +188,17 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"unread_messages": 0,
- "last_read": "2025-01-31T16:18:35Z"
+ "last_read": "2025-02-15T00:14:51Z"
}
],
"members": [
@@ -216,15 +217,15 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -246,15 +247,15 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -276,18 +277,18 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -309,18 +310,18 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"channel_role": "channel_member",
@@ -331,5 +332,5 @@
]
}
],
- "duration": "121.59ms"
+ "duration": "121.48ms"
}
\ 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 0e684119d8d..48c06f29456 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_events.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_events.json
@@ -1,8 +1,8 @@
{
"event": {
"type": "typing.start",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "channel_id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "channel_id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"channel_type": "messaging",
"user": {
"id": "luke_skywalker",
@@ -17,16 +17,16 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
- "created_at": "2025-01-31T16:18:36.200384029Z"
+ "created_at": "2025-02-15T00:14:52.576220416Z"
},
- "duration": "6.70ms"
+ "duration": "6.35ms"
}
\ No newline at end of file
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_giphy_link.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_giphy_link.json
index f84ade5ea03..3e18620c9b7 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_giphy_link.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_giphy_link.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "b78d492d-b171-4209-97fb-2327c8bfe34d",
+ "id": "779ca24a-b661-46fa-893e-e4e87eb6b46a",
"text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
"html": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi
\n",
"type": "regular",
@@ -17,14 +17,14 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
+ "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
- "team": "test",
- "type": "team"
+ "team": "test"
},
"attachments": [
{
@@ -32,9 +32,9 @@
"title": "Test Computer GIF - Find & Share on GIPHY",
"title_link": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
"text": "Discover & share this Test Computer GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.",
- "image_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdnN6bThhZnN4bHQ3dTl3YzA2dDB6ZWhqcm4weGJtNzR6b2p0eHN2MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
- "thumb_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdnN6bThhZnN4bHQ3dTl3YzA2dDB6ZWhqcm4weGJtNzR6b2p0eHN2MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
- "asset_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdnN6bThhZnN4bHQ3dTl3YzA2dDB6ZWhqcm4weGJtNzR6b2p0eHN2MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.mp4",
+ "image_url": "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmMwOHM4ajZzemdzemlkeHg2dHV5cnlvbnhyczFxYXB0eDF2NGw5ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
+ "thumb_url": "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmMwOHM4ajZzemdzemlkeHg2dHV5cnlvbnhyczFxYXB0eDF2NGw5ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
+ "asset_url": "https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmMwOHM4ajZzemdzemlkeHg2dHV5cnlvbnhyczFxYXB0eDF2NGw5ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.mp4",
"og_scrape_url": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi"
}
],
@@ -50,18 +50,26 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:39.448734Z",
- "updated_at": "2025-01-31T16:18:39.448734Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:15:01.733573Z",
+ "updated_at": "2025-02-15T00:15:01.733573Z",
"shadowed": false,
"mentioned_users": [
],
+ "i18n": {
+ "id_text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
+ "ru_text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
+ "language": "id"
+ },
"silent": false,
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
},
- "duration": "217.62ms"
+ "duration": "414.19ms"
}
\ 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 d390b1059ea..40b056ae68d 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -17,7 +17,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -41,9 +41,9 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:37.094228Z",
- "updated_at": "2025-01-31T16:18:37.094228Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:54.796665Z",
+ "updated_at": "2025-02-15T00:14:54.796665Z",
"shadowed": false,
"mentioned_users": [
@@ -52,7 +52,10 @@
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
},
- "duration": "736.77ms"
+ "duration": "1673.28ms"
}
\ 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 bfa31c20fec..3ce1d660530 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message_ephemeral.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message_ephemeral.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "40ee6375-ab6f-4dd1-885b-6f37b7053420",
+ "id": "8ab9f3aa-85f3-4c28-921b-360b75d1bfe1",
"text": "/giphy Test",
"command": "giphy",
"html": "/giphy Test
\n",
@@ -18,7 +18,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -31,8 +31,8 @@
{
"type": "giphy",
"title": "Test",
- "title_link": "https://giphy.com/gifs/school-test-annoyed-cJ7gVV0QL9RPG",
- "thumb_url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/giphy.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=giphy.gif&ct=g",
+ "title_link": "https://giphy.com/gifs/test-video-monicawut-YLIafuhsghrxWHXEVE",
+ "thumb_url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/giphy.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=giphy.gif&ct=g",
"actions": [
{
"name": "image_action",
@@ -58,52 +58,52 @@
],
"giphy": {
"original": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/giphy.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=giphy.gif&ct=g",
- "width": "250",
- "height": "157",
- "size": "509090",
- "frames": "25"
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/giphy.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=giphy.gif&ct=g",
+ "width": "480",
+ "height": "270",
+ "size": "204947",
+ "frames": "41"
},
"fixed_height": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/200.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=200.gif&ct=g",
- "width": "318",
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/200.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=200.gif&ct=g",
+ "width": "356",
"height": "200",
- "size": "568679",
+ "size": "127618",
"frames": ""
},
"fixed_height_still": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/200_s.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=200_s.gif&ct=g",
- "width": "318",
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/200_s.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=200_s.gif&ct=g",
+ "width": "356",
"height": "200",
- "size": "23937",
+ "size": "13850",
"frames": ""
},
"fixed_height_downsampled": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/200_d.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=200_d.gif&ct=g",
- "width": "318",
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/200_d.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=200_d.gif&ct=g",
+ "width": "356",
"height": "200",
- "size": "152752",
+ "size": "48900",
"frames": ""
},
"fixed_width": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/200w.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=200w.gif&ct=g",
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/200w.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=200w.gif&ct=g",
"width": "200",
- "height": "126",
- "size": "387570",
+ "height": "113",
+ "size": "56861",
"frames": ""
},
"fixed_width_still": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/200w_s.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=200w_s.gif&ct=g",
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/200w_s.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=200w_s.gif&ct=g",
"width": "200",
- "height": "126",
- "size": "15905",
+ "height": "113",
+ "size": "6521",
"frames": ""
},
"fixed_width_downsampled": {
- "url": "https://media1.giphy.com/media/cJ7gVV0QL9RPG/200w_d.gif?cid=c4b036753mf32agujfmseqxlghr13nakqw3bgfmvrgjesayg&ep=v1_gifs_search&rid=200w_d.gif&ct=g",
+ "url": "https://media1.giphy.com/media/YLIafuhsghrxWHXEVE/200w_d.gif?cid=c4b03675wluerfv48eadtr30pghqzt4uhv684615i2lkoygs&ep=v1_gifs_search&rid=200w_d.gif&ct=g",
"width": "200",
- "height": "126",
- "size": "89634",
+ "height": "113",
+ "size": "20122",
"frames": ""
}
}
@@ -121,22 +121,30 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:38.068878Z",
- "updated_at": "2025-01-31T16:18:38.068878Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:57.857068Z",
+ "updated_at": "2025-02-15T00:14:57.857068Z",
"shadowed": false,
"mentioned_users": [
],
+ "i18n": {
+ "id_text": "/giphy Test",
+ "ru_text": "/giphy Testo",
+ "language": "id"
+ },
"silent": false,
"pinned": false,
"pinned_at": null,
"pinned_by": null,
"pin_expires": null,
+ "restricted_visibility": [
+
+ ],
"args": "Test",
"command_info": {
"name": "Giphy"
}
},
- "duration": "34.12ms"
+ "duration": "315.67ms"
}
\ 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 09481e7d332..5b57f5e53b8 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -17,7 +17,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -31,7 +31,7 @@
],
"latest_reactions": [
{
- "message_id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "message_id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
@@ -46,7 +46,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -57,13 +57,13 @@
},
"type": "like",
"score": 1,
- "created_at": "2025-01-31T16:18:37.304432Z",
- "updated_at": "2025-01-31T16:18:37.304432Z"
+ "created_at": "2025-02-15T00:14:55.545757Z",
+ "updated_at": "2025-02-15T00:14:55.545757Z"
}
],
"own_reactions": [
{
- "message_id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "message_id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
@@ -78,7 +78,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -89,8 +89,8 @@
},
"type": "like",
"score": 1,
- "created_at": "2025-01-31T16:18:37.304432Z",
- "updated_at": "2025-01-31T16:18:37.304432Z"
+ "created_at": "2025-02-15T00:14:55.545757Z",
+ "updated_at": "2025-02-15T00:14:55.545757Z"
}
],
"reaction_counts": {
@@ -103,15 +103,15 @@
"like": {
"count": 1,
"sum_scores": 1,
- "first_reaction_at": "2025-01-31T16:18:37.304432Z",
- "last_reaction_at": "2025-01-31T16:18:37.304432Z"
+ "first_reaction_at": "2025-02-15T00:14:55.545757Z",
+ "last_reaction_at": "2025-02-15T00:14:55.545757Z"
}
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:37.094228Z",
- "updated_at": "2025-01-31T16:18:37.310539Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:54.796665Z",
+ "updated_at": "2025-02-15T00:14:55.555595Z",
"shadowed": false,
"mentioned_users": [
@@ -120,10 +120,13 @@
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
},
"reaction": {
- "message_id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "message_id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
@@ -138,7 +141,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -149,8 +152,8 @@
},
"type": "like",
"score": 1,
- "created_at": "2025-01-31T16:18:37.304432Z",
- "updated_at": "2025-01-31T16:18:37.304432Z"
+ "created_at": "2025-02-15T00:14:55.545757Z",
+ "updated_at": "2025-02-15T00:14:55.545757Z"
},
- "duration": "25.87ms"
+ "duration": "30.29ms"
}
\ No newline at end of file
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json
index 89cbffa70e8..d7427d6c6f2 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json
@@ -1,12 +1,12 @@
{
- "duration": "57.01ms",
+ "duration": "229.96ms",
"channel": {
- "id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"type": "messaging",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"last_message_at": "0001-01-01T00:00:00Z",
- "created_at": "2025-01-31T16:18:35.560277Z",
- "updated_at": "2025-01-31T16:18:39.67828Z",
+ "created_at": "2025-02-15T00:14:50.645951Z",
+ "updated_at": "2025-02-15T00:15:02.086477Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -20,14 +20,14 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
"frozen": false,
"disabled": false,
@@ -47,15 +47,15 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -77,15 +77,15 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -107,7 +107,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
@@ -117,8 +117,8 @@
"birthland": "Tatooine"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -140,11 +140,10 @@
"updated_at": "2025-01-16T12:34:56.765249Z",
"banned": false,
"online": false,
- "last_active": "2025-01-29T16:35:31.022088Z",
+ "last_active": "2025-02-14T13:12:46.241933Z",
"blocked_user_ids": [
],
- "birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
"enabled": false
@@ -152,11 +151,12 @@
"typingIndicators": {
"enabled": false
}
- }
+ },
+ "birthland": "Polis Massa"
},
"status": "member",
- "created_at": "2025-01-31T16:18:37.521245Z",
- "updated_at": "2025-01-31T16:18:37.521245Z",
+ "created_at": "2025-02-15T00:14:56.183774Z",
+ "updated_at": "2025-02-15T00:14:56.183774Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -167,7 +167,7 @@
"member_count": 4,
"config": {
"created_at": "2021-03-01T19:26:18.406502Z",
- "updated_at": "2024-11-13T11:49:47.368244Z",
+ "updated_at": "2025-02-13T14:46:16.795117Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
@@ -214,7 +214,7 @@
}
]
},
- "truncated_at": "2025-01-31T16:18:39.672019Z",
+ "truncated_at": "2025-02-15T00:15:02.05596Z",
"truncated_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -228,7 +228,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
@@ -240,7 +240,7 @@
"name": "Sync Mock Server"
},
"message": {
- "id": "af2b17fe-a3d3-47de-bd9b-92eb08ae9877",
+ "id": "9ade079c-db9c-408a-a127-53a5dcfd95f6",
"text": "Channel truncated",
"html": "Channel truncated
\n",
"type": "system",
@@ -257,14 +257,14 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.979000026Z",
"blocked_user_ids": [
],
+ "pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
"team": "test",
- "type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}"
+ "type": "team"
},
"attachments": [
@@ -281,9 +281,9 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:39.67202Z",
- "updated_at": "2025-01-31T16:18:39.67202Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:15:02.055961Z",
+ "updated_at": "2025-02-15T00:15:02.055961Z",
"shadowed": false,
"mentioned_users": [
@@ -292,6 +292,9 @@
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
}
}
\ 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 11db691bed6..f0b725aa231 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "6a3952aa-9e3e-4c1a-8f41-f6c8223b6b94",
+ "id": "90a69671-5806-40ff-9d53-4babfed29140",
"text": "https://unsplash.com/photos/1_2d3MRbI9c",
"html": "https://unsplash.com/photos/1_2d3MRbI9c
\n",
"type": "regular",
@@ -17,7 +17,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:59.541924866Z",
"blocked_user_ids": [
],
@@ -32,8 +32,8 @@
"title": "Photo by Joao Branco on Unsplash",
"title_link": "https://unsplash.com/photos/green-pine-tree-mountain-slope-scenery-1_2d3MRbI9c",
"text": "Download this photo by Joao Branco on Unsplash",
- "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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzM4MzMzNTI0fA&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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzM4MzMzNTI0fA&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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzM5NTc4NTAwfA&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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzM5NTc4NTAwfA&ixlib=rb-4.0.3",
"og_scrape_url": "https://unsplash.com/photos/1_2d3MRbI9c"
}
],
@@ -49,18 +49,26 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:39.023537Z",
- "updated_at": "2025-01-31T16:18:39.023537Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:15:00.705081Z",
+ "updated_at": "2025-02-15T00:15:00.705081Z",
"shadowed": false,
"mentioned_users": [
],
+ "i18n": {
+ "id_text": "https://unsplash.com/photos/1_2d3MRbI9c",
+ "ru_text": "https://unsplash.com/photos/1_2d3MRbI9c",
+ "language": "id"
+ },
"silent": false,
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
},
- "duration": "290.16ms"
+ "duration": "754.21ms"
}
\ 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 de0d8883a02..e9aa2f91bfc 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_youtube_link.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_youtube_link.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "29b0f068-c354-463c-922b-b381913da5df",
+ "id": "2eeec5e4-d9f4-4f4b-ac83-fd48ebc40e69",
"text": "https://youtube.com/watch?v=xOX7MsrbaPY",
"html": "https://youtube.com/watch?v=xOX7MsrbaPY
\n",
"type": "regular",
@@ -17,7 +17,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -51,18 +51,26 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:38.512552Z",
- "updated_at": "2025-01-31T16:18:38.512552Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:59.328297Z",
+ "updated_at": "2025-02-15T00:14:59.328297Z",
"shadowed": false,
"mentioned_users": [
],
+ "i18n": {
+ "id_text": "https://youtube.com/watch?v=xOX7MsrbaPY",
+ "ru_text": "https://youtube.com/watch?v=xOX7MsrbaPY",
+ "language": "id"
+ },
"silent": false,
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
},
- "duration": "280.08ms"
+ "duration": "1186.74ms"
}
\ 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 4644bfec1a0..2aa8e3f11c0 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events.json
@@ -1,7 +1,7 @@
{
"type": "typing.start",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "channel_id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "channel_id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"channel_type": "messaging",
"user": {
"id": "luke_skywalker",
@@ -16,7 +16,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -33,186 +33,186 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
- "created_at": "2025-01-29T12:48:17.917988Z",
+ "id": "e2c6bed363247a93ea49699385e50de99efba5707808cbd7b2d3f4e46a7cbfb9",
+ "created_at": "2025-02-13T17:54:30.965611Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
- "created_at": "2025-01-29T11:17:58.405065Z",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-02-12T13:52:59.060649Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "52c03e3cd1477169d34772e1755a6009448b9115b3fe99fc6f50b4e8ded1a397",
- "created_at": "2025-01-27T14:10:52.981213Z",
- "disabled": true,
- "disabled_reason": "Unregistered",
+ "id": "805fbd71d240360c14b785cd89f59846acf40e15514e3cf89efdbc30f360999873c4713b6fd6f192072c075c41243b82f8b89ea4880f7694f72815934477e7cf8d80c303d7bb25c8c0730e6268a1e3d2",
+ "created_at": "2025-02-12T10:55:34.960205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
- "created_at": "2025-01-27T09:26:50.733298Z",
+ "id": "38cf0b02ebe66d4bbc17981502acf8ef7806337f45e3094fbf662293185dd663",
+ "created_at": "2025-02-12T10:45:49.811205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
- "created_at": "2025-01-27T07:18:18.62698Z",
+ "id": "2716b6f605e691bf7dc7d88d1a45968dd6807ed91dbbf793777d152f796994b0",
+ "created_at": "2025-02-11T01:03:34.75674Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
- "created_at": "2025-01-26T19:17:41.00587Z",
+ "id": "809975048776300710e9bf304eabc27813c3042266dc085420362ea254ebeccea39666720bc25ffc5a022b79062f1e5f1f1de6d1565850cc78ba3c9df1a810e7d4128b73c2f76210189febf4ee0df9b1",
+ "created_at": "2025-02-10T01:52:00.239203Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
- "created_at": "2025-01-26T18:56:24.995825Z",
+ "id": "8082e4f123b0f83543d1f7c04920c76ec7d4b4db9d8f2e8ff0cc4daeef338209be72df2b35257cfdb86623c14d241cf1d1f42b78fef01e16052d13b03f75ed135360886f1900d3a0e80e97b268514c77",
+ "created_at": "2025-02-09T10:04:50.694565Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
- "created_at": "2025-01-24T15:23:31.160845Z",
+ "id": "809a57e2ed4470cb0b372e79d31cff19276879f5b312b3b71e51fc05ad338bbda27220c4f36c1283e0340d6b8816702d123dda05604dfc0d9a2eae03099f718db1f14f13de887394280380cbcb945223",
+ "created_at": "2025-02-08T13:42:01.054257Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
- "created_at": "2025-01-24T05:36:02.440032Z",
+ "id": "800dd2e8d35942e0a2124b71e87dcb54467f63b148190dbd04821e71100924fdcf58d8cc17eb1030529f07bfaae1bc5c2f9313139067e7392edf1fdadf837762409698640194be2ca788aa9b5660ff4d",
+ "created_at": "2025-02-07T09:29:01.123928Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
- "created_at": "2025-01-23T05:30:44.092337Z",
+ "id": "8009a9c2db8394cdaa8ebd8f0f5731ba1fe28c3cc89397b8e38b0d55b018cafead52512e0d9dfbd10ca9abaacbfa1e96586f1e2c6a4f831977607fa415188249f6d88b7369e476554f0ce8a23e033f49",
+ "created_at": "2025-02-06T07:45:34.135591Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
- "created_at": "2025-01-22T04:43:11.923746Z",
+ "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
+ "created_at": "2025-01-29T12:48:17.917988Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
- "created_at": "2025-01-21T03:06:34.117688Z",
+ "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
+ "created_at": "2025-01-29T11:17:58.405065Z",
+ "disabled": true,
+ "disabled_reason": "Unregistered",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
- "created_at": "2025-01-15T13:18:37.405547Z",
+ "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
+ "created_at": "2025-01-27T09:26:50.733298Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
- "created_at": "2025-01-15T07:59:32.530759Z",
+ "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
+ "created_at": "2025-01-27T07:18:18.62698Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
- "created_at": "2025-01-14T20:36:03.102001Z",
+ "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
+ "created_at": "2025-01-26T19:17:41.00587Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-01-14T17:54:05.295746Z",
+ "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
+ "created_at": "2025-01-26T18:56:24.995825Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "2c0a2b1f6891af16b45df24c7b05b1598d414f64fae7c5ef3eb41d92b45bc275",
- "created_at": "2025-01-13T06:09:35.973958Z",
+ "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
+ "created_at": "2025-01-24T15:23:31.160845Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "f1f43292dc972188ccf2bb5a2523342482280cd37e6e80540c7f41890b8f5868",
- "created_at": "2025-01-13T02:06:21.211287Z",
+ "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
+ "created_at": "2025-01-24T05:36:02.440032Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80560b4f465af52f745b646beaeba703a1877ef6988eb06f38b5f023e6affb4f6a8d8c22123c53767d2f17fae4bfa1df218117400a0d175ffd2066bae361bab07d589bbfd9ae5e693e8af34476625f58",
- "created_at": "2025-01-12T12:18:40.312161Z",
+ "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
+ "created_at": "2025-01-23T05:30:44.092337Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8009ae3cb1eb8350aa78dfe4539e5858be8c0de73b66cccdb2baff95b211f3c13c56c9e9e837c9a44af93b9b631d5db658bfed4d899ba8ee773898acf49956d5df2638f37191fa5262572b75108cc075",
- "created_at": "2025-01-10T14:43:30.030261Z",
+ "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
+ "created_at": "2025-01-22T04:43:11.923746Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809155ab6e601835c405261f32a7046cc265d8899ffe5d3d484df422424101cba27d22e11531645c4c7d660a9b2c69641c9884622d08ba27b51458a03b67995e21791ba038a40f670fdcaa3a8f73403b",
- "created_at": "2025-01-10T12:25:15.095949Z",
+ "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
+ "created_at": "2025-01-21T03:06:34.117688Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a416c7a38c0b0e5f4cdc67cc22db4429b1abf069719b5fd249e067bd40f39a93c9ae72186cd4dbe9ee3f7d4b75b0572f6fcb6fd61f1447924bb5049e49d64111494690ab89f39ae64f13828d692357",
- "created_at": "2025-01-08T21:35:48.016721Z",
+ "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
+ "created_at": "2025-01-15T13:18:37.405547Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80677e1cd0e34efdaf88b76a3bb2ce7086c4c20853c3b1ca7c638a1da310ffd884491071d224cacd90e72a0116dcc1c3ebaab19a79e7eabb560f7ca21458945ff1a085e43a8b41d491f3eec625186aa1",
- "created_at": "2025-01-08T16:07:56.454603Z",
+ "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
+ "created_at": "2025-01-15T07:59:32.530759Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80d2897aafd7c4a6e382d80212ba469b5348669930fd34db0f8aae76f7059db340f7118963ef89d109480f2cb83679e7dd71a5766b764a8a546ff958f66f5ddbf17140658079e329f170bf9e47f68ae5",
- "created_at": "2025-01-08T15:16:15.675842Z",
+ "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
+ "created_at": "2025-01-14T20:36:03.102001Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca9df3ad609fa3e61a033f403e3b6acb7039aaa58285061f5bd189d9a1659857216129e70e3fa267691c5b4ca20452aaaf683aa7d2f1aa98adaa8c162e8f8ef668da54070e79c3b8470add34e45b31",
- "created_at": "2025-01-08T12:58:51.72573Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-01-14T17:54:05.295746Z",
"user_id": "luke_skywalker"
}
],
"invisible": false,
+ "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
- "team": "test",
- "type": "team"
+ "team": "test"
},
- "created_at": "2025-01-31T16:18:36.200384029Z"
+ "created_at": "2025-02-15T00:14:52.576220416Z"
}
\ 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 034bf7036d3..2d9a6672f71 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json
@@ -1,18 +1,18 @@
{
"type": "channel.updated",
- "created_at": "2025-01-31T16:18:37.542936957Z",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "channel_last_message_at": "2025-01-31T16:18:37.094228Z",
+ "created_at": "2025-02-15T00:14:56.210415251Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "channel_last_message_at": "2025-02-15T00:14:54.796665Z",
"channel_member_count": 4,
"channel_type": "messaging",
- "channel_id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "channel_id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"channel": {
- "id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"type": "messaging",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "last_message_at": "2025-01-31T16:18:37.094228Z",
- "created_at": "2025-01-31T16:18:35.560277Z",
- "updated_at": "2025-01-31T16:18:35.560277Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "last_message_at": "2025-02-15T00:14:54.796665Z",
+ "created_at": "2025-02-15T00:14:50.645951Z",
+ "updated_at": "2025-02-15T00:14:50.645951Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -26,7 +26,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -43,178 +43,178 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
- "created_at": "2025-01-29T12:48:17.917988Z",
+ "id": "e2c6bed363247a93ea49699385e50de99efba5707808cbd7b2d3f4e46a7cbfb9",
+ "created_at": "2025-02-13T17:54:30.965611Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
- "created_at": "2025-01-29T11:17:58.405065Z",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-02-12T13:52:59.060649Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "52c03e3cd1477169d34772e1755a6009448b9115b3fe99fc6f50b4e8ded1a397",
- "created_at": "2025-01-27T14:10:52.981213Z",
- "disabled": true,
- "disabled_reason": "Unregistered",
+ "id": "805fbd71d240360c14b785cd89f59846acf40e15514e3cf89efdbc30f360999873c4713b6fd6f192072c075c41243b82f8b89ea4880f7694f72815934477e7cf8d80c303d7bb25c8c0730e6268a1e3d2",
+ "created_at": "2025-02-12T10:55:34.960205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
- "created_at": "2025-01-27T09:26:50.733298Z",
+ "id": "38cf0b02ebe66d4bbc17981502acf8ef7806337f45e3094fbf662293185dd663",
+ "created_at": "2025-02-12T10:45:49.811205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
- "created_at": "2025-01-27T07:18:18.62698Z",
+ "id": "2716b6f605e691bf7dc7d88d1a45968dd6807ed91dbbf793777d152f796994b0",
+ "created_at": "2025-02-11T01:03:34.75674Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
- "created_at": "2025-01-26T19:17:41.00587Z",
+ "id": "809975048776300710e9bf304eabc27813c3042266dc085420362ea254ebeccea39666720bc25ffc5a022b79062f1e5f1f1de6d1565850cc78ba3c9df1a810e7d4128b73c2f76210189febf4ee0df9b1",
+ "created_at": "2025-02-10T01:52:00.239203Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
- "created_at": "2025-01-26T18:56:24.995825Z",
+ "id": "8082e4f123b0f83543d1f7c04920c76ec7d4b4db9d8f2e8ff0cc4daeef338209be72df2b35257cfdb86623c14d241cf1d1f42b78fef01e16052d13b03f75ed135360886f1900d3a0e80e97b268514c77",
+ "created_at": "2025-02-09T10:04:50.694565Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
- "created_at": "2025-01-24T15:23:31.160845Z",
+ "id": "809a57e2ed4470cb0b372e79d31cff19276879f5b312b3b71e51fc05ad338bbda27220c4f36c1283e0340d6b8816702d123dda05604dfc0d9a2eae03099f718db1f14f13de887394280380cbcb945223",
+ "created_at": "2025-02-08T13:42:01.054257Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
- "created_at": "2025-01-24T05:36:02.440032Z",
+ "id": "800dd2e8d35942e0a2124b71e87dcb54467f63b148190dbd04821e71100924fdcf58d8cc17eb1030529f07bfaae1bc5c2f9313139067e7392edf1fdadf837762409698640194be2ca788aa9b5660ff4d",
+ "created_at": "2025-02-07T09:29:01.123928Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
- "created_at": "2025-01-23T05:30:44.092337Z",
+ "id": "8009a9c2db8394cdaa8ebd8f0f5731ba1fe28c3cc89397b8e38b0d55b018cafead52512e0d9dfbd10ca9abaacbfa1e96586f1e2c6a4f831977607fa415188249f6d88b7369e476554f0ce8a23e033f49",
+ "created_at": "2025-02-06T07:45:34.135591Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
- "created_at": "2025-01-22T04:43:11.923746Z",
+ "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
+ "created_at": "2025-01-29T12:48:17.917988Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
- "created_at": "2025-01-21T03:06:34.117688Z",
+ "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
+ "created_at": "2025-01-29T11:17:58.405065Z",
+ "disabled": true,
+ "disabled_reason": "Unregistered",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
- "created_at": "2025-01-15T13:18:37.405547Z",
+ "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
+ "created_at": "2025-01-27T09:26:50.733298Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
- "created_at": "2025-01-15T07:59:32.530759Z",
+ "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
+ "created_at": "2025-01-27T07:18:18.62698Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
- "created_at": "2025-01-14T20:36:03.102001Z",
+ "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
+ "created_at": "2025-01-26T19:17:41.00587Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-01-14T17:54:05.295746Z",
+ "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
+ "created_at": "2025-01-26T18:56:24.995825Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "2c0a2b1f6891af16b45df24c7b05b1598d414f64fae7c5ef3eb41d92b45bc275",
- "created_at": "2025-01-13T06:09:35.973958Z",
+ "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
+ "created_at": "2025-01-24T15:23:31.160845Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "f1f43292dc972188ccf2bb5a2523342482280cd37e6e80540c7f41890b8f5868",
- "created_at": "2025-01-13T02:06:21.211287Z",
+ "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
+ "created_at": "2025-01-24T05:36:02.440032Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80560b4f465af52f745b646beaeba703a1877ef6988eb06f38b5f023e6affb4f6a8d8c22123c53767d2f17fae4bfa1df218117400a0d175ffd2066bae361bab07d589bbfd9ae5e693e8af34476625f58",
- "created_at": "2025-01-12T12:18:40.312161Z",
+ "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
+ "created_at": "2025-01-23T05:30:44.092337Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8009ae3cb1eb8350aa78dfe4539e5858be8c0de73b66cccdb2baff95b211f3c13c56c9e9e837c9a44af93b9b631d5db658bfed4d899ba8ee773898acf49956d5df2638f37191fa5262572b75108cc075",
- "created_at": "2025-01-10T14:43:30.030261Z",
+ "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
+ "created_at": "2025-01-22T04:43:11.923746Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809155ab6e601835c405261f32a7046cc265d8899ffe5d3d484df422424101cba27d22e11531645c4c7d660a9b2c69641c9884622d08ba27b51458a03b67995e21791ba038a40f670fdcaa3a8f73403b",
- "created_at": "2025-01-10T12:25:15.095949Z",
+ "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
+ "created_at": "2025-01-21T03:06:34.117688Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a416c7a38c0b0e5f4cdc67cc22db4429b1abf069719b5fd249e067bd40f39a93c9ae72186cd4dbe9ee3f7d4b75b0572f6fcb6fd61f1447924bb5049e49d64111494690ab89f39ae64f13828d692357",
- "created_at": "2025-01-08T21:35:48.016721Z",
+ "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
+ "created_at": "2025-01-15T13:18:37.405547Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80677e1cd0e34efdaf88b76a3bb2ce7086c4c20853c3b1ca7c638a1da310ffd884491071d224cacd90e72a0116dcc1c3ebaab19a79e7eabb560f7ca21458945ff1a085e43a8b41d491f3eec625186aa1",
- "created_at": "2025-01-08T16:07:56.454603Z",
+ "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
+ "created_at": "2025-01-15T07:59:32.530759Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80d2897aafd7c4a6e382d80212ba469b5348669930fd34db0f8aae76f7059db340f7118963ef89d109480f2cb83679e7dd71a5766b764a8a546ff958f66f5ddbf17140658079e329f170bf9e47f68ae5",
- "created_at": "2025-01-08T15:16:15.675842Z",
+ "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
+ "created_at": "2025-01-14T20:36:03.102001Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca9df3ad609fa3e61a033f403e3b6acb7039aaa58285061f5bd189d9a1659857216129e70e3fa267691c5b4ca20452aaaf683aa7d2f1aa98adaa8c162e8f8ef668da54070e79c3b8470add34e45b31",
- "created_at": "2025-01-08T12:58:51.72573Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-01-14T17:54:05.295746Z",
"user_id": "luke_skywalker"
}
],
@@ -242,7 +242,7 @@
"updated_at": "2024-07-11T05:45:57.296628Z",
"banned": false,
"online": false,
- "last_active": "2025-01-14T21:47:15.722632Z",
+ "last_active": "2025-02-14T13:02:21.846063Z",
"blocked_user_ids": [
],
@@ -328,8 +328,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -351,7 +351,7 @@
"updated_at": "2025-01-16T12:35:13.266041Z",
"banned": false,
"online": false,
- "last_active": "2025-01-31T14:12:07.724208Z",
+ "last_active": "2025-02-14T09:00:12.17086Z",
"blocked_user_ids": [
],
@@ -368,8 +368,22 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "baf4b773dce673f8798202e71525c4bd737d8516351a3b255eb96ce044401a0a",
- "created_at": "2025-01-31T14:10:34.827212Z",
+ "id": "14216b28af8556468ae8dbf08a5ea602f9deb9c2eb115d9b6aacf7f7bde74ade",
+ "created_at": "2025-02-12T10:58:40.574878Z",
+ "user_id": "han_solo"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "327aea75615b5163a87779ea32e241e360a049e418bc4e42993a996d3d6cf94c",
+ "created_at": "2025-02-04T13:16:37.534389Z",
+ "user_id": "han_solo"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "4d6ab888ea255eb6412108c4875977860cc08099ba45de161959c4bc7a9cb69a",
+ "created_at": "2025-02-03T12:42:48.406824Z",
"user_id": "han_solo"
},
{
@@ -518,35 +532,14 @@
"id": "ed4ffeeccf2e779b162b27135cab28916d2a9a907c0401d00b032c0bcc7432d1",
"created_at": "2024-11-26T17:17:31.601203Z",
"user_id": "han_solo"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "802d5502038579bc88e1842e7adc19317c656fbb427024e9d09b2fccc3c761c0975082e99bff525721300c64af0500852f342e5b40af9c1fb581013a10c8090ce3a2fa4be55b4db96f5c822d40802321",
- "created_at": "2024-11-11T15:05:13.174817Z",
- "user_id": "han_solo"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "308135f38a9df34059a827bcc671fe89b81b3ae8d9fa7b90bc2dfeacc896d9fe",
- "created_at": "2024-11-11T07:31:34.464374Z",
- "user_id": "han_solo"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "ed8267ab036e152959b891f11ce28d119d3ae9a36d49a3da9d75106e753709c1",
- "created_at": "2024-11-05T13:03:40.481073Z",
- "user_id": "han_solo"
}
],
"invisible": false,
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -568,7 +561,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -585,190 +578,190 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
- "created_at": "2025-01-29T12:48:17.917988Z",
+ "id": "e2c6bed363247a93ea49699385e50de99efba5707808cbd7b2d3f4e46a7cbfb9",
+ "created_at": "2025-02-13T17:54:30.965611Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
- "created_at": "2025-01-29T11:17:58.405065Z",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-02-12T13:52:59.060649Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "52c03e3cd1477169d34772e1755a6009448b9115b3fe99fc6f50b4e8ded1a397",
- "created_at": "2025-01-27T14:10:52.981213Z",
- "disabled": true,
- "disabled_reason": "Unregistered",
+ "id": "805fbd71d240360c14b785cd89f59846acf40e15514e3cf89efdbc30f360999873c4713b6fd6f192072c075c41243b82f8b89ea4880f7694f72815934477e7cf8d80c303d7bb25c8c0730e6268a1e3d2",
+ "created_at": "2025-02-12T10:55:34.960205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
- "created_at": "2025-01-27T09:26:50.733298Z",
+ "id": "38cf0b02ebe66d4bbc17981502acf8ef7806337f45e3094fbf662293185dd663",
+ "created_at": "2025-02-12T10:45:49.811205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
- "created_at": "2025-01-27T07:18:18.62698Z",
+ "id": "2716b6f605e691bf7dc7d88d1a45968dd6807ed91dbbf793777d152f796994b0",
+ "created_at": "2025-02-11T01:03:34.75674Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
- "created_at": "2025-01-26T19:17:41.00587Z",
+ "id": "809975048776300710e9bf304eabc27813c3042266dc085420362ea254ebeccea39666720bc25ffc5a022b79062f1e5f1f1de6d1565850cc78ba3c9df1a810e7d4128b73c2f76210189febf4ee0df9b1",
+ "created_at": "2025-02-10T01:52:00.239203Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
- "created_at": "2025-01-26T18:56:24.995825Z",
+ "id": "8082e4f123b0f83543d1f7c04920c76ec7d4b4db9d8f2e8ff0cc4daeef338209be72df2b35257cfdb86623c14d241cf1d1f42b78fef01e16052d13b03f75ed135360886f1900d3a0e80e97b268514c77",
+ "created_at": "2025-02-09T10:04:50.694565Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
- "created_at": "2025-01-24T15:23:31.160845Z",
+ "id": "809a57e2ed4470cb0b372e79d31cff19276879f5b312b3b71e51fc05ad338bbda27220c4f36c1283e0340d6b8816702d123dda05604dfc0d9a2eae03099f718db1f14f13de887394280380cbcb945223",
+ "created_at": "2025-02-08T13:42:01.054257Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
- "created_at": "2025-01-24T05:36:02.440032Z",
+ "id": "800dd2e8d35942e0a2124b71e87dcb54467f63b148190dbd04821e71100924fdcf58d8cc17eb1030529f07bfaae1bc5c2f9313139067e7392edf1fdadf837762409698640194be2ca788aa9b5660ff4d",
+ "created_at": "2025-02-07T09:29:01.123928Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
- "created_at": "2025-01-23T05:30:44.092337Z",
+ "id": "8009a9c2db8394cdaa8ebd8f0f5731ba1fe28c3cc89397b8e38b0d55b018cafead52512e0d9dfbd10ca9abaacbfa1e96586f1e2c6a4f831977607fa415188249f6d88b7369e476554f0ce8a23e033f49",
+ "created_at": "2025-02-06T07:45:34.135591Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
- "created_at": "2025-01-22T04:43:11.923746Z",
+ "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
+ "created_at": "2025-01-29T12:48:17.917988Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
- "created_at": "2025-01-21T03:06:34.117688Z",
+ "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
+ "created_at": "2025-01-29T11:17:58.405065Z",
+ "disabled": true,
+ "disabled_reason": "Unregistered",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
- "created_at": "2025-01-15T13:18:37.405547Z",
+ "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
+ "created_at": "2025-01-27T09:26:50.733298Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
- "created_at": "2025-01-15T07:59:32.530759Z",
+ "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
+ "created_at": "2025-01-27T07:18:18.62698Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
- "created_at": "2025-01-14T20:36:03.102001Z",
+ "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
+ "created_at": "2025-01-26T19:17:41.00587Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-01-14T17:54:05.295746Z",
+ "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
+ "created_at": "2025-01-26T18:56:24.995825Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "2c0a2b1f6891af16b45df24c7b05b1598d414f64fae7c5ef3eb41d92b45bc275",
- "created_at": "2025-01-13T06:09:35.973958Z",
+ "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
+ "created_at": "2025-01-24T15:23:31.160845Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "f1f43292dc972188ccf2bb5a2523342482280cd37e6e80540c7f41890b8f5868",
- "created_at": "2025-01-13T02:06:21.211287Z",
+ "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
+ "created_at": "2025-01-24T05:36:02.440032Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80560b4f465af52f745b646beaeba703a1877ef6988eb06f38b5f023e6affb4f6a8d8c22123c53767d2f17fae4bfa1df218117400a0d175ffd2066bae361bab07d589bbfd9ae5e693e8af34476625f58",
- "created_at": "2025-01-12T12:18:40.312161Z",
+ "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
+ "created_at": "2025-01-23T05:30:44.092337Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8009ae3cb1eb8350aa78dfe4539e5858be8c0de73b66cccdb2baff95b211f3c13c56c9e9e837c9a44af93b9b631d5db658bfed4d899ba8ee773898acf49956d5df2638f37191fa5262572b75108cc075",
- "created_at": "2025-01-10T14:43:30.030261Z",
+ "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
+ "created_at": "2025-01-22T04:43:11.923746Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809155ab6e601835c405261f32a7046cc265d8899ffe5d3d484df422424101cba27d22e11531645c4c7d660a9b2c69641c9884622d08ba27b51458a03b67995e21791ba038a40f670fdcaa3a8f73403b",
- "created_at": "2025-01-10T12:25:15.095949Z",
+ "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
+ "created_at": "2025-01-21T03:06:34.117688Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a416c7a38c0b0e5f4cdc67cc22db4429b1abf069719b5fd249e067bd40f39a93c9ae72186cd4dbe9ee3f7d4b75b0572f6fcb6fd61f1447924bb5049e49d64111494690ab89f39ae64f13828d692357",
- "created_at": "2025-01-08T21:35:48.016721Z",
+ "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
+ "created_at": "2025-01-15T13:18:37.405547Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80677e1cd0e34efdaf88b76a3bb2ce7086c4c20853c3b1ca7c638a1da310ffd884491071d224cacd90e72a0116dcc1c3ebaab19a79e7eabb560f7ca21458945ff1a085e43a8b41d491f3eec625186aa1",
- "created_at": "2025-01-08T16:07:56.454603Z",
+ "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
+ "created_at": "2025-01-15T07:59:32.530759Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80d2897aafd7c4a6e382d80212ba469b5348669930fd34db0f8aae76f7059db340f7118963ef89d109480f2cb83679e7dd71a5766b764a8a546ff958f66f5ddbf17140658079e329f170bf9e47f68ae5",
- "created_at": "2025-01-08T15:16:15.675842Z",
+ "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
+ "created_at": "2025-01-14T20:36:03.102001Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca9df3ad609fa3e61a033f403e3b6acb7039aaa58285061f5bd189d9a1659857216129e70e3fa267691c5b4ca20452aaaf683aa7d2f1aa98adaa8c162e8f8ef668da54070e79c3b8470add34e45b31",
- "created_at": "2025-01-08T12:58:51.72573Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-01-14T17:54:05.295746Z",
"user_id": "luke_skywalker"
}
],
"invisible": false,
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"status": "member",
- "created_at": "2025-01-31T16:18:35.564453Z",
- "updated_at": "2025-01-31T16:18:35.564453Z",
+ "created_at": "2025-02-15T00:14:50.66974Z",
+ "updated_at": "2025-02-15T00:14:50.66974Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -790,7 +783,7 @@
"updated_at": "2025-01-16T12:34:56.765249Z",
"banned": false,
"online": false,
- "last_active": "2025-01-29T16:35:31.022088Z",
+ "last_active": "2025-02-14T13:12:46.241933Z",
"blocked_user_ids": [
],
@@ -807,22 +800,36 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "06f2e16cacf2bc6c1917d945e3aa875d2c0faa6cd1a379f7c2c28b9b61269bd2",
- "created_at": "2025-01-25T23:50:24.74497Z",
+ "id": "80c9f99c1d02347ba3599ab752e55308681244fe97fcbc95eb0e630bcd97cb3ee927e8b15d5085938b4b18246f2e9ba2de3f357a6ca9127b1971df14742725841f0fd8537233b5e43a07a24422411816",
+ "created_at": "2025-02-06T05:12:00.670052Z",
"user_id": "leia_organa"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80902ac37a00a72af2fcebb5fa7db31603af4ed96681d8c356409dc391ce5d0a",
- "created_at": "2025-01-24T05:28:13.061448Z",
+ "id": "8099cf56e010eb7a7ec2202eacffbc22c0d84b2c3007b9b45403f4f99fd42b670d6552521aea4c14263fd0ed0d034f2e3f25aa44145a4054ed7d067e25e2f221582d30de66f750e62e45e2c0fa73ed74",
+ "created_at": "2025-02-05T14:25:05.737649Z",
+ "user_id": "leia_organa"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "003ddc257b04f7d989eb91bf70fd2e19aaef86bd7419d16993cba3e74f6e2ba4",
+ "created_at": "2025-02-01T23:38:43.460976Z",
+ "user_id": "leia_organa"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "06f2e16cacf2bc6c1917d945e3aa875d2c0faa6cd1a379f7c2c28b9b61269bd2",
+ "created_at": "2025-01-25T23:50:24.74497Z",
"user_id": "leia_organa"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80d7928379a90a4e06623ff511036f50df5fe140ae14ab04c4998662eaa42e607b960280e4a24dfd3ef49b360f7837c8b493e463bd2d6b1f467bcad9a70a5f3bd3250b1196a0938e63001952d68f75a6",
- "created_at": "2025-01-23T15:22:37.89033Z",
+ "id": "80902ac37a00a72af2fcebb5fa7db31603af4ed96681d8c356409dc391ce5d0a",
+ "created_at": "2025-01-24T05:28:13.061448Z",
"user_id": "leia_organa"
},
{
@@ -957,24 +964,9 @@
"id": "8060ebab3e48f5ca984a9673ab0decdeb67f9359925f57a73930160d9f8ee24acca80c515409ab89f73fbc1ef27f092faf1363b9806b754592fd4a93e1b6fdcf2802feea16aec889aaef14574d1dd076",
"created_at": "2024-11-12T11:48:44.01663Z",
"user_id": "leia_organa"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "3fb4736e2bc02e51f13e403adef98fb1301f18e71b9825bf0aa6fef6666c2112",
- "created_at": "2024-11-05T16:33:34.780594Z",
- "user_id": "leia_organa"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80b04f87691de56ae755b28cddb3eee63ef786f3ed952f53632a14f1af72053ee01e026918a71ad7518de5ae73910c3528d00e465d78d1702d9e79a620b207c60c337a938c4c364e5e5b8f3963acefb4",
- "created_at": "2024-10-26T18:09:15.629886Z",
- "user_id": "leia_organa"
}
],
"invisible": false,
- "birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
"enabled": false
@@ -982,11 +974,12 @@
"typingIndicators": {
"enabled": false
}
- }
+ },
+ "birthland": "Polis Massa"
},
"status": "member",
- "created_at": "2025-01-31T16:18:37.521245Z",
- "updated_at": "2025-01-31T16:18:37.521245Z",
+ "created_at": "2025-02-15T00:14:56.183774Z",
+ "updated_at": "2025-02-15T00:14:56.183774Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -997,7 +990,7 @@
"member_count": 4,
"config": {
"created_at": "2021-03-01T19:26:18.406502Z",
- "updated_at": "2024-11-13T11:49:47.368244Z",
+ "updated_at": "2025-02-13T14:46:16.795117Z",
"name": "messaging",
"typing_events": true,
"read_events": true,
@@ -1059,10 +1052,13 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:15:56.152156879Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
+ "team": "test",
+ "type": "team",
+ "pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
"privacy_settings": {
"read_receipts": {
@@ -1071,9 +1067,6 @@
"typing_indicators": {
"enabled": false
}
- },
- "team": "test",
- "type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}"
+ }
}
}
\ 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 4cad347181c..53f0207391c 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_member.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_member.json
@@ -1,7 +1,7 @@
{
"type": "member.added",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "channel_id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "channel_id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"channel_type": "messaging",
"member": {
"user_id": "leia_organa",
@@ -10,12 +10,11 @@
"role": "admin",
"created_at": "2024-04-04T09:42:00.68335Z",
"updated_at": "2025-01-16T12:34:56.765249Z",
- "last_active": "2025-01-29T16:35:31.022088Z",
- "last_engaged_at": "2025-01-29T01:28:41.520256Z",
+ "last_active": "2025-02-14T13:12:46.241933Z",
+ "last_engaged_at": "2025-02-14T08:58:37.266996Z",
"banned": false,
"online": false,
"language": "ru",
- "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png",
"birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
@@ -25,11 +24,12 @@
"enabled": false
}
},
- "name": "Leia Organa"
+ "name": "Leia Organa",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png"
},
"status": "member",
- "created_at": "2025-01-31T16:18:37.521245Z",
- "updated_at": "2025-01-31T16:18:37.521245Z",
+ "created_at": "2025-02-15T00:14:56.183774Z",
+ "updated_at": "2025-02-15T00:14:56.183774Z",
"banned": false,
"shadow_banned": false,
"archived_at": null,
@@ -43,14 +43,11 @@
"role": "admin",
"created_at": "2024-04-04T09:42:00.68335Z",
"updated_at": "2025-01-16T12:34:56.765249Z",
- "last_active": "2025-01-29T16:35:31.022088Z",
- "last_engaged_at": "2025-01-29T01:28:41.520256Z",
+ "last_active": "2025-02-14T13:12:46.241933Z",
+ "last_engaged_at": "2025-02-14T08:58:37.266996Z",
"banned": false,
"online": false,
"language": "ru",
- "name": "Leia Organa",
- "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png",
- "birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
"enabled": false
@@ -58,8 +55,11 @@
"typingIndicators": {
"enabled": false
}
- }
+ },
+ "name": "Leia Organa",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png",
+ "birthland": "Polis Massa"
},
- "channel_last_message_at": "2025-01-31T16:18:37.094228Z",
- "created_at": "2025-01-31T16:18:37.531605452Z"
+ "channel_last_message_at": "2025-02-15T00:14:54.796665Z",
+ "created_at": "2025-02-15T00:14:56.195943204Z"
}
\ 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 88357c65958..1f4a4da6aac 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_health_check.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_health_check.json
@@ -1,5 +1,5 @@
{
- "connection_id": "679c8249-0a15-3bf6-0200-0000000001c0",
+ "connection_id": "67add740-0a15-3bf6-0200-000000000f44",
"me": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -13,7 +13,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:18:35.2853414Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"privacy_settings": {
"typing_indicators": {
"enabled": true
@@ -43,5 +43,5 @@
},
"cid": "*",
"type": "health.check",
- "created_at": "2025-01-31T16:18:35.302174503Z"
+ "created_at": "2025-02-15T00:14:49.812925379Z"
}
\ 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 0f540037903..13977ae18d9 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json
@@ -1,17 +1,17 @@
{
"type": "message.new",
- "created_at": "2025-01-31T16:18:37.112003835Z",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "channel_last_message_at": "2025-01-31T16:18:37.094228Z",
+ "created_at": "2025-02-15T00:14:54.844825276Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "channel_last_message_at": "2025-02-15T00:14:54.796665Z",
"channel_member_count": 3,
"channel_custom": {
"name": "Sync Mock Server"
},
"channel_type": "messaging",
- "channel_id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "message_id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "channel_id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "message_id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"message": {
- "id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -28,7 +28,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:17:53.97317831Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -37,178 +37,178 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
- "created_at": "2025-01-29T12:48:17.917988Z",
+ "id": "e2c6bed363247a93ea49699385e50de99efba5707808cbd7b2d3f4e46a7cbfb9",
+ "created_at": "2025-02-13T17:54:30.965611Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
- "created_at": "2025-01-29T11:17:58.405065Z",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-02-12T13:52:59.060649Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "52c03e3cd1477169d34772e1755a6009448b9115b3fe99fc6f50b4e8ded1a397",
- "created_at": "2025-01-27T14:10:52.981213Z",
- "disabled": true,
- "disabled_reason": "Unregistered",
+ "id": "805fbd71d240360c14b785cd89f59846acf40e15514e3cf89efdbc30f360999873c4713b6fd6f192072c075c41243b82f8b89ea4880f7694f72815934477e7cf8d80c303d7bb25c8c0730e6268a1e3d2",
+ "created_at": "2025-02-12T10:55:34.960205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
- "created_at": "2025-01-27T09:26:50.733298Z",
+ "id": "38cf0b02ebe66d4bbc17981502acf8ef7806337f45e3094fbf662293185dd663",
+ "created_at": "2025-02-12T10:45:49.811205Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
- "created_at": "2025-01-27T07:18:18.62698Z",
+ "id": "2716b6f605e691bf7dc7d88d1a45968dd6807ed91dbbf793777d152f796994b0",
+ "created_at": "2025-02-11T01:03:34.75674Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
- "created_at": "2025-01-26T19:17:41.00587Z",
+ "id": "809975048776300710e9bf304eabc27813c3042266dc085420362ea254ebeccea39666720bc25ffc5a022b79062f1e5f1f1de6d1565850cc78ba3c9df1a810e7d4128b73c2f76210189febf4ee0df9b1",
+ "created_at": "2025-02-10T01:52:00.239203Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
- "created_at": "2025-01-26T18:56:24.995825Z",
+ "id": "8082e4f123b0f83543d1f7c04920c76ec7d4b4db9d8f2e8ff0cc4daeef338209be72df2b35257cfdb86623c14d241cf1d1f42b78fef01e16052d13b03f75ed135360886f1900d3a0e80e97b268514c77",
+ "created_at": "2025-02-09T10:04:50.694565Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
- "created_at": "2025-01-24T15:23:31.160845Z",
+ "id": "809a57e2ed4470cb0b372e79d31cff19276879f5b312b3b71e51fc05ad338bbda27220c4f36c1283e0340d6b8816702d123dda05604dfc0d9a2eae03099f718db1f14f13de887394280380cbcb945223",
+ "created_at": "2025-02-08T13:42:01.054257Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
- "created_at": "2025-01-24T05:36:02.440032Z",
+ "id": "800dd2e8d35942e0a2124b71e87dcb54467f63b148190dbd04821e71100924fdcf58d8cc17eb1030529f07bfaae1bc5c2f9313139067e7392edf1fdadf837762409698640194be2ca788aa9b5660ff4d",
+ "created_at": "2025-02-07T09:29:01.123928Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
- "created_at": "2025-01-23T05:30:44.092337Z",
+ "id": "8009a9c2db8394cdaa8ebd8f0f5731ba1fe28c3cc89397b8e38b0d55b018cafead52512e0d9dfbd10ca9abaacbfa1e96586f1e2c6a4f831977607fa415188249f6d88b7369e476554f0ce8a23e033f49",
+ "created_at": "2025-02-06T07:45:34.135591Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
- "created_at": "2025-01-22T04:43:11.923746Z",
+ "id": "adcc46baccb0cc4b2dfd893f7762b6ee15b0f9d9e39c014286a501dcce558ec3",
+ "created_at": "2025-01-29T12:48:17.917988Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
- "created_at": "2025-01-21T03:06:34.117688Z",
+ "id": "8d534aa2033befff9291da0e371b223b0f60a34ec0b498ad364259e20541d374",
+ "created_at": "2025-01-29T11:17:58.405065Z",
+ "disabled": true,
+ "disabled_reason": "Unregistered",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
- "created_at": "2025-01-15T13:18:37.405547Z",
+ "id": "27dc3cd279808ba179d46e04a2e95bc17a94062dfc91e1a317d7e39c314f65c6",
+ "created_at": "2025-01-27T09:26:50.733298Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
- "created_at": "2025-01-15T07:59:32.530759Z",
+ "id": "806dcda419b98288b170fcaee122e709197f8ae3c727329f1ea3ec2aed31419acd1028da485a462e39850aec933df018da4fb2d75f02f4a003f7749084d6b4a37818caffc674c41f947592f648155a93",
+ "created_at": "2025-01-27T07:18:18.62698Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
- "created_at": "2025-01-14T20:36:03.102001Z",
+ "id": "80ca028a4646d58ef07166eaed2c5f068430fb9d7aa6b2bb94d53f8ce6261df2f932611aea12cad01a7b2cbc5169a4a0242c8d95c78ed5cde48bc139414ec63bbf696a56fb7c9b07218fa17bf8ba0aec",
+ "created_at": "2025-01-26T19:17:41.00587Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-01-14T17:54:05.295746Z",
+ "id": "80647c152794bfb16ff75836d87e93363fe6827fd4a5673f84c839c65048fa9f8b4691a75f12ee974538b29858d8b6d1b935276ed594d657c754a84fb6229dd3b5881199598128dc1fae6a1e969d765b",
+ "created_at": "2025-01-26T18:56:24.995825Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "2c0a2b1f6891af16b45df24c7b05b1598d414f64fae7c5ef3eb41d92b45bc275",
- "created_at": "2025-01-13T06:09:35.973958Z",
+ "id": "803fac504e3f9eca842b146b0d5dc358a258a83a1da25c53404f928eed4b607581a274499978e886f4923c1002d6ef33bd4b1374b0ede16c962dec4a562d53467ccf3c835c784544d07a181c56c886a5",
+ "created_at": "2025-01-24T15:23:31.160845Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "f1f43292dc972188ccf2bb5a2523342482280cd37e6e80540c7f41890b8f5868",
- "created_at": "2025-01-13T02:06:21.211287Z",
+ "id": "8031fa3a733e7c218e91ad97413ed5640dbf934909f76ae7452fbd63c11eaa30825208b63462c29fe073410262afd99f39fa5f2d65d5bb834e6cff93e77e7fbeb7e63bdd3f2f6a91fa05dffe78335548",
+ "created_at": "2025-01-24T05:36:02.440032Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80560b4f465af52f745b646beaeba703a1877ef6988eb06f38b5f023e6affb4f6a8d8c22123c53767d2f17fae4bfa1df218117400a0d175ffd2066bae361bab07d589bbfd9ae5e693e8af34476625f58",
- "created_at": "2025-01-12T12:18:40.312161Z",
+ "id": "d88cf79198e6489f4965d4855de57d12e33ba3d0d91c9490807c6f092f176c90",
+ "created_at": "2025-01-23T05:30:44.092337Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "8009ae3cb1eb8350aa78dfe4539e5858be8c0de73b66cccdb2baff95b211f3c13c56c9e9e837c9a44af93b9b631d5db658bfed4d899ba8ee773898acf49956d5df2638f37191fa5262572b75108cc075",
- "created_at": "2025-01-10T14:43:30.030261Z",
+ "id": "80a866c1775d76e7884f3e27eae779ffe9a0fe2bdf8f8c5d9ae20ab1d3de4211d3dbfc689613a18ad21957a625ee60a00a2b811666d6a8b96ce62e17353626bb643bb9fa4062b6e6d6b8d9aea0371c79",
+ "created_at": "2025-01-22T04:43:11.923746Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809155ab6e601835c405261f32a7046cc265d8899ffe5d3d484df422424101cba27d22e11531645c4c7d660a9b2c69641c9884622d08ba27b51458a03b67995e21791ba038a40f670fdcaa3a8f73403b",
- "created_at": "2025-01-10T12:25:15.095949Z",
+ "id": "805adb0dfe07ccf6c50df056ee5b0cca2ef67f1cee8045db9e2dec6dd48d6c9633b86635efd73673c72a3bc8ab84e3af4f85b7d1a8e377563272e7999eb16fcbfbced5fe9e26dc9e08db8973e0891418",
+ "created_at": "2025-01-21T03:06:34.117688Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80a416c7a38c0b0e5f4cdc67cc22db4429b1abf069719b5fd249e067bd40f39a93c9ae72186cd4dbe9ee3f7d4b75b0572f6fcb6fd61f1447924bb5049e49d64111494690ab89f39ae64f13828d692357",
- "created_at": "2025-01-08T21:35:48.016721Z",
+ "id": "3eef457f5a0c9dd01c600ee142c1cd812034f603690c0d3e8c6c6c6c073ab075",
+ "created_at": "2025-01-15T13:18:37.405547Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80677e1cd0e34efdaf88b76a3bb2ce7086c4c20853c3b1ca7c638a1da310ffd884491071d224cacd90e72a0116dcc1c3ebaab19a79e7eabb560f7ca21458945ff1a085e43a8b41d491f3eec625186aa1",
- "created_at": "2025-01-08T16:07:56.454603Z",
+ "id": "9adb5efb25e5dd635103e3fa8475ccfb4806f262235015dcde5985fcddfc00db",
+ "created_at": "2025-01-15T07:59:32.530759Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80d2897aafd7c4a6e382d80212ba469b5348669930fd34db0f8aae76f7059db340f7118963ef89d109480f2cb83679e7dd71a5766b764a8a546ff958f66f5ddbf17140658079e329f170bf9e47f68ae5",
- "created_at": "2025-01-08T15:16:15.675842Z",
+ "id": "9f4c91f42937f2f6991d772680e6d2f6f840e7eee3d4992328c3912e45365190",
+ "created_at": "2025-01-14T20:36:03.102001Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80ca9df3ad609fa3e61a033f403e3b6acb7039aaa58285061f5bd189d9a1659857216129e70e3fa267691c5b4ca20452aaaf683aa7d2f1aa98adaa8c162e8f8ef668da54070e79c3b8470add34e45b31",
- "created_at": "2025-01-08T12:58:51.72573Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-01-14T17:54:05.295746Z",
"user_id": "luke_skywalker"
}
],
@@ -233,9 +233,9 @@
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:37.094228Z",
- "updated_at": "2025-01-31T16:18:37.094228Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:54.796665Z",
+ "updated_at": "2025-02-15T00:14:54.796665Z",
"shadowed": false,
"mentioned_users": [
@@ -244,7 +244,10 @@
"pinned": false,
"pinned_at": null,
"pinned_by": null,
- "pin_expires": null
+ "pin_expires": null,
+ "restricted_visibility": [
+
+ ]
},
"user": {
"id": "luke_skywalker",
@@ -259,7 +262,7 @@
"updated_at": "2025-01-31T09:38:22.825054Z",
"banned": false,
"online": true,
- "last_active": "2025-01-31T16:15:56.152156879Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
"blocked_user_ids": [
],
@@ -279,5 +282,5 @@
"watcher_count": 1,
"unread_count": 0,
"total_unread_count": 0,
- "unread_channels": 1
+ "unread_channels": 0
}
\ 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 23e9d20d52b..3dfae71f5ef 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json
@@ -1,10 +1,10 @@
{
"type": "reaction.new",
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "channel_id": "09f99d09-a2a7-4fb0-ae8d-25927c147b67",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "channel_id": "bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
"channel_type": "messaging",
"message": {
- "id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -13,46 +13,49 @@
"role": "admin",
"created_at": "2024-04-04T09:26:11.805899Z",
"updated_at": "2025-01-31T09:38:22.825054Z",
- "last_active": "2025-01-31T16:15:56.152156879Z",
- "last_engaged_at": "2025-01-31T00:11:13.65581Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
+ "last_engaged_at": "2025-02-14T00:00:13.035318Z",
"banned": false,
"online": true,
"language": "id",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
- "name": "Luke Skywalker",
"team": "test",
"type": "team",
- "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg"
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg",
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine",
+ "name": "Luke Skywalker"
},
+ "restricted_visibility": [
+
+ ],
"attachments": [
],
"latest_reactions": [
{
- "message_id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "message_id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
"role": "admin",
"created_at": "2024-04-04T09:26:11.805899Z",
"updated_at": "2025-01-31T09:38:22.825054Z",
- "last_active": "2025-01-31T16:15:56.152156879Z",
- "last_engaged_at": "2025-01-31T00:11:13.65581Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
+ "last_engaged_at": "2025-02-14T00:00:13.035318Z",
"banned": false,
"online": true,
"language": "id",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg",
+ "pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
"name": "Luke Skywalker",
"team": "test",
- "type": "team",
- "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg",
- "pando": "{\"speciality\":\"ios engineer\"}"
+ "type": "team"
},
"type": "like",
"score": 1,
- "created_at": "2025-01-31T16:18:37.304432Z",
- "updated_at": "2025-01-31T16:18:37.304432Z"
+ "created_at": "2025-02-15T00:14:55.545757Z",
+ "updated_at": "2025-02-15T00:14:55.545757Z"
}
],
"own_reactions": [
@@ -68,15 +71,15 @@
"like": {
"count": 1,
"sum_scores": 1,
- "first_reaction_at": "2025-01-31T16:18:37.304432Z",
- "last_reaction_at": "2025-01-31T16:18:37.304432Z"
+ "first_reaction_at": "2025-02-15T00:14:55.545757Z",
+ "last_reaction_at": "2025-02-15T00:14:55.545757Z"
}
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:09f99d09-a2a7-4fb0-ae8d-25927c147b67",
- "created_at": "2025-01-31T16:18:37.094228Z",
- "updated_at": "2025-01-31T16:18:37.310539Z",
+ "cid": "messaging:bf00ad34-c8b4-413f-ba3a-b1fcc321da9b",
+ "created_at": "2025-02-15T00:14:54.796665Z",
+ "updated_at": "2025-02-15T00:14:55.555595Z",
"shadowed": false,
"mentioned_users": [
@@ -88,47 +91,47 @@
"pin_expires": null
},
"reaction": {
- "message_id": "b222d2eb-d0e3-4d82-a029-68a9e5a3d261",
+ "message_id": "aefe3ec6-d123-41a2-b055-f80546bf2e04",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
"role": "admin",
"created_at": "2024-04-04T09:26:11.805899Z",
"updated_at": "2025-01-31T09:38:22.825054Z",
- "last_active": "2025-01-31T16:15:56.152156879Z",
- "last_engaged_at": "2025-01-31T00:11:13.65581Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
+ "last_engaged_at": "2025-02-14T00:00:13.035318Z",
"banned": false,
"online": true,
"language": "id",
+ "name": "Luke Skywalker",
"team": "test",
"type": "team",
"image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
- "name": "Luke Skywalker"
+ "birthland": "Tatooine"
},
"type": "like",
"score": 1,
- "created_at": "2025-01-31T16:18:37.304432Z",
- "updated_at": "2025-01-31T16:18:37.304432Z"
+ "created_at": "2025-02-15T00:14:55.545757Z",
+ "updated_at": "2025-02-15T00:14:55.545757Z"
},
"user": {
"id": "luke_skywalker",
"role": "admin",
"created_at": "2024-04-04T09:26:11.805899Z",
"updated_at": "2025-01-31T09:38:22.825054Z",
- "last_active": "2025-01-31T16:15:56.152156879Z",
- "last_engaged_at": "2025-01-31T00:11:13.65581Z",
+ "last_active": "2025-02-15T00:14:49.778901469Z",
+ "last_engaged_at": "2025-02-14T00:00:13.035318Z",
"banned": false,
"online": true,
"language": "id",
+ "team": "test",
+ "type": "team",
"image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg",
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
- "name": "Luke Skywalker",
- "team": "test",
- "type": "team"
+ "name": "Luke Skywalker"
},
- "channel_last_message_at": "2025-01-31T16:18:37.094228Z",
- "created_at": "2025-01-31T16:18:37.319338494Z"
+ "channel_last_message_at": "2025-02-15T00:14:54.796665Z",
+ "created_at": "2025-02-15T00:14:55.564979349Z"
}
\ No newline at end of file
diff --git a/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift b/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift
index 9825c4a845a..9a8676a1300 100644
--- a/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift
+++ b/TestTools/StreamChatTestTools/Assertions/AssertJSONEqual.swift
@@ -161,14 +161,19 @@ public func AssertJSONEqual(
}
public func AssertDictionary(
+ ignoringKeys: [String] = [],
_ expression1: @autoclosure () throws -> [String: Any],
_ expression2: @autoclosure () throws -> [String: Any],
file: StaticString = #filePath,
line: UInt = #line
) {
do {
- let expr1 = try NSDictionary(dictionary: expression1())
- let expr2 = try NSDictionary(dictionary: expression2())
+ let expr1 = try NSDictionary(
+ dictionary: expression1().filter { !ignoringKeys.contains($0.key) }
+ )
+ let expr2 = try NSDictionary(
+ dictionary: expression2().filter { !ignoringKeys.contains($0.key) }
+ )
XCTAssertEqual(expr1, expr2, file: file, line: line)
} catch {
XCTFail("Error: \(error)", file: file, line: line)
diff --git a/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift b/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
index eaf855cb650..246c4612360 100644
--- a/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
+++ b/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
@@ -52,7 +52,8 @@ extension ChatMessage {
moderationDetails: nil,
readBy: [],
poll: nil,
- textUpdatedAt: nil
+ textUpdatedAt: nil,
+ draftReply: nil
)
}
}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/DraftMessage.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/DraftMessage.json
new file mode 100644
index 00000000000..9986b895b31
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/DraftMessage.json
@@ -0,0 +1,85 @@
+{
+ "draft": {
+ "channel": {
+ "cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "config": {
+ "automod": "disabled",
+ "automod_behavior": "flag",
+ "commands": [
+ {
+ "args": "[text]",
+ "description": "Post a random gif to the channel",
+ "name": "giphy",
+ "set": "fun_set"
+ }
+ ],
+ "connect_events": true,
+ "created_at": "2025-02-06T09:58:57.737862823Z",
+ "custom_events": true,
+ "mark_messages_pending": false,
+ "max_message_length": 5000,
+ "message_retention": "infinite",
+ "mutes": true,
+ "name": "messaging",
+ "polls": false,
+ "push_notifications": true,
+ "quotes": true,
+ "reactions": true,
+ "read_events": true,
+ "reminders": false,
+ "replies": true,
+ "search": true,
+ "skip_last_msg_update_for_system_msgs": false,
+ "typing_events": true,
+ "updated_at": "2025-02-06T09:58:57.737862946Z",
+ "uploads": true,
+ "url_enrichment": true
+ },
+ "created_at": "2024-07-10T11:47:43.591964Z",
+ "created_by": {
+ "banned": false,
+ "birthland": "Corellia",
+ "blocked_user_ids": [],
+ "created_at": "2024-07-09T10:25:12.255599Z",
+ "id": "han_solo",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png",
+ "language": "",
+ "last_active": "2025-02-11T12:26:45.282392588Z",
+ "name": "Han Solo",
+ "online": true,
+ "role": "user",
+ "teams": [],
+ "updated_at": "2024-07-09T10:25:12.260463Z"
+ },
+ "disabled": false,
+ "frozen": false,
+ "id": "!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "last_message_at": "2025-02-11T12:11:59.677265Z",
+ "member_count": 2,
+ "type": "messaging",
+ "updated_at": "2024-07-10T11:47:43.591964Z"
+ },
+ "channel_cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "created_at": "2025-02-11T12:27:04.780633395Z",
+ "message": {
+ "id": "eb8ab3c9-5ade-4b4d-b32d-3d7f037682c0",
+ "mentioned_users": [
+ {
+ "id": "leia_organa",
+ "role": "user",
+ "created_at": "2023-09-27T13:21:25.781323Z",
+ "updated_at": "2023-09-27T13:21:25.781323Z",
+ "last_active": "2024-03-26T12:03:39.847771Z",
+ "banned": false,
+ "online": false
+ }
+ ],
+ "show_in_channel": false,
+ "silent": false,
+ "text": "Hi @Leia Organa ahah"
+ },
+ "parent_message": null,
+ "quoted_message": null
+ },
+ "duration": "22.64ms"
+}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Draft/DraftDeleted.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Draft/DraftDeleted.json
new file mode 100644
index 00000000000..1f7e97a522d
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Draft/DraftDeleted.json
@@ -0,0 +1,20 @@
+{
+ "type": "draft.deleted",
+ "connection_id": "12345",
+ "cid": "messaging:general",
+ "created_at": "2024-02-11T15:42:21.123Z",
+ "draft": {
+ "cid": "messaging:general",
+ "created_at": "2024-02-11T15:42:21.123Z",
+ "parent_id": "thread-123",
+ "message": {
+ "id": "draft-123",
+ "text": "Test draft message",
+ "show_reply_in_channel": false,
+ "silent": false,
+ "mentioned_users": [],
+ "attachments": [],
+ "extra_data": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Draft/DraftUpdated.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Draft/DraftUpdated.json
new file mode 100644
index 00000000000..c5a14270415
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Draft/DraftUpdated.json
@@ -0,0 +1,78 @@
+{
+ "type": "draft.updated",
+ "connection_id": "12345",
+ "cid": "messaging:general",
+ "created_at": "2024-02-11T15:42:21.123Z",
+ "draft": {
+ "cid": "messaging:general",
+ "channel": {
+ "cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "config": {
+ "automod": "disabled",
+ "automod_behavior": "flag",
+ "commands": [
+ {
+ "args": "[text]",
+ "description": "Post a random gif to the channel",
+ "name": "giphy",
+ "set": "fun_set"
+ }
+ ],
+ "connect_events": true,
+ "created_at": "2025-02-06T09:58:57.737862823Z",
+ "custom_events": true,
+ "mark_messages_pending": false,
+ "max_message_length": 5000,
+ "message_retention": "infinite",
+ "mutes": true,
+ "name": "messaging",
+ "polls": false,
+ "push_notifications": true,
+ "quotes": true,
+ "reactions": true,
+ "read_events": true,
+ "reminders": false,
+ "replies": true,
+ "search": true,
+ "skip_last_msg_update_for_system_msgs": false,
+ "typing_events": true,
+ "updated_at": "2025-02-06T09:58:57.737862946Z",
+ "uploads": true,
+ "url_enrichment": true
+ },
+ "created_at": "2024-07-10T11:47:43.591964Z",
+ "created_by": {
+ "banned": false,
+ "birthland": "Corellia",
+ "blocked_user_ids": [],
+ "created_at": "2024-07-09T10:25:12.255599Z",
+ "id": "han_solo",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png",
+ "language": "",
+ "last_active": "2025-02-11T12:26:45.282392588Z",
+ "name": "Han Solo",
+ "online": true,
+ "role": "user",
+ "teams": [],
+ "updated_at": "2024-07-09T10:25:12.260463Z"
+ },
+ "disabled": false,
+ "frozen": false,
+ "id": "!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "last_message_at": "2025-02-11T12:11:59.677265Z",
+ "member_count": 2,
+ "type": "messaging",
+ "updated_at": "2024-07-10T11:47:43.591964Z"
+ },
+ "created_at": "2024-02-11T15:42:21.123Z",
+ "message": {
+ "id": "draft-123",
+ "text": "Test draft message",
+ "show_reply_in_channel": false,
+ "silent": false,
+ "mentioned_users": [],
+ "attachments": [],
+ "extra_data": {}
+ }
+ }
+}
diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannel_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannel_Mock.swift
index f7b7adf14cd..326c3bc94c7 100644
--- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannel_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannel_Mock.swift
@@ -91,7 +91,8 @@ public extension ChatChannel {
latestMessages: [ChatMessage] = [],
pinnedMessages: [ChatMessage] = [],
muteDetails: MuteDetails? = nil,
- previewMessage: ChatMessage? = nil
+ previewMessage: ChatMessage? = nil,
+ draftMessage: DraftMessage? = nil
) -> Self {
self.init(
cid: cid,
@@ -120,7 +121,8 @@ public extension ChatChannel {
lastMessageFromCurrentUser: nil,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
- previewMessage: previewMessage
+ previewMessage: previewMessage,
+ draftMessage: draftMessage
)
}
@@ -148,7 +150,8 @@ public extension ChatChannel {
latestMessages: [ChatMessage] = [],
pinnedMessages: [ChatMessage] = [],
muteDetails: MuteDetails? = nil,
- previewMessage: ChatMessage? = nil
+ previewMessage: ChatMessage? = nil,
+ draftMessage: DraftMessage? = nil
) -> Self {
self.init(
cid: .init(type: .messaging, id: "!members" + .newUniqueId),
@@ -175,7 +178,8 @@ public extension ChatChannel {
lastMessageFromCurrentUser: nil,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
- previewMessage: previewMessage
+ previewMessage: previewMessage,
+ draftMessage: draftMessage
)
}
@@ -202,7 +206,8 @@ public extension ChatChannel {
latestMessages: [ChatMessage] = [],
pinnedMessages: [ChatMessage] = [],
muteDetails: MuteDetails? = nil,
- previewMessage: ChatMessage? = nil
+ previewMessage: ChatMessage? = nil,
+ draftMessage: DraftMessage? = nil
) -> Self {
self.init(
cid: .init(type: .messaging, id: .newUniqueId),
@@ -228,7 +233,8 @@ public extension ChatChannel {
lastMessageFromCurrentUser: nil,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
- previewMessage: previewMessage
+ previewMessage: previewMessage,
+ draftMessage: draftMessage
)
}
}
diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift
index 6e226e4cbbd..8851e5383eb 100644
--- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift
@@ -49,7 +49,8 @@ public extension ChatMessage {
readBy: Set = [],
underlyingContext: NSManagedObjectContext? = nil,
textUpdatedAt: Date? = nil,
- poll: Poll? = nil
+ poll: Poll? = nil,
+ draftReply: DraftMessage? = nil
) -> Self {
.init(
id: id,
@@ -89,7 +90,8 @@ public extension ChatMessage {
moderationDetails: moderationsDetails,
readBy: readBy,
poll: poll,
- textUpdatedAt: textUpdatedAt
+ textUpdatedAt: textUpdatedAt,
+ draftReply: draftReply
)
}
}
diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift
new file mode 100644
index 00000000000..bc23edb2345
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/DraftMessage_Mock.swift
@@ -0,0 +1,43 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+@testable import StreamChat
+
+public extension DraftMessage {
+ /// Creates a new `ChatMessage` object from the provided data.
+ static func mock(
+ id: MessageId = .unique,
+ cid: ChannelId = .unique,
+ threadId: MessageId? = nil,
+ text: String = .unique,
+ currentUser: ChatUser = .unique,
+ command: String? = nil,
+ createdAt: Date = Date(timeIntervalSince1970: 113),
+ arguments: String? = nil,
+ quotedMessage: ChatMessage? = nil,
+ showReplyInChannel: Bool = false,
+ extraData: [String: RawJSON] = [:],
+ isSilent: Bool = false,
+ mentionedUsers: Set = [],
+ attachments: [AnyChatMessageAttachment] = []
+ ) -> Self {
+ .init(
+ id: id,
+ cid: cid,
+ threadId: threadId,
+ text: text,
+ isSilent: isSilent,
+ command: command,
+ createdAt: createdAt,
+ arguments: arguments,
+ showReplyInChannel: showReplyInChannel,
+ extraData: extraData,
+ currentUser: currentUser,
+ quotedMessage: { quotedMessage },
+ mentionedUsers: mentionedUsers,
+ attachments: attachments
+ )
+ }
+}
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift
index 8b68ceb26b9..bd166069c44 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift
@@ -148,7 +148,13 @@ extension ChatClient {
internetConnection: { center, _ in
InternetConnection_Mock(notificationCenter: center)
},
- authenticationRepositoryBuilder: AuthenticationRepository_Mock.init
+ authenticationRepositoryBuilder: AuthenticationRepository_Mock.init,
+ syncRepositoryBuilder: SyncRepository_Mock.init,
+ pollsRepositoryBuilder: PollsRepository_Mock.init,
+ draftMessagesRepositoryBuilder: DraftMessagesRepository_Mock.init,
+ channelListUpdaterBuilder: ChannelListUpdater_Spy.init,
+ messageRepositoryBuilder: MessageRepository_Mock.init,
+ offlineRequestsRepositoryBuilder: OfflineRequestsRepository_Mock.init
)
)
}
@@ -203,6 +209,10 @@ extension ChatClient {
pollsRepository as! PollsRepository_Mock
}
+ var mockDraftMessagesRepository: DraftMessagesRepository_Mock {
+ draftMessagesRepository as! DraftMessagesRepository_Mock
+ }
+
func simulateProvidedConnectionId(connectionId: ConnectionId?) {
guard let connectionId = connectionId else {
webSocketClient(
@@ -241,6 +251,7 @@ extension ChatClient.Environment {
authenticationRepositoryBuilder: AuthenticationRepository_Mock.init,
syncRepositoryBuilder: SyncRepository_Mock.init,
pollsRepositoryBuilder: PollsRepository_Mock.init,
+ draftMessagesRepositoryBuilder: DraftMessagesRepository_Mock.init,
channelListUpdaterBuilder: ChannelListUpdater_Spy.init,
messageRepositoryBuilder: MessageRepository_Mock.init,
offlineRequestsRepositoryBuilder: OfflineRequestsRepository_Mock.init
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift
index 623eb91568a..3b77bcf6395 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelController_Mock.swift
@@ -65,6 +65,7 @@ class ChatChannelController_Mock: ChatChannelController {
quotedMessageId: MessageId? = nil,
skipPush: Bool = false,
skipEnrichUrl: Bool = false,
+ restrictedVisibility: [UserId] = [],
extraData: [String : RawJSON] = [:],
completion: ((Result) -> Void)? = nil
) {
@@ -132,6 +133,41 @@ class ChatChannelController_Mock: ChatChannelController {
) {
loadPageAroundMessageIdCallCount += 1
}
+
+ var updateDraftMessage_callCount = 0
+ var updateDraftMessage_completion: ((Result) -> Void)?
+ var updateDraftMessage_text = ""
+
+ override func updateDraftMessage(
+ text: String,
+ isSilent: Bool = false,
+ attachments: [AnyAttachmentPayload] = [],
+ mentionedUserIds: [UserId] = [],
+ quotedMessageId: MessageId? = nil,
+ command: Command? = nil,
+ extraData: [String : RawJSON] = [:],
+ completion: ((Result) -> Void)? = nil
+ ) {
+ updateDraftMessage_text = text
+ updateDraftMessage_callCount += 1
+ updateDraftMessage_completion = completion
+ }
+
+ var deleteDraftMessage_callCount = 0
+ var deleteDraftMessage_completion: ((Error?) -> Void)?
+
+ override func deleteDraftMessage(completion: ((Error?) -> Void)? = nil) {
+ deleteDraftMessage_callCount += 1
+ deleteDraftMessage_completion = completion
+ }
+
+ var loadDraftMessage_callCount = 0
+ var loadDraftMessage_completion: ((Result) -> Void)?
+
+ override func loadDraftMessage(completion: ((Result) -> Void)? = nil) {
+ loadDraftMessage_callCount += 1
+ loadDraftMessage_completion = completion
+ }
}
extension ChatChannelController_Mock {
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
index 0a6830d8ef0..8491c8dcf4e 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
@@ -7,7 +7,6 @@ import Foundation
/// This class allows you to wrap an existing `DatabaseSession` and adjust the behavior of its methods.
class DatabaseSession_Mock: DatabaseSession {
-
/// The wrapped session
let underlyingSession: DatabaseSession
@@ -140,6 +139,7 @@ class DatabaseSession_Mock: DatabaseSession {
skipPush: Bool,
skipEnrichUrl: Bool,
poll: PollPayload?,
+ restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON]
) throws -> MessageDTO {
try throwErrorIfNeeded()
@@ -162,6 +162,35 @@ class DatabaseSession_Mock: DatabaseSession {
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
poll: poll,
+ restrictedVisibility: restrictedVisibility,
+ extraData: extraData
+ )
+ }
+
+ func createNewDraftMessage(
+ in cid: ChannelId,
+ text: String,
+ command: String?,
+ arguments: String?,
+ parentMessageId: MessageId?,
+ attachments: [AnyAttachmentPayload],
+ mentionedUserIds: [UserId],
+ showReplyInChannel: Bool,
+ isSilent: Bool,
+ quotedMessageId: MessageId?,
+ extraData: [String : RawJSON]
+ ) throws -> MessageDTO {
+ try underlyingSession.createNewDraftMessage(
+ in: cid,
+ text: text,
+ command: command,
+ arguments: arguments,
+ parentMessageId: parentMessageId,
+ attachments: attachments,
+ mentionedUserIds: mentionedUserIds,
+ showReplyInChannel: showReplyInChannel,
+ isSilent: isSilent,
+ quotedMessageId: quotedMessageId,
extraData: extraData
)
}
@@ -170,15 +199,58 @@ class DatabaseSession_Mock: DatabaseSession {
payload: MessagePayload,
for cid: ChannelId?,
syncOwnReactions: Bool,
+ skipDraftUpdate: Bool,
cache: PreWarmedCache?
) throws -> MessageDTO {
try throwErrorIfNeeded()
- return try underlyingSession.saveMessage(payload: payload, for: cid, syncOwnReactions: syncOwnReactions, cache: cache)
+ return try underlyingSession.saveMessage(
+ payload: payload,
+ for: cid,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: skipDraftUpdate,
+ cache: cache
+ )
}
- func saveMessage(payload: MessagePayload, channelDTO: ChannelDTO, syncOwnReactions: Bool, cache: PreWarmedCache?) throws -> MessageDTO {
+ func saveMessage(
+ payload: MessagePayload,
+ for cid: ChannelId?,
+ syncOwnReactions: Bool,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO {
+ try saveMessage(payload: payload, for: cid, syncOwnReactions: syncOwnReactions, skipDraftUpdate: false, cache: cache)
+ }
+
+ func saveMessage(
+ payload: MessagePayload,
+ channelDTO: ChannelDTO,
+ syncOwnReactions: Bool,
+ skipDraftUpdate: Bool,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO {
try throwErrorIfNeeded()
- return try underlyingSession.saveMessage(payload: payload, channelDTO: channelDTO, syncOwnReactions: syncOwnReactions, cache: cache)
+ return try underlyingSession.saveMessage(
+ payload: payload,
+ channelDTO: channelDTO,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: skipDraftUpdate,
+ cache: cache
+ )
+ }
+
+ func saveMessage(
+ payload: MessagePayload,
+ channelDTO: ChannelDTO,
+ syncOwnReactions: Bool,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO {
+ try saveMessage(
+ payload: payload,
+ channelDTO: channelDTO,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: false,
+ cache: cache
+ )
}
func saveMessages(messagesPayload: MessageListPayload, for cid: ChannelId?, syncOwnReactions: Bool) -> [MessageDTO] {
@@ -210,6 +282,14 @@ class DatabaseSession_Mock: DatabaseSession {
underlyingSession.delete(message: message)
}
+ func saveDraftMessage(payload: DraftPayload, for cid: ChannelId, cache: PreWarmedCache?) throws -> MessageDTO {
+ try underlyingSession.saveDraftMessage(payload: payload, for: cid, cache: cache)
+ }
+
+ func deleteDraftMessage(in cid: ChannelId, threadId: MessageId?) {
+ underlyingSession.deleteDraftMessage(in: cid, threadId: threadId)
+ }
+
func preview(for cid: ChannelId) -> MessageDTO? {
underlyingSession.preview(for: cid)
}
@@ -529,3 +609,37 @@ private extension DatabaseSession_Mock {
throw error
}
}
+
+extension DatabaseSession {
+ @discardableResult
+ func saveMessage(
+ payload: MessagePayload,
+ for cid: ChannelId?,
+ syncOwnReactions: Bool,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO {
+ try self.saveMessage(
+ payload: payload,
+ for: cid,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: false,
+ cache: cache
+ )
+ }
+
+ @discardableResult
+ func saveMessage(
+ payload: MessagePayload,
+ channelDTO: ChannelDTO,
+ syncOwnReactions: Bool,
+ cache: PreWarmedCache?
+ ) throws -> MessageDTO {
+ try self.saveMessage(
+ payload: payload,
+ channelDTO: channelDTO,
+ syncOwnReactions: syncOwnReactions,
+ skipDraftUpdate: false,
+ cache: cache
+ )
+ }
+}
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift
new file mode 100644
index 00000000000..ef00a1b8693
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/DraftMessagesRepository_Mock.swift
@@ -0,0 +1,104 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+import XCTest
+
+final class DraftMessagesRepository_Mock: DraftMessagesRepository {
+ // MARK: - Load Drafts
+
+ var loadDrafts_callCount = 0
+ var loadDrafts_calledWith: DraftListQuery?
+ var loadDrafts_completion: ((Result) -> Void)?
+
+ override func loadDrafts(
+ query: DraftListQuery,
+ completion: @escaping (Result) -> Void
+ ) {
+ loadDrafts_callCount += 1
+ loadDrafts_calledWith = query
+ loadDrafts_completion = completion
+ }
+
+ // MARK: - Update Draft
+
+ var updateDraft_callCount = 0
+ var updateDraft_calledWith: (
+ cid: ChannelId,
+ threadId: MessageId?,
+ text: String,
+ isSilent: Bool,
+ showReplyInChannel: Bool,
+ command: String?,
+ arguments: String?,
+ attachments: [AnyAttachmentPayload],
+ mentionedUserIds: [UserId],
+ quotedMessageId: MessageId?,
+ extraData: [String: RawJSON]
+ )?
+ var updateDraft_completion: ((Result) -> Void)?
+
+ override func updateDraft(
+ for cid: ChannelId,
+ threadId: MessageId?,
+ text: String,
+ isSilent: Bool,
+ showReplyInChannel: Bool,
+ command: String?,
+ arguments: String?,
+ attachments: [AnyAttachmentPayload],
+ mentionedUserIds: [UserId],
+ quotedMessageId: MessageId?,
+ extraData: [String: RawJSON],
+ completion: ((Result) -> Void)?
+ ) {
+ updateDraft_callCount += 1
+ updateDraft_calledWith = (
+ cid: cid,
+ threadId: threadId,
+ text: text,
+ isSilent: isSilent,
+ showReplyInChannel: showReplyInChannel,
+ command: command,
+ arguments: arguments,
+ attachments: attachments,
+ mentionedUserIds: mentionedUserIds,
+ quotedMessageId: quotedMessageId,
+ extraData: extraData
+ )
+ updateDraft_completion = completion
+ }
+
+ // MARK: - Get Draft
+
+ var getDraft_callCount = 0
+ var getDraft_calledWith: (cid: ChannelId, threadId: MessageId?)?
+ var getDraft_completion: ((Result) -> Void)?
+
+ override func getDraft(
+ for cid: ChannelId,
+ threadId: MessageId?,
+ completion: ((Result) -> Void)?
+ ) {
+ getDraft_callCount += 1
+ getDraft_calledWith = (cid: cid, threadId: threadId)
+ getDraft_completion = completion
+ }
+
+ // MARK: - Delete Draft
+
+ var deleteDraft_callCount = 0
+ var deleteDraft_calledWith: (cid: ChannelId, threadId: MessageId?)?
+ var deleteDraft_completion: ((Error?) -> Void)?
+
+ override func deleteDraft(
+ for cid: ChannelId,
+ threadId: MessageId?,
+ completion: @escaping (Error?) -> Void
+ ) {
+ deleteDraft_callCount += 1
+ deleteDraft_calledWith = (cid: cid, threadId: threadId)
+ deleteDraft_completion = completion
+ }
+}
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift
index 64613be9ad3..4eb034f924a 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/State/Chat_Mock.swift
@@ -59,7 +59,8 @@ public class Chat_Mock: Chat, Spy {
silent: Bool = false,
skipPushNotification: Bool = false,
skipEnrichURL: Bool = false,
- messageId: MessageId? = nil
+ messageId: MessageId? = nil,
+ restrictedVisibility: [UserId] = []
) async throws -> ChatMessage {
createNewMessageCallCount += 1
return ChatMessage.mock()
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
index cfea188495b..77a1393c7fd 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
@@ -353,6 +353,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
quotedMessageId: MessageId?,
skipPush: Bool,
skipEnrichUrl: Bool,
+ restrictedVisibility: [UserId] = [],
poll: PollPayload?,
extraData: [String: RawJSON] = [:],
completion: ((Result) -> Void)? = nil
diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift
index 7eb16e4d5c6..48e5b32ddd6 100644
--- a/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift
+++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift
@@ -322,7 +322,13 @@ extension DatabaseContainer {
reactionCounts: reactionCounts
)
- let messageDTO = try session.saveMessage(payload: message, channelDTO: channelDTO, syncOwnReactions: true, cache: nil)
+ let messageDTO = try session.saveMessage(
+ payload: message,
+ channelDTO: channelDTO,
+ syncOwnReactions: true,
+ skipDraftUpdate: true,
+ cache: nil)
+
messageDTO.localMessageState = localState
messageDTO.reactionCounts = reactionCounts.mapKeys(\.rawValue)
messageDTO.reactionScores = reactionScores.mapKeys(\.rawValue)
@@ -340,7 +346,13 @@ extension DatabaseContainer {
extraData: extraData
)
- let replyDTO = try session.saveMessage(payload: reply, for: cid, syncOwnReactions: true, cache: nil)
+ let replyDTO = try session.saveMessage(
+ payload: reply,
+ for: cid,
+ syncOwnReactions: true,
+ skipDraftUpdate: true,
+ cache: nil
+ )
messageDTO.replies.insert(replyDTO)
}
}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/ChannelPayload.swift b/TestTools/StreamChatTestTools/TestData/DummyData/ChannelPayload.swift
index b1a3c59c49c..7dd7c0a72e2 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/ChannelPayload.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/ChannelPayload.swift
@@ -17,7 +17,8 @@ extension ChannelPayload {
pendingMessages: [MessagePayload] = [],
pinnedMessages: [MessagePayload] = [],
channelReads: [ChannelReadPayload] = [],
- isHidden: Bool? = nil
+ isHidden: Bool? = nil,
+ draft: DraftPayload? = nil
) -> Self {
.init(
channel: channel,
@@ -29,7 +30,8 @@ extension ChannelPayload {
pendingMessages: pendingMessages,
pinnedMessages: pinnedMessages,
channelReads: channelReads,
- isHidden: isHidden
+ isHidden: isHidden,
+ draft: draft
)
}
}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/DraftPayload.swift b/TestTools/StreamChatTestTools/TestData/DummyData/DraftPayload.swift
new file mode 100644
index 00000000000..b294b6b4e2d
--- /dev/null
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/DraftPayload.swift
@@ -0,0 +1,55 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+@testable import StreamChat
+
+extension DraftPayload {
+ /// Returns dummy draft payload with the given values.
+ static func dummy(
+ cid: ChannelId? = nil,
+ channelPayload: ChannelDetailPayload? = nil,
+ createdAt: Date = .unique,
+ message: DraftMessagePayload = .dummy(),
+ quotedMessage: MessagePayload? = nil,
+ parentId: String? = nil,
+ parentMessage: MessagePayload? = nil
+ ) -> DraftPayload {
+ .init(
+ cid: cid,
+ channelPayload: channelPayload,
+ createdAt: createdAt,
+ message: message,
+ quotedMessage: quotedMessage,
+ parentId: parentId,
+ parentMessage: parentMessage
+ )
+ }
+}
+
+extension DraftMessagePayload {
+ static func dummy(
+ id: String = .unique,
+ text: String = .unique,
+ command: String? = nil,
+ args: String? = nil,
+ showReplyInChannel: Bool = false,
+ mentionedUsers: [UserPayload]? = nil,
+ extraData: [String: RawJSON] = [:],
+ attachments: [MessageAttachmentPayload]? = nil,
+ isSilent: Bool = false
+ ) -> DraftMessagePayload {
+ .init(
+ id: id,
+ text: text,
+ command: command,
+ args: args,
+ showReplyInChannel: showReplyInChannel,
+ mentionedUsers: mentionedUsers,
+ extraData: extraData,
+ attachments: attachments,
+ isSilent: isSilent
+ )
+ }
+}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/MessagePayload.swift b/TestTools/StreamChatTestTools/TestData/DummyData/MessagePayload.swift
index 0288cbbaf41..16aea0d94d5 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/MessagePayload.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/MessagePayload.swift
@@ -49,7 +49,8 @@ extension MessagePayload {
moderationDetails: MessageModerationDetailsPayload? = nil,
mentionedUsers: [UserPayload] = [.dummy(userId: .unique)],
messageTextUpdatedAt: Date? = nil,
- poll: PollPayload? = nil
+ poll: PollPayload? = nil,
+ draft: DraftPayload? = nil
) -> MessagePayload {
.init(
id: messageId,
@@ -89,7 +90,8 @@ extension MessagePayload {
moderation: moderation,
moderationDetails: moderationDetails,
messageTextUpdatedAt: messageTextUpdatedAt,
- poll: poll
+ poll: poll,
+ draft: draft
)
}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/ThreadPayload.swift b/TestTools/StreamChatTestTools/TestData/DummyData/ThreadPayload.swift
index 9fa46c9ad55..4826f568588 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/ThreadPayload.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/ThreadPayload.swift
@@ -22,6 +22,7 @@ extension ThreadPayload {
title: String? = nil,
latestReplies: [MessagePayload] = [],
read: [ThreadReadPayload] = [],
+ draft: DraftPayload? = nil,
extraData: [String: RawJSON] = [:]
) -> Self {
.init(
@@ -38,6 +39,7 @@ extension ThreadPayload {
title: title,
latestReplies: latestReplies,
read: read,
+ draft: draft,
extraData: extraData
)
}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
index 218a14ea64c..cbb963852af 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
@@ -201,7 +201,8 @@ extension XCTestCase {
pendingMessages: pendingMessages,
pinnedMessages: pinnedMessages,
channelReads: channelReads ?? [dummyChannelRead],
- isHidden: false
+ isHidden: false,
+ draft: nil
)
return payload
@@ -316,7 +317,8 @@ extension XCTestCase {
pendingMessages: nil,
pinnedMessages: [dummyMessageWithNoExtraData],
channelReads: [dummyChannelReadWithNoExtraData],
- isHidden: nil
+ isHidden: nil,
+ draft: nil
)
return payload
@@ -336,6 +338,7 @@ extension XCTestCase {
title: String? = .unique,
latestReplies: [MessagePayload] = [],
read: [ThreadReadPayload] = [],
+ draft: DraftPayload? = nil,
extraData: [String: RawJSON] = [:]
) -> ThreadPayload {
.init(
@@ -352,6 +355,7 @@ extension XCTestCase {
title: title,
latestReplies: latestReplies,
read: read,
+ draft: draft,
extraData: extraData
)
}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/CallEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/CallEndpoints_Tests.swift
deleted file mode 100644
index 7fa29f43338..00000000000
--- a/Tests/StreamChatTests/APIClient/Endpoints/CallEndpoints_Tests.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-//
-// Copyright © 2025 Stream.io Inc. All rights reserved.
-//
-
-@testable import StreamChat
-@testable import StreamChatTestTools
-import XCTest
-
-final class CallEndpoints_Tests: XCTestCase {
- func test_getCallToken_buildsCorrectly() {
- let callId: String = .unique
-
- // GIVEN
- let expectedEndpoint = Endpoint(
- path: .callToken(callId),
- method: .post,
- queryItems: nil,
- requiresConnectionId: false,
- requiresToken: true
- )
-
- // WHEN
- let endpoint: Endpoint = .getCallToken(callId: callId)
-
- // THEN
- XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
- XCTAssertEqual("calls/\(callId)", endpoint.path.value)
- }
-
- func test_createCall_buildsCorrectly() {
- let channedId: ChannelId = .unique
- let callId: String = .unique
- let callType = "video"
-
- // GIVEN
- let expectedEndpoint: Endpoint = Endpoint(
- path: .createCall(channedId.apiPath),
- method: .post,
- body: CallRequestBody(id: callId, type: callType)
- )
-
- // WHEN
- let endpoint: Endpoint = .createCall(cid: channedId, callId: callId, type: callType)
-
- // THEN
- XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
- XCTAssertEqual("channels/\(channedId.apiPath)/call", endpoint.path.value)
- }
-}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/DraftEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/DraftEndpoints_Tests.swift
new file mode 100644
index 00000000000..55c04c1defd
--- /dev/null
+++ b/Tests/StreamChatTests/APIClient/Endpoints/DraftEndpoints_Tests.swift
@@ -0,0 +1,142 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class DraftEndpoints_Tests: XCTestCase {
+ func test_drafts() throws {
+ let query = DraftListQuery()
+ let endpoint = Endpoint.drafts(query: query)
+ let body = try AnyEndpoint(endpoint).bodyAsDictionary()
+ let expectedBody: [String: Any] = [
+ "limit": 25,
+ "sort": [
+ ["field": "created_at", "direction": -1]
+ ]
+ ]
+
+ XCTAssertEqual(endpoint.method, .post)
+ XCTAssertEqual(endpoint.path.value, "drafts/query")
+ XCTAssertEqual(endpoint.requiresConnectionId, false)
+ XCTAssertNil(endpoint.queryItems)
+ AssertDictionary(body, expectedBody)
+ }
+
+ func test_updateDraftMessage() throws {
+ let cid = ChannelId(type: .messaging, id: "123")
+ let requestBody = DraftMessageRequestBody(
+ id: "draft-id",
+ text: "Hello",
+ command: nil,
+ args: nil,
+ parentId: "parent-id",
+ showReplyInChannel: true,
+ isSilent: false,
+ quotedMessageId: "quoted-id",
+ attachments: [],
+ mentionedUserIds: ["user1", "user2"],
+ extraData: [:]
+ )
+
+ let endpoint = Endpoint.updateDraftMessage(
+ channelId: cid,
+ requestBody: requestBody
+ )
+
+ let body = try AnyEndpoint(endpoint).bodyAsDictionary()
+ let expectedBody: [String: Any] = [
+ "message": [
+ "id": "draft-id",
+ "text": "Hello",
+ "parent_id": "parent-id",
+ "show_in_channel": true,
+ "silent": false,
+ "quoted_message_id": "quoted-id",
+ "mentioned_users": ["user1", "user2"]
+ ]
+ ]
+
+ XCTAssertEqual(endpoint.method, .post)
+ XCTAssertEqual(endpoint.path.value, "channels/messaging/123/draft")
+ XCTAssertEqual(endpoint.requiresConnectionId, false)
+ XCTAssertNil(endpoint.queryItems)
+ AssertDictionary(body, expectedBody)
+ }
+
+ func test_getDraftMessage() throws {
+ let cid = ChannelId(type: .messaging, id: "123")
+ let threadId = "thread-id"
+
+ let endpoint = Endpoint.getDraftMessage(
+ channelId: cid,
+ threadId: threadId
+ )
+
+ let queryItems = try AnyEndpoint(endpoint).queryItemsAsDictionary()
+ let expectedQueryItems: [String: Any] = [
+ "parent_id": "thread-id"
+ ]
+
+ XCTAssertEqual(endpoint.method, .get)
+ XCTAssertEqual(endpoint.path.value, "channels/messaging/123/draft")
+ XCTAssertEqual(endpoint.requiresConnectionId, false)
+ XCTAssertNil(endpoint.body)
+ AssertDictionary(queryItems, expectedQueryItems)
+ }
+
+ func test_getDraftMessage_withoutThreadId() throws {
+ let cid = ChannelId(type: .messaging, id: "123")
+
+ let endpoint = Endpoint.getDraftMessage(
+ channelId: cid,
+ threadId: nil
+ )
+
+ XCTAssertEqual(endpoint.method, .get)
+ XCTAssertEqual(endpoint.path.value, "channels/messaging/123/draft")
+ XCTAssertEqual(endpoint.requiresConnectionId, false)
+ XCTAssertNil(endpoint.body)
+ XCTAssertNil(endpoint.queryItems)
+ }
+
+ func test_deleteDraftMessage() throws {
+ let cid = ChannelId(type: .messaging, id: "123")
+ let threadId = "thread-id"
+
+ let endpoint = Endpoint.deleteDraftMessage(
+ channelId: cid,
+ threadId: threadId
+ )
+
+ let body = try AnyEndpoint(endpoint).bodyAsDictionary()
+ let expectedBody: [String: Any] = [
+ "parent_id": "thread-id"
+ ]
+
+ XCTAssertEqual(endpoint.method, .delete)
+ XCTAssertEqual(endpoint.path.value, "channels/messaging/123/draft")
+ XCTAssertEqual(endpoint.requiresConnectionId, false)
+ XCTAssertNil(endpoint.queryItems)
+ AssertDictionary(body, expectedBody)
+ }
+
+ func test_deleteDraftMessage_withoutThreadId() throws {
+ let cid = ChannelId(type: .messaging, id: "123")
+
+ let endpoint = Endpoint.deleteDraftMessage(
+ channelId: cid,
+ threadId: nil
+ )
+
+ XCTAssertEqual(endpoint.method, .delete)
+ XCTAssertEqual(endpoint.path.value, "channels/messaging/123/draft")
+ XCTAssertEqual(endpoint.requiresConnectionId, false)
+ XCTAssertNil(endpoint.queryItems)
+ XCTAssertNil(endpoint.body)
+ }
+}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
index 1a35aef3aaf..852df46419c 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
@@ -80,6 +80,14 @@ final class EndpointPathTests: XCTestCase {
XCTAssertEqual(path, "channels/\(cid.apiPath)/member/1")
}
+ func test_drafts_shouldNOTBeQueuedOffline() {
+ XCTAssertFalse(EndpointPath.drafts.shouldBeQueuedOffline)
+ }
+
+ func test_draftMessage_shouldBeQueuedOffline() {
+ XCTAssertTrue(EndpointPath.draftMessage(.unique).shouldBeQueuedOffline)
+ }
+
// MARK: - Codable
func test_isProperlyEncodedAndDecoded() throws {
@@ -135,6 +143,9 @@ final class EndpointPathTests: XCTestCase {
assertResultEncodingAndDecoding(.pollOption(pollId: "test_poll", optionId: "option_id"))
assertResultEncodingAndDecoding(.pollVoteInMessage(messageId: "test_message", pollId: "test_poll"))
assertResultEncodingAndDecoding(.pollVote(messageId: "test_message", pollId: "test_poll", voteId: "test_vote"))
+
+ assertResultEncodingAndDecoding(.drafts)
+ assertResultEncodingAndDecoding(.draftMessage(ChannelId(type: .messaging, id: "test_channel")))
}
}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
index 61b59ce987f..50dc214ca9c 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
@@ -247,7 +247,8 @@ final class ChannelListPayload_Tests: XCTestCase {
unreadMessagesCount: (0..<10).randomElement()!
)
},
- isHidden: false
+ isHidden: false,
+ draft: nil
)
}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift
deleted file mode 100644
index 2da85f4bdef..00000000000
--- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/CreateCallPayload_Tests.swift
+++ /dev/null
@@ -1,62 +0,0 @@
-//
-// Copyright © 2025 Stream.io Inc. All rights reserved.
-//
-
-import Foundation
-@testable import StreamChat
-import StreamChatTestTools
-import XCTest
-
-final class CreateCallPayload_Tests: XCTestCase {
- func test_createCallPayload_decodedCorrectly() throws {
- // Create Call Payload data
- let id: String = .unique
- let provider: String = "agora"
- let token: String = .unique
- let channel: String = .unique
- let roomId: String = .unique
- let roomName: String = .unique
- let agoraUid: UInt = 10
- let agoraAppId: String = .unique
-
- let expectedPayload = CreateCallPayload(
- call: CallPayload(
- id: id,
- provider: provider,
- agora: AgoraPayload(channel: channel),
- hms: HMSPayload(
- roomId: roomId,
- roomName: roomName
- )
- ),
- token: token,
- agoraUid: agoraUid,
- agoraAppId: agoraAppId
- )
-
- // GIVEN
- let mockData: [String: Any] = [
- "call": [
- "id": id,
- "provider": provider,
- "agora": [
- "channel": channel
- ],
- "hms": [
- "room_id": roomId,
- "room_name": roomName
- ]
- ] as [String: Any],
- "token": token,
- "agora_uid": agoraUid,
- "agora_app_id": agoraAppId
- ]
- let mockJson = try JSONSerialization.data(withJSONObject: mockData)
-
- // WHEN
- let payload = try JSONDecoder.default.decode(CreateCallPayload.self, from: mockJson)
-
- // THEN
- XCTAssertEqual(expectedPayload, payload)
- }
-}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/DraftPayloads_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/DraftPayloads_Tests.swift
new file mode 100644
index 00000000000..64b6f8b7728
--- /dev/null
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/DraftPayloads_Tests.swift
@@ -0,0 +1,91 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+private extension Data {
+ static let draftMessage = XCTestCase.mockData(fromJSONFile: "DraftMessage")
+}
+
+final class DraftPayloads_Tests: XCTestCase {
+ func test_draftPayloadResponse_decodingFromJSON() throws {
+ // Given
+ let json = try JSONDecoder.default.decode(DraftPayloadResponse.self, from: .draftMessage)
+
+ // Then
+ XCTAssertEqual(json.draft.cid?.rawValue, "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E")
+ XCTAssertEqual(json.draft.message.id, "eb8ab3c9-5ade-4b4d-b32d-3d7f037682c0")
+ XCTAssertEqual(json.draft.message.text, "Hi @Leia Organa ahah")
+ XCTAssertEqual(json.draft.message.mentionedUsers?.count, 1)
+ XCTAssertEqual(json.draft.message.mentionedUsers?.first?.id, "leia_organa")
+ XCTAssertFalse(json.draft.message.showReplyInChannel)
+ XCTAssertFalse(json.draft.message.isSilent)
+ XCTAssertNil(json.draft.parentMessage)
+ XCTAssertNil(json.draft.quotedMessage)
+ XCTAssertNotNil(json.draft.channelPayload)
+ }
+
+ func test_draftListPayloadResponse_decodingFromJSON() throws {
+ let jsonString = """
+ {
+ "drafts": [{
+ "channel_cid": "messaging:123",
+ "created_at": "2025-02-11T12:27:04.780633395Z",
+ "message": {
+ "id": "draft-1",
+ "text": "Hello world"
+ }
+ }],
+ "next": "next-page-token"
+ }
+ """
+ let data = Data(jsonString.utf8)
+
+ // When
+ let response = try JSONDecoder.default.decode(DraftListPayloadResponse.self, from: data)
+
+ // Then
+ XCTAssertEqual(response.drafts.count, 1)
+ XCTAssertEqual(response.drafts[0].cid?.rawValue, "messaging:123")
+ XCTAssertEqual(response.drafts[0].message.id, "draft-1")
+ XCTAssertEqual(response.drafts[0].message.text, "Hello world")
+ XCTAssertEqual(response.next, "next-page-token")
+ }
+
+ func test_draftMessageRequestBody_encoding() throws {
+ // Given
+ let requestBody = DraftMessageRequestBody(
+ id: "draft-id",
+ text: "Hello @user1",
+ command: "/giphy",
+ args: "hello",
+ parentId: "parent-123",
+ showReplyInChannel: true,
+ isSilent: false,
+ quotedMessageId: "quoted-123",
+ attachments: [],
+ mentionedUserIds: ["user1"],
+ extraData: ["custom_field": .string("value")]
+ )
+
+ // When
+ let encodedData = try JSONEncoder.default.encode(requestBody)
+ let decodedJSON = try JSONDecoder.default.decode([String: RawJSON].self, from: encodedData)
+
+ // Then
+ XCTAssertEqual(decodedJSON["id"]?.stringValue, "draft-id")
+ XCTAssertEqual(decodedJSON["text"]?.stringValue, "Hello @user1")
+ XCTAssertEqual(decodedJSON["command"]?.stringValue, "/giphy")
+ XCTAssertEqual(decodedJSON["args"]?.stringValue, "hello")
+ XCTAssertEqual(decodedJSON["parent_id"]?.stringValue, "parent-123")
+ XCTAssertEqual(decodedJSON["show_in_channel"]?.boolValue, true)
+ XCTAssertEqual(decodedJSON["silent"]?.boolValue, false)
+ XCTAssertEqual(decodedJSON["quoted_message_id"]?.stringValue, "quoted-123")
+ XCTAssertEqual(decodedJSON["mentioned_users"]?.stringArrayValue, ["user1"])
+ XCTAssertEqual(decodedJSON["custom_field"]?.stringValue, "value")
+ }
+}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift
index 2184badce02..12e07939c48 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift
@@ -403,7 +403,8 @@ final class IdentifiablePayload_Tests: XCTestCase {
unreadMessagesCount: (0..<10).randomElement()!
)
},
- isHidden: false
+ isHidden: false,
+ draft: nil
)
}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift
index a12103b9832..65e35c8dc8a 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift
@@ -155,6 +155,7 @@ final class MessageRequestBody_Tests: XCTestCase {
mentionedUserIds: [.unique],
pinned: true,
pinExpires: "2021-05-15T06:43:08.776Z".toDate(),
+ restrictedVisibility: ["test"],
extraData: ["secret_note": .string("Anakin is Vader ;-)")]
)
@@ -171,7 +172,8 @@ final class MessageRequestBody_Tests: XCTestCase {
"secret_note": "Anakin is Vader ;-)",
"command": payload.command!,
"pinned": true,
- "pin_expires": "2021-05-15T06:43:08.776Z"
+ "pin_expires": "2021-05-15T06:43:08.776Z",
+ "restricted_visibility": ["test"]
]
let expectedJSON = try JSONSerialization.data(withJSONObject: expected, options: [])
AssertJSONEqual(serializedJSON, expectedJSON)
diff --git a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Drafts_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Drafts_Tests.swift
new file mode 100644
index 00000000000..13ff832bdc3
--- /dev/null
+++ b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController+Drafts_Tests.swift
@@ -0,0 +1,202 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class ChannelController_Drafts_Tests: XCTestCase {
+ var client: ChatClient_Mock!
+ var controller: ChatChannelController!
+ var draftsRepository: DraftMessagesRepository_Mock!
+
+ override func setUp() {
+ super.setUp()
+
+ client = ChatClient.mock
+ draftsRepository = client.draftMessagesRepository as? DraftMessagesRepository_Mock
+
+ let query = ChannelQuery(cid: .unique)
+ controller = ChatChannelController(
+ channelQuery: query,
+ channelListQuery: nil,
+ client: client,
+ isChannelAlreadyCreated: true
+ )
+ }
+
+ override func tearDown() {
+ client.cleanUp()
+ draftsRepository = nil
+ controller = nil
+ client = nil
+
+ super.tearDown()
+ }
+
+ // MARK: - Update Draft Tests
+
+ func test_updateDraftMessage_whenChannelNotCreated_fails() {
+ // Create controller with non-created channel
+ controller = ChatChannelController(
+ channelQuery: .init(cid: .unique),
+ channelListQuery: nil,
+ client: client,
+ isChannelAlreadyCreated: false
+ )
+
+ let expectation = expectation(description: "updateDraft completion called")
+ controller.updateDraftMessage(text: "test") { result in
+ if case .failure(let error) = result {
+ XCTAssertTrue(error is ClientError.ChannelNotCreatedYet)
+ expectation.fulfill()
+ }
+ }
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.updateDraft_callCount, 0)
+ }
+
+ func test_updateDraftMessage_whenSuccessful() {
+ let text = "Draft message"
+ let message = DraftMessage.mock(text: text)
+
+ let expectation = expectation(description: "updateDraft completion called")
+ controller.updateDraftMessage(
+ text: text,
+ isSilent: false,
+ attachments: [],
+ mentionedUserIds: [],
+ quotedMessageId: nil,
+ extraData: [:]
+ ) { result in
+ XCTAssertEqual(try? result.get(), message)
+ expectation.fulfill()
+ }
+
+ draftsRepository.updateDraft_completion?(.success(message))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.updateDraft_callCount, 1)
+ let calledWith = draftsRepository.updateDraft_calledWith
+ XCTAssertEqual(calledWith?.cid, controller.cid)
+ XCTAssertEqual(calledWith?.threadId, nil)
+ }
+
+ func test_updateDraftMessage_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "updateDraft completion called")
+ controller.updateDraftMessage(text: "test") { result in
+ XCTAssertEqual(error, result.error as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.updateDraft_completion?(.failure(error))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.updateDraft_callCount, 1)
+ }
+
+ // MARK: - Load Draft Tests
+
+ func test_loadDraftMessage_whenChannelNotCreated_fails() {
+ // Create controller with non-created channel
+ controller = ChatChannelController(
+ channelQuery: .init(cid: .unique),
+ channelListQuery: nil,
+ client: client,
+ isChannelAlreadyCreated: false
+ )
+
+ let expectation = expectation(description: "loadDraft completion called")
+ controller.loadDraftMessage { result in
+ if case .failure(let error) = result {
+ XCTAssertTrue(error is ClientError.ChannelNotCreatedYet)
+ expectation.fulfill()
+ }
+ }
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.getDraft_callCount, 0)
+ }
+
+ func test_loadDraftMessage_whenSuccessful() {
+ let message = DraftMessage.mock()
+
+ let expectation = expectation(description: "loadDraft completion called")
+ controller.loadDraftMessage { result in
+ XCTAssertEqual(try? result.get(), message)
+ expectation.fulfill()
+ }
+
+ draftsRepository.getDraft_completion?(.success(message))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.getDraft_callCount, 1)
+ }
+
+ func test_loadDraftMessage_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "loadDraft completion called")
+ controller.loadDraftMessage { result in
+ XCTAssertEqual(error, result.error as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.getDraft_completion?(.failure(error))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.getDraft_callCount, 1)
+ }
+
+ // MARK: - Delete Draft Tests
+
+ func test_deleteDraftMessage_whenChannelNotCreated_fails() {
+ // Create controller with non-created channel
+ controller = ChatChannelController(
+ channelQuery: .init(cid: .unique),
+ channelListQuery: nil,
+ client: client,
+ isChannelAlreadyCreated: false
+ )
+
+ let expectation = expectation(description: "deleteDraft completion called")
+ controller.deleteDraftMessage { error in
+ XCTAssertTrue(error is ClientError.ChannelNotCreatedYet)
+ expectation.fulfill()
+ }
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.deleteDraft_callCount, 0)
+ }
+
+ func test_deleteDraftMessage_whenSuccessful() {
+ let expectation = expectation(description: "deleteDraft completion called")
+ controller.deleteDraftMessage { error in
+ XCTAssertNil(error)
+ expectation.fulfill()
+ }
+
+ draftsRepository.deleteDraft_completion?(nil)
+
+ waitForExpectations(timeout: defaultTimeout)
+ }
+
+ func test_deleteDraftMessage_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "deleteDraft completion called")
+ controller.deleteDraftMessage { receivedError in
+ XCTAssertEqual(error, receivedError as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.deleteDraft_completion?(error)
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.deleteDraft_callCount, 1)
+ }
+}
diff --git a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
index 47e8f3b72d3..6c2a4af21cb 100644
--- a/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
+++ b/Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
@@ -954,6 +954,7 @@ final class ChannelController_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
// Simulate sending failed for this message
diff --git a/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Drafts_Tests.swift b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Drafts_Tests.swift
new file mode 100644
index 00000000000..1001d1e2966
--- /dev/null
+++ b/Tests/StreamChatTests/Controllers/CurrentUserController/CurrentUserController+Drafts_Tests.swift
@@ -0,0 +1,222 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class CurrentUserController_Drafts_Tests: XCTestCase {
+ var client: ChatClient_Mock!
+ var controller: CurrentChatUserController!
+ var draftsRepository: DraftMessagesRepository_Mock!
+
+ override func setUp() {
+ super.setUp()
+
+ client = ChatClient.mock
+ draftsRepository = client.draftMessagesRepository as? DraftMessagesRepository_Mock
+ controller = CurrentChatUserController(client: client)
+ }
+
+ override func tearDown() {
+ client.cleanUp()
+ draftsRepository = nil
+ controller = nil
+ client = nil
+
+ super.tearDown()
+ }
+
+ // MARK: - Load Draft Messages Tests
+
+ func test_loadDraftMessages_whenSuccessful() {
+ let messages: [DraftMessage] = [.mock(), .mock()]
+ let nextCursor = "next_page"
+
+ let expectation = expectation(description: "loadDraftMessages completion called")
+ controller.loadDraftMessages { result in
+ XCTAssertEqual(try? result.get(), messages)
+ expectation.fulfill()
+ }
+
+ draftsRepository.loadDrafts_completion?(.success(.init(drafts: messages, next: nextCursor)))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.loadDrafts_callCount, 1)
+ XCTAssertFalse(controller.hasLoadedAllDrafts)
+ }
+
+ func test_loadDraftMessages_whenNoNextCursor_marksAsLoadedAll() {
+ let messages: [DraftMessage] = [.mock(), .mock()]
+
+ let expectation = expectation(description: "loadDraftMessages completion called")
+ controller.loadDraftMessages { result in
+ XCTAssertEqual(try? result.get(), messages)
+ expectation.fulfill()
+ }
+
+ draftsRepository.loadDrafts_completion?(.success(.init(drafts: messages, next: nil)))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.loadDrafts_callCount, 1)
+ XCTAssertTrue(controller.hasLoadedAllDrafts)
+ }
+
+ func test_loadDraftMessages_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "loadDraftMessages completion called")
+ controller.loadDraftMessages { result in
+ XCTAssertEqual(error, result.error as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.loadDrafts_completion?(.failure(error))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.loadDrafts_callCount, 1)
+ }
+
+ // MARK: - Load More Draft Messages Tests
+
+ func test_loadMoreDraftMessages_whenNoNextCursor_returnsEmpty() {
+ let expectation = expectation(description: "loadMoreDraftMessages completion called")
+ controller.loadMoreDraftMessages { result in
+ XCTAssertEqual(try? result.get(), [])
+ expectation.fulfill()
+ }
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.loadDrafts_callCount, 0)
+ }
+
+ func test_loadMoreDraftMessages_whenSuccessful() {
+ // First load initial page
+ let initialMessages: [DraftMessage] = [.mock(), .mock()]
+ let nextCursor = "next_page"
+ controller.loadDraftMessages { _ in }
+ draftsRepository.loadDrafts_completion?(.success(.init(drafts: initialMessages, next: nextCursor)))
+
+ // Then load more
+ let moreMessages: [DraftMessage] = [.mock(), .mock()]
+ let expectation = expectation(description: "loadMoreDraftMessages completion called")
+ controller.loadMoreDraftMessages { result in
+ XCTAssertEqual(try? result.get(), moreMessages)
+ expectation.fulfill()
+ }
+
+ draftsRepository.loadDrafts_completion?(.success(.init(drafts: moreMessages, next: nil)))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.loadDrafts_callCount, 2)
+ XCTAssertTrue(controller.hasLoadedAllDrafts)
+ }
+
+ func test_loadMoreDraftMessages_whenFailure() {
+ // First load initial page
+ let initialMessages: [DraftMessage] = [.mock(), .mock()]
+ let nextCursor = "next_page"
+ controller.loadDraftMessages { _ in }
+ draftsRepository.loadDrafts_completion?(.success(.init(drafts: initialMessages, next: nextCursor)))
+
+ // Then try to load more
+ let error = TestError()
+ let expectation = expectation(description: "loadMoreDraftMessages completion called")
+ controller.loadMoreDraftMessages { result in
+ XCTAssertEqual(error, result.error as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.loadDrafts_completion?(.failure(error))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.loadDrafts_callCount, 2)
+ }
+
+ // MARK: - Delete Draft Message Tests
+
+ func test_deleteDraftMessage_whenSuccessful() {
+ let cid = ChannelId.unique
+ let threadId = MessageId.unique
+
+ let expectation = expectation(description: "deleteDraftMessage completion called")
+ controller.deleteDraftMessage(for: cid, threadId: threadId) { error in
+ XCTAssertNil(error)
+ expectation.fulfill()
+ }
+
+ draftsRepository.deleteDraft_completion?(nil)
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.deleteDraft_callCount, 1)
+ let calledWith = draftsRepository.deleteDraft_calledWith
+ XCTAssertEqual(calledWith?.cid, cid)
+ XCTAssertEqual(calledWith?.threadId, threadId)
+ }
+
+ func test_deleteDraftMessage_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "deleteDraftMessage completion called")
+ controller.deleteDraftMessage(for: .unique) { receivedError in
+ XCTAssertEqual(error, receivedError as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.deleteDraft_completion?(error)
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.deleteDraft_callCount, 1)
+ }
+
+ // MARK: - Delegate Tests
+
+ func test_draftMessagesObserver_notifiesDelegate() throws {
+ class DelegateMock: CurrentChatUserControllerDelegate {
+ var messages: [DraftMessage] = []
+ let expectation = XCTestExpectation(description: "Did Change Draft Messages")
+ let expectedMessagesCount: Int
+
+ init(expectedMessagesCount: Int) {
+ self.expectedMessagesCount = expectedMessagesCount
+ }
+
+ func currentUserController(
+ _ controller: CurrentChatUserController,
+ didChangeDraftMessages messages: [DraftMessage]
+ ) {
+ self.messages = messages
+ guard expectedMessagesCount == messages.count else { return }
+ expectation.fulfill()
+ }
+ }
+
+ let delegate = DelegateMock(expectedMessagesCount: 2)
+ controller.loadDraftMessages()
+ controller.delegate = delegate
+
+ try client.databaseContainer.writeSynchronously { session in
+ let date = Date.unique
+ let cid = ChannelId.unique
+ let parentId = MessageId.unique
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+ try session.saveMessage(payload: .dummy(messageId: parentId), for: cid, syncOwnReactions: false, cache: nil)
+
+ // Test a draft in a channel and thread in the same channel
+ let messages = [
+ DraftPayload.dummy(cid: cid, createdAt: date, message: .dummy(text: "1")),
+ DraftPayload.dummy(cid: cid, createdAt: date.addingTimeInterval(1), message: .dummy(text: "2"), parentId: parentId)
+ ]
+
+ try messages.forEach {
+ try session.saveDraftMessage(payload: $0, for: cid, cache: nil)
+ }
+ }
+
+ wait(for: [delegate.expectation], timeout: defaultTimeout)
+ XCTAssertEqual(controller.draftMessages.count, 2)
+ XCTAssertEqual(delegate.messages.map(\.text), ["2", "1"])
+ }
+}
diff --git a/Tests/StreamChatTests/Controllers/MessageController/MessageController+Drafts_Tests.swift b/Tests/StreamChatTests/Controllers/MessageController/MessageController+Drafts_Tests.swift
new file mode 100644
index 00000000000..1991ce8a0c9
--- /dev/null
+++ b/Tests/StreamChatTests/Controllers/MessageController/MessageController+Drafts_Tests.swift
@@ -0,0 +1,153 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class MessageController_Drafts_Tests: XCTestCase {
+ var client: ChatClient_Mock!
+ var controller: ChatMessageController!
+ var draftsRepository: DraftMessagesRepository_Mock!
+
+ override func setUp() {
+ super.setUp()
+
+ client = ChatClient.mock
+ draftsRepository = client.draftMessagesRepository as? DraftMessagesRepository_Mock
+
+ let cid = ChannelId.unique
+ let messageId = MessageId.unique
+ controller = ChatMessageController(
+ client: client,
+ cid: cid,
+ messageId: messageId,
+ replyPaginationHandler: MessagesPaginationStateHandler_Mock()
+ )
+ }
+
+ override func tearDown() {
+ client.cleanUp()
+ draftsRepository = nil
+ controller = nil
+ client = nil
+
+ super.tearDown()
+ }
+
+ // MARK: - Update Draft Reply Tests
+
+ func test_updateDraftReply_whenSuccessful() {
+ let text = "Draft reply"
+ let message = DraftMessage.mock(text: text)
+
+ let expectation = expectation(description: "updateDraft completion called")
+ controller.updateDraftReply(
+ text: text,
+ isSilent: false,
+ attachments: [],
+ mentionedUserIds: [],
+ quotedMessageId: nil,
+ showReplyInChannel: false,
+ extraData: [:]
+ ) { result in
+ XCTAssertEqual(try? result.get(), message)
+ expectation.fulfill()
+ }
+
+ draftsRepository.updateDraft_completion?(.success(message))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.updateDraft_callCount, 1)
+
+ let calledWith = draftsRepository.updateDraft_calledWith
+ XCTAssertEqual(calledWith?.cid, controller.cid)
+ XCTAssertEqual(calledWith?.threadId, controller.messageId)
+ }
+
+ func test_updateDraftReply_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "updateDraft completion called")
+ controller.updateDraftReply(text: "test") { result in
+ XCTAssertEqual(error, result.error as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.updateDraft_completion?(.failure(error))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.updateDraft_callCount, 1)
+ }
+
+ // MARK: - Load Draft Reply Tests
+
+ func test_loadDraftReply_whenSuccessful() {
+ let message = DraftMessage.mock()
+
+ let expectation = expectation(description: "loadDraft completion called")
+ controller.loadDraftReply { result in
+ XCTAssertEqual(try? result.get(), message)
+ expectation.fulfill()
+ }
+
+ draftsRepository.getDraft_completion?(.success(message))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.getDraft_callCount, 1)
+
+ let calledWith = draftsRepository.getDraft_calledWith
+ XCTAssertEqual(calledWith?.cid, controller.cid)
+ XCTAssertEqual(calledWith?.threadId, controller.messageId)
+ }
+
+ func test_loadDraftReply_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "loadDraft completion called")
+ controller.loadDraftReply { result in
+ XCTAssertEqual(error, result.error as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.getDraft_completion?(.failure(error))
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.getDraft_callCount, 1)
+ }
+
+ // MARK: - Delete Draft Reply Tests
+
+ func test_deleteDraftReply_whenSuccessful() {
+ let expectation = expectation(description: "deleteDraft completion called")
+ controller.deleteDraftReply { error in
+ XCTAssertNil(error)
+ expectation.fulfill()
+ }
+
+ draftsRepository.deleteDraft_completion?(nil)
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.deleteDraft_callCount, 1)
+
+ let calledWith = draftsRepository.deleteDraft_calledWith
+ XCTAssertEqual(calledWith?.cid, controller.cid)
+ XCTAssertEqual(calledWith?.threadId, controller.messageId)
+ }
+
+ func test_deleteDraftReply_whenFailure() {
+ let error = TestError()
+
+ let expectation = expectation(description: "deleteDraft completion called")
+ controller.deleteDraftReply { receivedError in
+ XCTAssertEqual(error, receivedError as? TestError)
+ expectation.fulfill()
+ }
+
+ draftsRepository.deleteDraft_completion?(error)
+
+ waitForExpectations(timeout: defaultTimeout)
+ XCTAssertEqual(draftsRepository.deleteDraft_callCount, 1)
+ }
+}
diff --git a/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift b/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift
index 257a865c371..6156818365c 100644
--- a/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift
+++ b/Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift
@@ -988,6 +988,46 @@ final class MessageController_Tests: XCTestCase {
XCTAssertEqual(env.messageUpdater.editMessage_extraData, extraData)
}
+ func test_editMessage_whenMessageTransformerIsProvided_callsUpdaterWithTransformedValues() throws {
+ class MockTransformer: StreamModelsTransformer {
+ var mockTransformedMessage = NewMessageTransformableInfo(
+ text: "transformed",
+ attachments: [.mockFile],
+ extraData: ["transformed": true]
+ )
+ func transform(newMessageInfo: NewMessageTransformableInfo) -> NewMessageTransformableInfo {
+ mockTransformedMessage
+ }
+ }
+
+ let transformer = MockTransformer()
+ var config = ChatClientConfig(apiKeyString: .unique)
+ config.modelsTransformer = transformer
+ client = .mock(config: config)
+ controller = ChatMessageController(
+ client: client,
+ cid: cid,
+ messageId: messageId,
+ replyPaginationHandler: replyPaginationHandler,
+ environment: env.controllerEnvironment
+ )
+
+ let exp = expectation(description: "should complete edit message")
+
+ controller.editMessage(
+ text: .unique
+ ) { _ in
+ exp.fulfill()
+ }
+
+ env.messageUpdater.editMessage_completion?(.success(.unique))
+ wait(for: [exp], timeout: defaultTimeout)
+
+ XCTAssertEqual(env.messageUpdater.editMessage_text, transformer.mockTransformedMessage.text)
+ XCTAssertEqual(env.messageUpdater.editMessage_attachments, transformer.mockTransformedMessage.attachments)
+ XCTAssertEqual(env.messageUpdater.editMessage_extraData, transformer.mockTransformedMessage.extraData)
+ }
+
// MARK: - Flag message
func test_flag_propagatesError() {
diff --git a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
index 08a23642965..4e42cdd9255 100644
--- a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
+++ b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
@@ -1105,7 +1105,8 @@ final class ChannelDTO_Tests: XCTestCase {
pendingMessages: nil,
pinnedMessages: [],
channelReads: [currentUserChannelReadPayload],
- isHidden: false
+ isHidden: false,
+ draft: nil
)
let unreadMessages = 5
@@ -1161,7 +1162,8 @@ final class ChannelDTO_Tests: XCTestCase {
pendingMessages: nil,
pinnedMessages: [],
channelReads: [],
- isHidden: nil
+ isHidden: nil,
+ draft: nil
)
try database.writeSynchronously { session in
try session.saveChannel(payload: channelPayload)
@@ -1494,6 +1496,240 @@ final class ChannelDTO_Tests: XCTestCase {
// THEN
XCTAssertEqual(channel.cid, transformer.mockTransformedChannel.cid)
}
+
+ func test_saveChannel_savesAndLoadsDraftMessage() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let draftMessagePayload = DraftMessagePayload(
+ id: .unique,
+ text: "Draft message text",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: ["custom_key": .string("custom_value")],
+ attachments: nil,
+ isSilent: false
+ )
+
+ let channelPayload = ChannelPayload(
+ channel: .dummy(cid: cid),
+ watcherCount: nil,
+ watchers: nil,
+ members: [],
+ membership: nil,
+ messages: [],
+ pendingMessages: nil,
+ pinnedMessages: [],
+ channelReads: [],
+ isHidden: nil,
+ draft: DraftPayload(
+ cid: cid,
+ channelPayload: nil,
+ createdAt: .init(),
+ message: draftMessagePayload,
+ quotedMessage: nil,
+ parentId: nil,
+ parentMessage: nil
+ )
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: channelPayload)
+ }
+
+ // THEN
+ let channel = try XCTUnwrap(database.viewContext.channel(cid: cid)?.asModel())
+ let draftMessage = try XCTUnwrap(channel.draftMessage)
+ XCTAssertEqual(draftMessage.id, draftMessagePayload.id)
+ XCTAssertEqual(draftMessage.text, draftMessagePayload.text)
+ XCTAssertEqual(draftMessage.extraData, draftMessagePayload.extraData)
+ }
+
+ func test_saveChannel_whenDraftMessageIsNil_removesExistingDraft() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let draftMessagePayload = DraftMessagePayload(
+ id: .unique,
+ text: "Draft message text",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ )
+
+ let channelPayload = ChannelPayload(
+ channel: .dummy(cid: cid),
+ watcherCount: nil,
+ watchers: nil,
+ members: [],
+ membership: nil,
+ messages: [],
+ pendingMessages: nil,
+ pinnedMessages: [],
+ channelReads: [],
+ isHidden: nil,
+ draft: DraftPayload(
+ cid: cid,
+ channelPayload: nil,
+ createdAt: .init(),
+ message: draftMessagePayload,
+ quotedMessage: nil,
+ parentId: nil,
+ parentMessage: nil
+ )
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: channelPayload)
+ }
+
+ // Verify draft exists
+ var channel = try XCTUnwrap(database.viewContext.channel(cid: cid)?.asModel())
+ XCTAssertNotNil(channel.draftMessage)
+
+ // WHEN
+ // Save channel without draft
+ let payloadWithoutDraft = ChannelPayload(
+ channel: .dummy(cid: cid),
+ watcherCount: nil,
+ watchers: nil,
+ members: [],
+ membership: nil,
+ messages: [],
+ pendingMessages: nil,
+ pinnedMessages: [],
+ channelReads: [],
+ isHidden: nil,
+ draft: nil
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveChannel(payload: payloadWithoutDraft)
+ }
+
+ // THEN
+ channel = try XCTUnwrap(database.viewContext.channel(cid: cid)?.asModel())
+ XCTAssertNil(channel.draftMessage)
+ }
+
+ func test_saveChannel_draftMessageWithQuotedMessage() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let quotedMessagePayload: MessagePayload = .dummy(
+ messageId: .unique,
+ authorUserId: .unique,
+ text: "Quoted message"
+ )
+ let draftMessagePayload = DraftMessagePayload(
+ id: .unique,
+ text: "Draft message text",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ )
+
+ let channelPayload = ChannelPayload(
+ channel: .dummy(cid: cid),
+ watcherCount: nil,
+ watchers: nil,
+ members: [],
+ membership: nil,
+ messages: [],
+ pendingMessages: nil,
+ pinnedMessages: [],
+ channelReads: [],
+ isHidden: nil,
+ draft: DraftPayload(
+ cid: cid,
+ channelPayload: nil,
+ createdAt: .init(),
+ message: draftMessagePayload,
+ quotedMessage: quotedMessagePayload,
+ parentId: nil,
+ parentMessage: nil
+ )
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: channelPayload)
+ }
+
+ // THEN
+ let channel = try XCTUnwrap(database.viewContext.channel(cid: cid)?.asModel())
+ let draftMessage = try XCTUnwrap(channel.draftMessage)
+ let quotedMessage = try XCTUnwrap(draftMessage.quotedMessage)
+ XCTAssertEqual(quotedMessage.id, quotedMessagePayload.id)
+ XCTAssertEqual(quotedMessage.text, quotedMessagePayload.text)
+ }
+
+ func test_saveChannel_draftMessageWithParentMessage() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let parentMessagePayload: MessagePayload = .dummy(
+ messageId: .unique,
+ authorUserId: .unique,
+ text: "Parent message"
+ )
+ let draftMessagePayload = DraftMessagePayload(
+ id: .unique,
+ text: "Draft message text",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ )
+
+ let channelPayload = ChannelPayload(
+ channel: .dummy(cid: cid),
+ watcherCount: nil,
+ watchers: nil,
+ members: [],
+ membership: nil,
+ messages: [],
+ pendingMessages: nil,
+ pinnedMessages: [],
+ channelReads: [],
+ isHidden: nil,
+ draft: DraftPayload(
+ cid: cid,
+ channelPayload: nil,
+ createdAt: .init(),
+ message: draftMessagePayload,
+ quotedMessage: nil,
+ parentId: parentMessagePayload.id,
+ parentMessage: parentMessagePayload
+ )
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: channelPayload)
+ }
+
+ // THEN
+ let channel = try XCTUnwrap(database.viewContext.channel(cid: cid)?.asModel())
+ let draftMessage = try XCTUnwrap(channel.draftMessage)
+ XCTAssertEqual(draftMessage.threadId, parentMessagePayload.id)
+ let parentMessageId = try XCTUnwrap(draftMessage.threadId)
+ XCTAssertEqual(parentMessageId, parentMessagePayload.id)
+ }
}
private extension ChannelDTO_Tests {
diff --git a/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift
index a795cf6311f..9ff21331acc 100644
--- a/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift
+++ b/Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift
@@ -1201,6 +1201,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: messageExtraData
)
@@ -1290,6 +1291,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
}
@@ -1394,6 +1396,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
message1Id = message1DTO.id
@@ -1418,6 +1421,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
// Reset the `locallyCreateAt` value of the second message to simulate the message was sent
@@ -1561,6 +1565,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: true,
skipEnrichUrl: true,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
newMessageId = messageDTO.id
@@ -1632,6 +1637,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: true,
skipEnrichUrl: true,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
newMessageId = messageDTO.id
@@ -1682,6 +1688,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
messageId = messageDTO.id
@@ -1729,6 +1736,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
threadReplyId = replyShownInChannelDTO.id
@@ -1789,6 +1797,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
}
@@ -1818,6 +1827,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
}, completion: completion)
@@ -1861,6 +1871,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
}, completion: completion)
@@ -1950,6 +1961,7 @@ final class MessageDTO_Tests: XCTestCase {
skipPush: false,
skipEnrichUrl: false,
poll: nil,
+ restrictedVisibility: [],
extraData: [:]
)
// Get reply messageId
@@ -2514,6 +2526,88 @@ final class MessageDTO_Tests: XCTestCase {
XCTAssertEqual(predicateCount, 1)
}
+ func test_channelMessagesPredicate_shouldNotIncludeDraftMessages() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+
+ // Create a regular message
+ let regularMessage = MessagePayload.dummy(
+ messageId: .unique,
+ text: "Regular message"
+ )
+
+ // Create a draft message
+ let draftPayload = DraftPayload.dummy(cid: cid, message: .dummy(text: "Draft message"))
+ let threadDraftPayload = DraftPayload.dummy(cid: cid, message: .dummy(text: "Thread draft message"), parentId: regularMessage.id)
+
+ // Save regular, draft message in the channel and draft message in the thread
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ let channel = try session.saveChannel(payload: channelPayload)
+ try session.saveMessage(payload: regularMessage, channelDTO: channel, syncOwnReactions: false, cache: nil)
+ try session.saveDraftMessage(payload: draftPayload, for: cid, cache: nil)
+ try session.saveDraftMessage(payload: threadDraftPayload, for: cid, cache: nil)
+ }
+
+ // Create an observer with channelMessagesPredicate
+ var receivedMessages: [ChatMessage] = []
+ let observer = try createMessagesFRC(for: channelPayload) { _ in }
+ receivedMessages = Array(observer.items)
+
+ // Only the regular message should be included in the results
+ XCTAssertEqual(receivedMessages.count, 1)
+ XCTAssertEqual(receivedMessages.first?.id, regularMessage.id)
+ XCTAssertEqual(receivedMessages.first?.text, regularMessage.text)
+ XCTAssertEqual(database.viewContext.channel(cid: cid)?.draftMessage?.id, draftPayload.message.id)
+ }
+
+ func test_threadMessagesPredicate_shouldNotIncludeDraftMessages() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+ let parentMessagePayload = MessagePayload.dummy(cid: cid)
+
+ // Create a regular message
+ let regularMessage = MessagePayload.dummy(
+ type: .regular,
+ messageId: .unique,
+ parentId: parentMessagePayload.id,
+ text: "Regular message",
+ cid: cid
+ )
+
+ // Create a draft message
+ let threadDraftPayload = DraftPayload.dummy(
+ cid: cid, message: .dummy(text: "Thread draft message"), parentId: regularMessage.id
+ )
+
+ // Save regular, draft message in the channel and draft message in the thread
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ let channel = try session.saveChannel(payload: channelPayload)
+ try session.saveMessage(payload: parentMessagePayload, channelDTO: channel, syncOwnReactions: false, cache: nil)
+ let reply = try session.saveMessage(payload: regularMessage, for: cid, syncOwnReactions: false, cache: nil)
+ reply.showInsideThread = true
+ try session.saveDraftMessage(payload: threadDraftPayload, for: cid, cache: nil)
+ }
+
+ // Create an observer with channelMessagesPredicate
+ var receivedMessages: [ChatMessage] = []
+ let observer = try createThreadMessagesFRC(for: parentMessagePayload) { _ in }
+ receivedMessages = Array(observer.items)
+
+ // Only the regular message should be included in the results
+ XCTAssertEqual(receivedMessages.count, 1)
+ XCTAssertEqual(receivedMessages.first?.id, regularMessage.id)
+ XCTAssertEqual(receivedMessages.first?.text, regularMessage.text)
+ XCTAssertEqual(database.viewContext.message(id: regularMessage.id)?.draftReply?.id, threadDraftPayload.message.id)
+ }
+
// MARK: - allAttachmentsAreUploadedOrEmptyPredicate()
func test_allAttachmentsAreUploadedOrEmptyPredicate_whenEmpty_returnsMessages() throws {
@@ -4353,4 +4447,333 @@ final class MessageDTO_Tests: XCTestCase {
try observer.startObserving(onContextDidChange: { _, changes in onChange(changes) })
return observer
}
+
+ // Creates a messages observer (FRC wrapper)
+ private func createThreadMessagesFRC(
+ for payload: MessagePayload,
+ onChange: @escaping ([ListChange]) -> Void
+ ) throws -> StateLayerDatabaseObserver {
+ let observer = StateLayerDatabaseObserver(
+ database: database,
+ fetchRequest: MessageDTO.repliesFetchRequest(
+ for: payload.id,
+ pageSize: 25,
+ deletedMessagesVisibility: .alwaysHidden,
+ shouldShowShadowedMessages: false
+ ),
+ itemCreator: { try $0.asModel() as ChatMessage },
+ itemReuseKeyPaths: (\ChatMessage.id, \MessageDTO.id),
+ sorting: []
+ )
+ try observer.startObserving(onContextDidChange: { _, changes in onChange(changes) })
+ return observer
+ }
+
+ // MARK: - Draft Messages Tests
+
+ func test_saveDraftMessage_inChannel() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+ let draftPayload = DraftPayload(
+ cid: cid,
+ channelPayload: channelDetailPayload,
+ createdAt: .init(),
+ message: .init(
+ id: .unique,
+ text: "Draft message",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ ),
+ quotedMessage: nil,
+ parentId: nil,
+ parentMessage: nil
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ try session.saveChannel(payload: channelPayload)
+ try session.saveDraftMessage(payload: draftPayload, for: cid, cache: nil)
+ }
+
+ // THEN
+ let channelDTO = try XCTUnwrap(database.viewContext.channel(cid: cid))
+ let draftMessage = try XCTUnwrap(channelDTO.draftMessage)
+ XCTAssertEqual(draftMessage.text, "Draft message")
+ XCTAssertEqual(draftMessage.type, MessageType.regular.rawValue)
+ XCTAssertTrue(draftMessage.isDraft)
+ XCTAssertNil(draftMessage.parentMessageId)
+ }
+
+ func test_saveDraftMessage_inThread() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+ let parentMessageId = MessageId.unique
+ let parentMessage = MessagePayload.dummy(messageId: parentMessageId)
+
+ let draftPayload = DraftPayload(
+ cid: cid,
+ channelPayload: channelDetailPayload,
+ createdAt: .init(),
+ message: .init(
+ id: .unique,
+ text: "Draft reply",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ ),
+ quotedMessage: nil,
+ parentId: parentMessageId,
+ parentMessage: parentMessage
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ let channel = try session.saveChannel(payload: channelPayload)
+ _ = try session.saveMessage(payload: parentMessage, channelDTO: channel, syncOwnReactions: false, cache: nil)
+ try session.saveDraftMessage(payload: draftPayload, for: cid, cache: nil)
+ }
+
+ // THEN
+ let parentMessageDTO = try XCTUnwrap(database.viewContext.message(id: parentMessageId))
+ let draftReply = try XCTUnwrap(parentMessageDTO.draftReply)
+ XCTAssertEqual(draftReply.text, "Draft reply")
+ XCTAssertEqual(draftReply.type, MessageType.regular.rawValue)
+ XCTAssertTrue(draftReply.isDraft)
+ XCTAssertEqual(draftReply.parentMessageId, parentMessageId)
+ }
+
+ func test_saveDraftMessage_whenParentOrQuotedMessageContainsDraft_shouldNotCrash() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+ let draftMessage = DraftMessagePayload.dummy(text: "Draft Reply")
+ let parentMessageId = MessageId.unique
+ let parentMessage = MessagePayload.dummy(
+ messageId: parentMessageId,
+ draft: .dummy(cid: cid, message: draftMessage)
+ )
+ let draftPayload = DraftPayload.dummy(
+ cid: cid,
+ channelPayload: channelDetailPayload,
+ message: draftMessage,
+ quotedMessage: .dummy(cid: cid, draft: .dummy(cid: cid, message: draftMessage)),
+ parentId: parentMessageId,
+ parentMessage: parentMessage
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ let channel = try session.saveChannel(payload: channelPayload)
+ _ = try session.saveMessage(payload: parentMessage, channelDTO: channel, syncOwnReactions: false, cache: nil)
+ try session.saveDraftMessage(payload: draftPayload, for: cid, cache: nil)
+ }
+
+ // THEN
+ let parentMessageDTO = try XCTUnwrap(database.viewContext.message(id: parentMessageId))
+ let draftReply = try XCTUnwrap(parentMessageDTO.draftReply)
+ XCTAssertEqual(draftReply.text, "Draft Reply")
+ XCTAssertEqual(draftReply.type, MessageType.regular.rawValue)
+ XCTAssertTrue(draftReply.isDraft)
+ XCTAssertEqual(draftReply.parentMessageId, parentMessageId)
+ }
+
+ func test_deleteDraftMessage_fromChannel() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+ let draftPayload = DraftPayload(
+ cid: cid,
+ channelPayload: channelDetailPayload,
+ createdAt: .init(),
+ message: .init(
+ id: .unique,
+ text: "Draft message",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ ),
+ quotedMessage: nil,
+ parentId: nil,
+ parentMessage: nil
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ try session.saveChannel(payload: channelPayload)
+ try session.saveDraftMessage(payload: draftPayload, for: cid, cache: nil)
+ }
+
+ // WHEN
+ try database.writeSynchronously { session in
+ session.deleteDraftMessage(in: cid, threadId: nil)
+ }
+
+ // THEN
+ let channelDTO = try XCTUnwrap(database.viewContext.channel(cid: cid))
+ XCTAssertNil(channelDTO.draftMessage)
+ }
+
+ func test_deleteDraftMessage_fromThread() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let currentUser: CurrentUserPayload = .dummy(userId: .unique, role: .user)
+ let channelDetailPayload = ChannelDetailPayload.dummy(cid: cid)
+ let channelPayload: ChannelPayload = .dummy(channel: channelDetailPayload)
+ let parentMessageId = MessageId.unique
+ let parentMessage = MessagePayload.dummy(messageId: parentMessageId)
+
+ let draftPayload = DraftPayload(
+ cid: cid,
+ channelPayload: channelDetailPayload,
+ createdAt: .init(),
+ message: .init(
+ id: .unique,
+ text: "Draft reply",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ ),
+ quotedMessage: nil,
+ parentId: parentMessageId,
+ parentMessage: parentMessage
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: currentUser)
+ let channel = try session.saveChannel(payload: channelPayload)
+ _ = try session.saveMessage(payload: parentMessage, channelDTO: channel, syncOwnReactions: false, cache: nil)
+ try session.saveDraftMessage(payload: draftPayload, for: cid, cache: nil)
+ }
+
+ // WHEN
+ try database.writeSynchronously { session in
+ session.deleteDraftMessage(in: cid, threadId: parentMessageId)
+ }
+
+ // THEN
+ let parentMessageDTO = try XCTUnwrap(database.viewContext.message(id: parentMessageId))
+ XCTAssertNil(parentMessageDTO.draftReply)
+ }
+
+ func test_saveMessage_savesAndLoadsDraftReply() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let draftMessagePayload = DraftMessagePayload.dummy(
+ text: "Draft message text"
+ )
+ let messagePayload = MessagePayload.dummy(
+ draft: .dummy(
+ cid: cid,
+ message: draftMessagePayload
+ )
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+ try session.saveMessage(payload: messagePayload, for: cid, syncOwnReactions: false, cache: nil)
+ }
+
+ // THEN
+ let message = try XCTUnwrap(database.viewContext.message(id: messagePayload.id)?.asModel())
+ let draftReply = try XCTUnwrap(message.draftReply)
+ XCTAssertEqual(draftReply.id, draftMessagePayload.id)
+ XCTAssertEqual(draftReply.text, draftMessagePayload.text)
+ XCTAssertEqual(draftReply.extraData, draftMessagePayload.extraData)
+ }
+
+ func test_saveMessage_whenDraftReplyIsNil_removesExistingDraft() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let draftMessagePayload = DraftMessagePayload.dummy(
+ text: "Draft message text"
+ )
+ let messagePayload = MessagePayload.dummy(
+ draft: .dummy(
+ cid: cid,
+ message: draftMessagePayload
+ )
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+ try session.saveMessage(payload: messagePayload, for: cid, syncOwnReactions: false, cache: nil)
+ }
+
+ // Verify draft exists
+ var message = try XCTUnwrap(database.viewContext.message(id: messagePayload.id)?.asModel())
+ XCTAssertNotNil(message.draftReply)
+
+ // WHEN
+ // Save channel without draft
+ let payloadWithoutDraft = MessagePayload.dummy(
+ messageId: messagePayload.id,
+ draft: nil
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveMessage(payload: payloadWithoutDraft, for: cid, syncOwnReactions: false, cache: nil)
+ }
+
+ // THEN
+ message = try XCTUnwrap(database.viewContext.message(id: messagePayload.id)?.asModel())
+ XCTAssertNil(message.draftReply)
+ }
+
+ func test_saveMessage_whenSkipsDraftUpdate_shouldNotSaveDraft() throws {
+ // GIVEN
+ let cid: ChannelId = .unique
+ let draftMessagePayload = DraftMessagePayload.dummy(
+ text: "Draft message text"
+ )
+ let messagePayload = MessagePayload.dummy(
+ draft: .dummy(
+ cid: cid,
+ message: draftMessagePayload
+ )
+ )
+
+ // WHEN
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+ try session.saveMessage(payload: messagePayload, for: cid, syncOwnReactions: false, skipDraftUpdate: true, cache: nil)
+ }
+
+ // THEN
+ let message = try XCTUnwrap(database.viewContext.message(id: messagePayload.id)?.asModel())
+ XCTAssertNil(message.draftReply)
+ }
}
diff --git a/Tests/StreamChatTests/Database/DTOs/ThreadDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ThreadDTO_Tests.swift
index 321c69f532c..007aee885fd 100644
--- a/Tests/StreamChatTests/Database/DTOs/ThreadDTO_Tests.swift
+++ b/Tests/StreamChatTests/Database/DTOs/ThreadDTO_Tests.swift
@@ -53,6 +53,7 @@ final class ThreadDTO_Tests: XCTestCase {
title: "Test",
latestReplies: [.dummy(), .dummy()],
read: [dummyThreadReadPayload()],
+ draft: nil,
extraData: [:]
)
@@ -91,6 +92,7 @@ final class ThreadDTO_Tests: XCTestCase {
title: "Test",
latestReplies: [.dummy(), .dummy()],
read: [dummyThreadReadPayload()],
+ draft: nil,
extraData: [:]
)
@@ -139,4 +141,148 @@ final class ThreadDTO_Tests: XCTestCase {
["1", "2", "3"]
)
}
+
+ func test_saveThreadPayload_withDraftReply() throws {
+ // GIVEN
+ let draftMessagePayload = DraftMessagePayload(
+ id: .unique,
+ text: "Draft reply text",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ )
+
+ let draftPayload = DraftPayload(
+ cid: .unique,
+ channelPayload: nil,
+ createdAt: .init(),
+ message: draftMessagePayload,
+ quotedMessage: nil,
+ parentId: nil,
+ parentMessage: nil
+ )
+
+ let payload = ThreadPayload(
+ parentMessageId: .unique,
+ parentMessage: .dummy(),
+ channel: .dummy(),
+ createdBy: .dummy(userId: .unique),
+ replyCount: 10,
+ participantCount: 10,
+ threadParticipants: [dummyThreadParticipantPayload()],
+ lastMessageAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique,
+ title: "Test",
+ latestReplies: [.dummy(), .dummy()],
+ read: [dummyThreadReadPayload()],
+ draft: draftPayload,
+ extraData: [:]
+ )
+
+ _ = try database.viewContext.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+
+ // WHEN
+ let dto = try database.viewContext.saveThread(
+ payload: payload,
+ cache: nil
+ )
+
+ // THEN
+ XCTAssertEqual(dto.parentMessage.draftReply?.text, "Draft reply text")
+ XCTAssertTrue(dto.parentMessage.draftReply?.isDraft ?? false)
+ XCTAssertEqual(dto.parentMessage.draftReply?.type, MessageType.regular.rawValue)
+
+ // Verify the draft reply is included in the model
+ let model = try dto.asModel()
+ XCTAssertEqual(model.parentMessage.draftReply?.text, "Draft reply text")
+ }
+
+ func test_saveThreadPayload_whenDraftIsNil_removesExistingDraft() throws {
+ // GIVEN
+ // First save a thread with a draft
+ let draftMessagePayload = DraftMessagePayload(
+ id: .unique,
+ text: "Draft reply text",
+ command: nil,
+ args: nil,
+ showReplyInChannel: false,
+ mentionedUsers: nil,
+ extraData: [:],
+ attachments: nil,
+ isSilent: false
+ )
+
+ let draftPayload = DraftPayload(
+ cid: .unique,
+ channelPayload: nil,
+ createdAt: .init(),
+ message: draftMessagePayload,
+ quotedMessage: nil,
+ parentId: nil,
+ parentMessage: nil
+ )
+
+ let payloadWithDraft = ThreadPayload(
+ parentMessageId: .unique,
+ parentMessage: .dummy(),
+ channel: .dummy(),
+ createdBy: .dummy(userId: .unique),
+ replyCount: 10,
+ participantCount: 10,
+ threadParticipants: [dummyThreadParticipantPayload()],
+ lastMessageAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique,
+ title: "Test",
+ latestReplies: [.dummy(), .dummy()],
+ read: [dummyThreadReadPayload()],
+ draft: draftPayload,
+ extraData: [:]
+ )
+
+ _ = try database.viewContext.saveCurrentUser(payload: .dummy(userId: .unique, role: .admin))
+
+ let dto = try database.viewContext.saveThread(
+ payload: payloadWithDraft,
+ cache: nil
+ )
+
+ // Verify draft exists
+ XCTAssertNotNil(dto.parentMessage.draftReply)
+
+ // WHEN
+ // Save the same thread without a draft
+ let payloadWithoutDraft = ThreadPayload(
+ parentMessageId: payloadWithDraft.parentMessageId,
+ parentMessage: payloadWithDraft.parentMessage,
+ channel: payloadWithDraft.channel,
+ createdBy: payloadWithDraft.createdBy,
+ replyCount: payloadWithDraft.replyCount,
+ participantCount: payloadWithDraft.participantCount,
+ threadParticipants: payloadWithDraft.threadParticipants,
+ lastMessageAt: payloadWithDraft.lastMessageAt,
+ createdAt: payloadWithDraft.createdAt,
+ updatedAt: payloadWithDraft.updatedAt,
+ title: payloadWithDraft.title,
+ latestReplies: payloadWithDraft.latestReplies,
+ read: payloadWithDraft.read,
+ draft: nil,
+ extraData: payloadWithDraft.extraData
+ )
+
+ let updatedDto = try database.viewContext.saveThread(
+ payload: payloadWithoutDraft,
+ cache: nil
+ )
+
+ // THEN
+ XCTAssertNil(updatedDto.parentMessage.draftReply)
+ let model = try updatedDto.asModel()
+ XCTAssertNil(model.parentMessage.draftReply)
+ }
}
diff --git a/Tests/StreamChatTests/Database/FetchCache_Tests.swift b/Tests/StreamChatTests/Database/FetchCache_Tests.swift
index d35f5957e0d..7bff87de786 100644
--- a/Tests/StreamChatTests/Database/FetchCache_Tests.swift
+++ b/Tests/StreamChatTests/Database/FetchCache_Tests.swift
@@ -56,6 +56,27 @@ final class FetchCache_Tests: XCTestCase {
XCTAssertEqual(cache.cacheEntriesCount, 3)
}
+
+ func test_ignoringCacheIfContextHasInsertedOrDeletedObjectsOfThatType() async throws {
+ let database = DatabaseContainer(kind: .inMemory, chatClientConfig: .init(apiKeyString: .unique))
+ let cid = ChannelId.unique
+ try database.createCurrentUser(id: .unique, name: "")
+ try database.createChannel(cid: cid, withMessages: true)
+ try await database.write { session in
+ // FetchCache caches the response
+ guard let firstPreviewMessage = session.preview(for: cid) else { throw ClientError.Unknown("Preview message missing") }
+
+ // Insert a new message what is newer than the current preview message
+ let newMessagePayload = MessagePayload.dummy(createdAt: firstPreviewMessage.createdAt.bridgeDate.addingTimeInterval(1.0))
+ try session.saveMessage(payload: newMessagePayload, for: cid, syncOwnReactions: false, cache: nil)
+
+ guard let secondPreviewMessage = session.preview(for: cid) else { throw ClientError.Unknown("Preview message missing") }
+ XCTAssertEqual(newMessagePayload.id, secondPreviewMessage.id)
+ XCTAssertNotEqual(firstPreviewMessage.id, secondPreviewMessage.id)
+ }
+ }
+
+ // MARK: - Test Data
private var tenIds: [TestId] {
(1...10).map { _ in TestId() }
diff --git a/Tests/StreamChatTests/Query/DraftListQuery_Tests.swift b/Tests/StreamChatTests/Query/DraftListQuery_Tests.swift
new file mode 100644
index 00000000000..efaf3d7e350
--- /dev/null
+++ b/Tests/StreamChatTests/Query/DraftListQuery_Tests.swift
@@ -0,0 +1,65 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class DraftListQuery_Tests: XCTestCase {
+ func test_defaultInitialization() {
+ let query = DraftListQuery()
+
+ XCTAssertEqual(query.pagination.pageSize, 25)
+ XCTAssertEqual(query.pagination.offset, 0)
+ XCTAssertEqual(query.sorting.count, 1)
+ XCTAssertEqual(query.sorting[0].key, .createdAt)
+ XCTAssertFalse(query.sorting[0].isAscending)
+ }
+
+ func test_customInitialization() {
+ let pagination = Pagination(pageSize: 10, offset: 5)
+ let sorting = [Sorting(key: .createdAt, isAscending: true)]
+
+ let query = DraftListQuery(pagination: pagination, sorting: sorting)
+
+ XCTAssertEqual(query.pagination.pageSize, 10)
+ XCTAssertEqual(query.pagination.offset, 5)
+ XCTAssertEqual(query.sorting.count, 1)
+ XCTAssertEqual(query.sorting[0].key, .createdAt)
+ XCTAssertTrue(query.sorting[0].isAscending)
+ }
+
+ func test_encode() throws {
+ let query = DraftListQuery(
+ pagination: .init(pageSize: 20, offset: 10),
+ sorting: [.init(key: .createdAt, isAscending: true)]
+ )
+
+ let expectedData: [String: Any] = [
+ "offset": 10,
+ "limit": 20,
+ "sort": [["field": "created_at", "direction": 1]]
+ ]
+
+ let expectedJSON = try JSONSerialization.data(withJSONObject: expectedData, options: [])
+ let encodedJSON = try JSONEncoder.default.encode(query)
+ AssertJSONEqual(expectedJSON, encodedJSON)
+ }
+
+ func test_encode_withoutSorting() throws {
+ let query = DraftListQuery(
+ pagination: .init(pageSize: 20, offset: 10),
+ sorting: []
+ )
+
+ let expectedData: [String: Any] = [
+ "offset": 10,
+ "limit": 20
+ ]
+
+ let expectedJSON = try JSONSerialization.data(withJSONObject: expectedData, options: [])
+ let encodedJSON = try JSONEncoder.default.encode(query)
+ AssertJSONEqual(expectedJSON, encodedJSON)
+ }
+}
diff --git a/Tests/StreamChatTests/Repositories/DraftMessagesRepository_Tests.swift b/Tests/StreamChatTests/Repositories/DraftMessagesRepository_Tests.swift
new file mode 100644
index 00000000000..b3cd5f2e98a
--- /dev/null
+++ b/Tests/StreamChatTests/Repositories/DraftMessagesRepository_Tests.swift
@@ -0,0 +1,252 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class DraftMessagesRepository_Tests: XCTestCase {
+ var apiClient: APIClient_Spy!
+ var database: DatabaseContainer!
+ var repository: DraftMessagesRepository!
+
+ override func setUp() {
+ super.setUp()
+
+ apiClient = APIClient_Spy()
+ database = DatabaseContainer_Spy()
+ repository = DraftMessagesRepository(database: database, apiClient: apiClient)
+ }
+
+ override func tearDown() {
+ apiClient.cleanUp()
+
+ apiClient = nil
+ repository = nil
+ database = nil
+
+ super.tearDown()
+ }
+
+ // MARK: - Load Drafts Tests
+
+ func test_loadDrafts_whenSuccessful() throws {
+ let channelId = ChannelId.unique
+ try savePreExistingData(channelId: channelId, threadId: nil)
+
+ let query = DraftListQuery()
+ let draftPayload1 = DraftPayload.dummy(
+ cid: channelId,
+ channelPayload: .dummy(cid: channelId),
+ message: .dummy(text: "Draft 1")
+ )
+ let draftPayload2 = DraftPayload.dummy(
+ cid: channelId,
+ channelPayload: .dummy(cid: channelId),
+ message: .dummy(text: "Draft 2")
+ )
+
+ let payload = DraftListPayloadResponse(drafts: [draftPayload1, draftPayload2], next: "next_page")
+
+ let completionCalled = expectation(description: "completion called")
+ repository.loadDrafts(query: query) { result in
+ XCTAssertNil(result.error)
+ XCTAssertEqual(result.value?.drafts.count, 2)
+ XCTAssertEqual(result.value?.next, "next_page")
+ completionCalled.fulfill()
+ }
+
+ apiClient.test_simulateResponse(.success(payload))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+
+ let referenceEndpoint: Endpoint = .drafts(query: query)
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))
+ }
+
+ func test_loadDrafts_whenFailure() {
+ let query = DraftListQuery()
+ let completionCalled = expectation(description: "completion called")
+
+ repository.loadDrafts(query: query) { result in
+ XCTAssertNotNil(result.error)
+ completionCalled.fulfill()
+ }
+
+ let error = TestError()
+ apiClient.test_simulateResponse(Result.failure(error))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+ }
+
+ // MARK: - Update Draft Tests
+
+ func test_updateDraft_whenSuccessful() throws {
+ let channelId = ChannelId.unique
+ let text = "Draft message"
+ let threadId: MessageId = .unique
+ try savePreExistingData(channelId: channelId, threadId: threadId)
+
+ let draftPayload = DraftPayload.dummy(
+ cid: channelId,
+ channelPayload: .dummy(cid: channelId),
+ message: .dummy(
+ text: text,
+ showReplyInChannel: false,
+ isSilent: false
+ ),
+ parentId: threadId
+ )
+
+ let completionCalled = expectation(description: "completion called")
+ repository.updateDraft(
+ for: channelId,
+ threadId: threadId,
+ text: text,
+ isSilent: false,
+ showReplyInChannel: true,
+ command: nil,
+ arguments: nil,
+ attachments: [],
+ mentionedUserIds: ["leia"],
+ quotedMessageId: "quotedID",
+ extraData: [:]
+ ) { result in
+ XCTAssertNil(result.error)
+ completionCalled.fulfill()
+ }
+
+ wait(for: [apiClient.request_expectation], timeout: defaultTimeout)
+
+ apiClient.test_simulateResponse(.success(DraftPayloadResponse(draft: draftPayload)))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+
+ let requestBodyMessage = try XCTUnwrap(apiClient.request_endpoint?.bodyAsDictionary()["message"] as? [String: Any])
+ AssertDictionary(ignoringKeys: ["id"], requestBodyMessage, [
+ "mentioned_users": ["leia"],
+ "parent_id": threadId,
+ "show_in_channel": 1,
+ "silent": 0,
+ "text": text
+ ])
+ }
+
+ func test_updateDraft_whenFailure() {
+ let channelId = ChannelId.unique
+ let text = "Draft message"
+
+ let completionCalled = expectation(description: "completion called")
+ repository.updateDraft(
+ for: channelId,
+ threadId: nil,
+ text: text,
+ isSilent: false,
+ showReplyInChannel: false,
+ command: nil,
+ arguments: nil,
+ attachments: [],
+ mentionedUserIds: [],
+ quotedMessageId: nil,
+ extraData: [:]
+ ) { result in
+ XCTAssertNotNil(result.error)
+ completionCalled.fulfill()
+ }
+
+ let error = TestError()
+ apiClient.test_simulateResponse(Result.failure(error))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+ }
+
+ // MARK: - Get Draft Tests
+
+ func test_getDraft_whenSuccessful() throws {
+ let channelId = ChannelId.unique
+ let threadId: MessageId = .unique
+ try savePreExistingData(channelId: channelId, threadId: threadId)
+
+ let draftPayload = DraftPayload.dummy(
+ cid: channelId,
+ channelPayload: .dummy(cid: channelId),
+ parentId: threadId
+ )
+
+ let completionCalled = expectation(description: "completion called")
+ repository.getDraft(for: channelId, threadId: threadId) { result in
+ XCTAssertNil(result.error)
+ completionCalled.fulfill()
+ }
+
+ apiClient.test_simulateResponse(.success(DraftPayloadResponse(draft: draftPayload)))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+
+ let referenceEndpoint: Endpoint = .getDraftMessage(channelId: channelId, threadId: threadId)
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))
+ }
+
+ func test_getDraft_whenFailure() {
+ let channelId = ChannelId.unique
+ let threadId: MessageId? = nil
+
+ let completionCalled = expectation(description: "completion called")
+ repository.getDraft(for: channelId, threadId: threadId) { result in
+ XCTAssertNotNil(result.error)
+ completionCalled.fulfill()
+ }
+
+ let error = TestError()
+ apiClient.test_simulateResponse(Result.failure(error))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+ }
+
+ // MARK: - Delete Draft Tests
+
+ func test_deleteDraft_whenSuccessful() {
+ let channelId = ChannelId.unique
+ let threadId: MessageId? = .unique
+
+ let completionCalled = expectation(description: "completion called")
+ repository.deleteDraft(for: channelId, threadId: threadId) { error in
+ XCTAssertNil(error)
+ completionCalled.fulfill()
+ }
+
+ apiClient.test_simulateResponse(Result.success(.init()))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+
+ let referenceEndpoint: Endpoint = .deleteDraftMessage(channelId: channelId, threadId: threadId)
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))
+ }
+
+ func test_deleteDraft_whenFailure() {
+ let channelId = ChannelId.unique
+ let threadId: MessageId? = nil
+
+ let completionCalled = expectation(description: "completion called")
+ repository.deleteDraft(for: channelId, threadId: threadId) { error in
+ XCTAssertNotNil(error)
+ completionCalled.fulfill()
+ }
+
+ let error = TestError()
+ apiClient.test_simulateResponse(Result.failure(error))
+
+ wait(for: [completionCalled], timeout: defaultTimeout)
+ }
+
+ private func savePreExistingData(channelId: ChannelId, threadId: MessageId?) throws {
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: .unique, role: .user))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: channelId)))
+ if let threadId {
+ try session.saveMessage(payload: .dummy(messageId: threadId), for: channelId, syncOwnReactions: false, cache: nil)
+ }
+ }
+ }
+}
diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift
index fb396e96d34..27b6c09cbb4 100644
--- a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift
+++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift
@@ -51,7 +51,8 @@ final class ChannelReadUpdaterMiddleware_Tests: XCTestCase {
pendingMessages: nil,
pinnedMessages: [],
channelReads: [currentUserReadPayload],
- isHidden: false
+ isHidden: false,
+ draft: nil
)
try! database.writeSynchronously { session in
diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/DraftUpdaterMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/DraftUpdaterMiddleware_Tests.swift
new file mode 100644
index 00000000000..bbcc4a63daa
--- /dev/null
+++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/DraftUpdaterMiddleware_Tests.swift
@@ -0,0 +1,167 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class DraftUpdaterMiddleware_Tests: XCTestCase {
+ var middleware: DraftUpdaterMiddleware!
+ var center: EventNotificationCenter_Mock!
+ var database: DatabaseContainer_Spy!
+
+ override func setUp() {
+ super.setUp()
+ database = DatabaseContainer_Spy()
+ center = EventNotificationCenter_Mock(database: database)
+ middleware = DraftUpdaterMiddleware()
+ }
+
+ override func tearDown() {
+ database = nil
+ AssertAsync.canBeReleased(&database)
+ super.tearDown()
+ }
+
+ func test_forwardsOtherEvents() throws {
+ let event = TestEvent()
+ let forwardedEvent = middleware.handle(event: event, session: database.viewContext)
+ let unwrappedForwardedEvent = try XCTUnwrap(forwardedEvent as? TestEvent)
+ XCTAssertEqual(unwrappedForwardedEvent, event)
+ }
+
+ // MARK: - Draft Updated Event Tests
+
+ func test_draftUpdatedEvent_savesMessageToDatabase() throws {
+ let currentUserId = UserId.unique
+ let cid = ChannelId.unique
+ let draftId = MessageId.unique
+
+ let eventPayload = EventPayload(
+ eventType: .draftUpdated,
+ cid: cid,
+ createdAt: .unique,
+ draft: .dummy(
+ cid: cid,
+ message: .dummy(
+ id: draftId,
+ text: "Test draft"
+ )
+ )
+ )
+
+ let event = try DraftUpdatedEventDTO(from: eventPayload)
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: currentUserId, role: .user))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+
+ // Verify draft doesn't exist before the event
+ XCTAssertNil(session.message(id: draftId))
+
+ _ = self.middleware.handle(event: event, session: session)
+
+ // Verify draft was saved
+ let savedDraft = try XCTUnwrap(session.message(id: draftId))
+ XCTAssertEqual(savedDraft.text, "Test draft")
+ XCTAssertEqual(savedDraft.cid, cid.rawValue)
+ }
+ }
+
+ // MARK: - Draft Deleted Event Tests
+
+ func test_draftDeletedEvent_deletesMessageFromDatabase() throws {
+ let currentUserId = UserId.unique
+ let cid = ChannelId.unique
+ let draftId = MessageId.unique
+
+ let eventPayload = EventPayload(
+ eventType: .draftDeleted,
+ cid: cid,
+ createdAt: .unique,
+ draft: .dummy(
+ cid: cid,
+ message: .dummy(
+ id: draftId,
+ text: "Test draft"
+ )
+ )
+ )
+
+ let event = try DraftDeletedEventDTO(from: eventPayload)
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: currentUserId, role: .user))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+
+ // Save a draft message first
+ try session.saveDraftMessage(
+ payload: eventPayload.draft!,
+ for: cid,
+ cache: nil
+ )
+
+ // Verify draft exists before deletion
+ XCTAssertNotNil(session.message(id: draftId))
+
+ _ = self.middleware.handle(event: event, session: session)
+
+ // Verify draft was deleted
+ XCTAssertNil(session.message(id: draftId))
+ }
+ }
+
+ func test_draftDeletedEvent_whenThreadIdExists_deletesMessageFromThread() throws {
+ let currentUserId = UserId.unique
+ let cid = ChannelId.unique
+ let draftId = MessageId.unique
+ let threadId = MessageId.unique
+
+ let eventPayload = EventPayload(
+ eventType: .draftDeleted,
+ cid: cid,
+ createdAt: .unique,
+ draft: .dummy(
+ cid: cid,
+ message: .dummy(
+ id: draftId,
+ text: "Test draft"
+ )
+ )
+ )
+
+ let event = try DraftDeletedEventDTO(from: eventPayload)
+
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(payload: .dummy(userId: currentUserId, role: .user))
+ try session.saveChannel(payload: .dummy(channel: .dummy(cid: cid)))
+
+ // Save a thread and draft message
+ try session.saveThread(
+ payload: .dummy(
+ parentMessageId: threadId,
+ channel: .dummy(cid: cid),
+ latestReplies: []
+ ),
+ cache: nil
+ )
+ try session.saveDraftMessage(
+ payload: eventPayload.draft!,
+ for: cid,
+ cache: nil
+ )
+
+ // Verify draft exists before deletion
+ XCTAssertNotNil(session.message(id: draftId))
+
+ _ = self.middleware.handle(event: event, session: session)
+
+ // Verify draft was deleted
+ XCTAssertNil(session.message(id: draftId))
+
+ // Verify thread still exists
+ XCTAssertNotNil(session.thread(parentMessageId: threadId, cache: nil))
+ }
+ }
+}
diff --git a/Tests/StreamChatTests/WebSocketClient/Events/DraftEvents_Tests.swift b/Tests/StreamChatTests/WebSocketClient/Events/DraftEvents_Tests.swift
new file mode 100644
index 00000000000..db7af746dba
--- /dev/null
+++ b/Tests/StreamChatTests/WebSocketClient/Events/DraftEvents_Tests.swift
@@ -0,0 +1,79 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class DraftEvents_Tests: XCTestCase {
+ let draftId: String = "draft-123"
+ let cid = ChannelId(type: .messaging, id: "general")
+ let threadId: MessageId = "thread-123"
+
+ var eventDecoder: EventDecoder!
+
+ override func setUp() {
+ super.setUp()
+ eventDecoder = EventDecoder()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ eventDecoder = nil
+ }
+
+ // MARK: - DraftUpdatedEvent Tests
+
+ func test_draftUpdatedEvent_decoding() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "DraftUpdated")
+ let event = try eventDecoder.decode(from: json) as? DraftUpdatedEventDTO
+ XCTAssertEqual(event?.cid, cid)
+ XCTAssertEqual(event?.draft.message.id, draftId)
+ XCTAssertEqual(event?.draft.message.text, "Test draft message")
+ XCTAssertEqual(event?.createdAt.description, "2024-02-11 15:42:21 +0000")
+ }
+
+ func test_draftUpdatedEvent_toDomainEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "DraftUpdated")
+ let event = try eventDecoder.decode(from: json) as? DraftUpdatedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ // Save required data
+ _ = try session.saveChannel(payload: .dummy(cid: cid), query: nil, cache: nil)
+ _ = try session.saveMessage(payload: .dummy(messageId: draftId, authorUserId: "test-user"), for: cid, cache: nil)
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? DraftUpdatedEvent)
+ XCTAssertEqual(domainEvent.cid, cid)
+ XCTAssertEqual(domainEvent.draftMessage.id, draftId)
+ }
+
+ func test_draftUpdatedEvent_toDomainEvent_returnsNilWhenMissingData() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "DraftUpdated")
+ let event = try eventDecoder.decode(from: json) as? DraftUpdatedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ // Don't save any data to test nil case
+ XCTAssertNil(event?.toDomainEvent(session: session))
+ }
+
+ // MARK: - DraftDeletedEvent Tests
+
+ func test_draftDeletedEvent_decoding() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "DraftDeleted")
+ let event = try eventDecoder.decode(from: json) as? DraftDeletedEventDTO
+ XCTAssertEqual(event?.cid, cid)
+ XCTAssertEqual(event?.draft.parentId, threadId)
+ XCTAssertEqual(event?.createdAt.description, "2024-02-11 15:42:21 +0000")
+ }
+
+ func test_draftDeletedEvent_toDomainEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "DraftDeleted")
+ let event = try eventDecoder.decode(from: json) as? DraftDeletedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? DraftDeletedEvent)
+ XCTAssertEqual(domainEvent.cid, cid)
+ XCTAssertEqual(domainEvent.threadId, threadId)
+ }
+}
diff --git a/Tests/StreamChatTests/Workers/ThreadsRepository_Tests.swift b/Tests/StreamChatTests/Workers/ThreadsRepository_Tests.swift
index 401eeeb7f67..6ea6ec95789 100644
--- a/Tests/StreamChatTests/Workers/ThreadsRepository_Tests.swift
+++ b/Tests/StreamChatTests/Workers/ThreadsRepository_Tests.swift
@@ -38,6 +38,7 @@ final class ThreadsRepository_Tests: XCTestCase {
payload: .dummy(messageId: messageId),
for: channelId,
syncOwnReactions: false,
+ skipDraftUpdate: true,
cache: nil
)
}
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift
index f76321d7a0c..6ba4e1efdf4 100644
--- a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift
+++ b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift
@@ -1523,6 +1523,98 @@ final class ChatChannelVC_Tests: XCTestCase {
XCTAssertEqual(mockAudioQueuePlayerNextItemProvider.findNextItemWasCalledWithLookUpScope, .subsequentMessagesFromUser)
XCTAssertEqual(actual, expected)
}
+
+ // MARK: - Draft Messages
+
+ func test_channelWithDraftMessage_showsDraftInComposer() {
+ let draftMessage = DraftMessage.mock(text: "Draft message text")
+
+ channelControllerMock.channel_mock = .mock(
+ cid: .unique,
+ draftMessage: draftMessage
+ )
+
+ vc.view.layoutIfNeeded()
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
+
+ func test_channelWithDraftMessage_withQuotedMessage_showsDraftInComposer() {
+ let quotedMessage = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "Quoted message",
+ author: .mock(id: .unique)
+ )
+
+ let draftMessage = DraftMessage.mock(
+ text: "Draft with quote",
+ quotedMessage: quotedMessage
+ )
+
+ channelControllerMock.channel_mock = .mock(
+ cid: .unique,
+ draftMessage: draftMessage
+ )
+
+ vc.view.layoutIfNeeded()
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
+
+ func test_channelWithDraftMessage_withCommand_showsDraftInComposer() {
+ let draftMessage = DraftMessage.mock(
+ text: "Hey",
+ command: "Giphy"
+ )
+
+ channelControllerMock.channel_mock = .mock(
+ cid: .unique,
+ draftMessage: draftMessage
+ )
+
+ vc.view.layoutIfNeeded()
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
+
+ func test_channelWithDraftMessage_withUnknownCommand_showsDraftInComposer() {
+ let draftMessage = DraftMessage.mock(
+ text: "/test"
+ )
+
+ channelControllerMock.channel_mock = .mock(
+ cid: .unique,
+ draftMessage: draftMessage
+ )
+
+ vc.view.layoutIfNeeded()
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
+
+ func test_channelWithDraftMessage_whenDraftIsUpdatedFromEvent_updatesDraftInComposer() {
+ let draftMessage = DraftMessage.mock(text: "Draft Message")
+
+ let channel = ChatChannel.mock(cid: .unique, draftMessage: draftMessage)
+ channelControllerMock.channel_mock = channel
+
+ vc.view.layoutIfNeeded()
+
+ let updatedDraftMessage = DraftMessage.mock(text: "Updated draft")
+ let updatedChannel = ChatChannel.mock(cid: .unique, draftMessage: updatedDraftMessage)
+ channelControllerMock.channel_mock = updatedChannel
+ channelControllerMock.mockCid = channel.cid
+ let event = DraftUpdatedEvent(
+ cid: channel.cid,
+ channel: channel,
+ draftMessage: updatedDraftMessage,
+ createdAt: .unique
+ )
+ vc.eventsController(vc.eventsController, didReceiveEvent: event)
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
}
private extension ChatChannelVC_Tests {
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_showsDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_showsDraftInComposer.default-light.png
new file mode 100644
index 00000000000..3063f79a23c
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_showsDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_whenDraftIsUpdatedFromEvent_updatesDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_whenDraftIsUpdatedFromEvent_updatesDraftInComposer.default-light.png
new file mode 100644
index 00000000000..3eb7592add4
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_whenDraftIsUpdatedFromEvent_updatesDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withCommand_showsDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withCommand_showsDraftInComposer.default-light.png
new file mode 100644
index 00000000000..6d01a658d6b
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withCommand_showsDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withQuotedMessage_showsDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withQuotedMessage_showsDraftInComposer.default-light.png
new file mode 100644
index 00000000000..335f080c05b
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withQuotedMessage_showsDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withUnknownCommand_showsDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withUnknownCommand_showsDraftInComposer.default-light.png
new file mode 100644
index 00000000000..f75224664cc
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_channelWithDraftMessage_withUnknownCommand_showsDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/ChatChannelListItemView_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/ChatChannelListItemView_Tests.swift
index 02ef589e15e..623bf6c9ee0 100644
--- a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/ChatChannelListItemView_Tests.swift
+++ b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/ChatChannelListItemView_Tests.swift
@@ -2,7 +2,7 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//
-import StreamChat
+@testable import StreamChat
@testable import StreamChatTestTools
@testable import StreamChatUI
import StreamSwiftTestHelpers
@@ -841,6 +841,71 @@ final class ChatChannelListItemView_Tests: XCTestCase {
AssertSnapshot(view, variants: [.defaultLight])
}
+ func test_appearance_draftPreview() throws {
+ let message: DraftMessage = .mock(
+ text: "Cool"
+ )
+
+ let view = channelItemView(
+ content: .init(
+ channel: channel(draftMessage: message),
+ currentUserId: currentUser.id
+ )
+ )
+
+ AssertSnapshot(view)
+ }
+
+ func test_appearance_draftPreview_withAttachments() throws {
+ let messageWithGiphy: ChatMessage = try mockGiphyMessage(
+ text: "Example message",
+ isSentByCurrentUser: true
+ )
+ let viewWithGiphy = channelItemView(
+ content: .init(
+ channel: channel(draftMessage: DraftMessage(messageWithGiphy)),
+ currentUserId: currentUser.id
+ )
+ )
+
+ let messageWithVoiceRecording = ChatMessage.mock(
+ attachments: [
+ .dummy(
+ type: .voiceRecording,
+ payload: try JSONEncoder().encode(VoiceRecordingAttachmentPayload(
+ title: nil,
+ voiceRecordingRemoteURL: .unique(),
+ file: .init(type: .aac, size: 120, mimeType: nil),
+ duration: nil,
+ waveformData: nil,
+ extraData: nil
+ ))
+ )
+ ]
+ )
+ let viewWithVoiceRecording = channelItemView(
+ content: .init(
+ channel: channel(draftMessage: DraftMessage(messageWithVoiceRecording)),
+ currentUserId: currentUser.id
+ )
+ )
+
+ let messageWithAudioRecording = try mockAudioMessage(
+ text: "Example message",
+ isSentByCurrentUser: true
+ )
+ let viewWithAudioRecording = channelItemView(
+ content: .init(
+ channel: channel(draftMessage: DraftMessage(messageWithAudioRecording)),
+ currentUserId: currentUser.id
+ )
+ )
+
+ AssertSnapshot(viewWithGiphy, variants: .onlyUserInterfaceStyles, suffix: "giphy")
+ AssertSnapshot(viewWithVoiceRecording, variants: .onlyUserInterfaceStyles, suffix: "voiceRecording")
+ AssertSnapshot(viewWithAudioRecording, variants: .onlyUserInterfaceStyles, suffix: "audioRecording")
+ }
+
func test_appearanceCustomization_usingAppearance() {
var appearance = Appearance()
appearance.fonts.bodyBold = .italicSystemFont(ofSize: 20)
@@ -937,7 +1002,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {
id: .unique,
cid: .unique,
text: .unique,
- author: .mock(id: .unique),
+ author: .mock(id: .unique, name: "Luke"),
attachments: [
.dummy(
type: .voiceRecording,
@@ -1261,7 +1326,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {
id: .unique,
cid: .unique,
text: .unique,
- author: .mock(id: .unique),
+ author: .mock(id: .unique, name: "Luke"),
attachments: [
.dummy(
type: .voiceRecording,
@@ -1646,6 +1711,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {
) -> ChatChannelListItemView {
let view = ChatChannelListItemView().withoutAutoresizingMaskConstraints
view.components = components
+ view.components.isDraftMessagesEnabled = true
view.appearance = appearance
view.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()
view.content = content
@@ -1655,6 +1721,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {
private func channel(
previewMessage: ChatMessage? = nil,
+ draftMessage: DraftMessage? = nil,
readEventsEnabled: Bool = true,
memberCount: Int = 0,
membership: ChatChannelMember? = nil
@@ -1667,7 +1734,8 @@ final class ChatChannelListItemView_Tests: XCTestCase {
config: .mock(readEventsEnabled: readEventsEnabled),
membership: membership,
memberCount: memberCount,
- previewMessage: previewMessage
+ previewMessage: previewMessage,
+ draftMessage: draftMessage
)
}
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.default-light.png
new file mode 100644
index 00000000000..0f727538c69
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.extraExtraExtraLarge-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.extraExtraExtraLarge-light.png
new file mode 100644
index 00000000000..80b080a364e
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.extraExtraExtraLarge-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.rightToLeftLayout-default.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.rightToLeftLayout-default.png
new file mode 100644
index 00000000000..775db2ceaee
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.rightToLeftLayout-default.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.small-dark.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.small-dark.png
new file mode 100644
index 00000000000..7f6dcb344d3
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview.small-dark.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-audioRecording.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-audioRecording.png
new file mode 100644
index 00000000000..2637fc7e669
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-audioRecording.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-giphy.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-giphy.png
new file mode 100644
index 00000000000..23d8bb61dfc
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-giphy.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-voiceRecording.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-voiceRecording.png
new file mode 100644
index 00000000000..a0820489039
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-dark-voiceRecording.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-audioRecording.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-audioRecording.png
new file mode 100644
index 00000000000..07d33e90e39
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-audioRecording.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-giphy.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-giphy.png
new file mode 100644
index 00000000000..6c7684adcff
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-giphy.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-voiceRecording.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-voiceRecording.png
new file mode 100644
index 00000000000..00067892895
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannelList/__Snapshots__/ChatChannelListItemView_Tests/test_appearance_draftPreview_withAttachments.default-light-voiceRecording.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThread/ChatThreadVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatThread/ChatThreadVC_Tests.swift
index 45ec8b60449..4709faecf7c 100644
--- a/Tests/StreamChatUITests/SnapshotTests/ChatThread/ChatThreadVC_Tests.swift
+++ b/Tests/StreamChatUITests/SnapshotTests/ChatThread/ChatThreadVC_Tests.swift
@@ -2,7 +2,7 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//
-import StreamChat
+@testable import StreamChat
@testable import StreamChatTestTools
@testable import StreamChatUI
import StreamSwiftTestHelpers
@@ -297,6 +297,69 @@ final class ChatThreadVC_Tests: XCTestCase {
XCTAssertEqual(messageListVCMock?.jumpToMessageCalledWith?.animated, false)
}
+ // MARK: - Draft Messages
+
+ func test_threadWithDraftReply_showsDraftInComposer() {
+ let draftMessage = DraftMessage.mock(
+ text: "Draft reply text",
+ showReplyInChannel: true
+ )
+
+ messageControllerMock.simulateInitial(
+ message: .mock(
+ id: .unique,
+ cid: .unique,
+ text: "Parent message",
+ author: .mock(id: .unique),
+ draftReply: draftMessage
+ ),
+ replies: [],
+ state: .localDataFetched
+ )
+ messageControllerMock.simulate(state: .remoteDataFetched)
+
+ vc.view.layoutIfNeeded()
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
+
+ func test_threadWithDraftReply_whenDraftIsUpdatedFromEvent_updatesDraftInComposer() {
+ let parentMessage = ChatMessage.mock(
+ id: messageControllerMock.messageId,
+ cid: .unique,
+ text: "Parent message",
+ author: .mock(id: .unique),
+ draftReply: .mock(text: "Draft Message")
+ )
+ messageControllerMock.simulateInitial(
+ message: parentMessage,
+ replies: [],
+ state: .localDataFetched
+ )
+ messageControllerMock.simulate(state: .remoteDataFetched)
+
+ vc.view.layoutIfNeeded()
+
+ let draftReply = DraftMessage.mock(threadId: parentMessage.id, text: "Updated Draft Message")
+ let updatedParentMessage = ChatMessage.mock(
+ id: messageControllerMock.messageId,
+ cid: .unique,
+ text: "Parent message",
+ author: .mock(id: .unique),
+ draftReply: draftReply
+ )
+ messageControllerMock.message_mock = updatedParentMessage
+ let updateDraftEvent = DraftUpdatedEvent(
+ cid: updatedParentMessage.cid!,
+ channel: .mock(cid: updatedParentMessage.cid!),
+ draftMessage: draftReply,
+ createdAt: .unique
+ )
+ vc.eventsController(vc.eventsController, didReceiveEvent: updateDraftEvent)
+
+ AssertSnapshot(vc, variants: [.defaultLight])
+ }
+
// MARK: - audioQueuePlayerNextAssetURL
func test_audioQueuePlayerNextAssetURL_callsNextAvailableVoiceRecordingProvideWithExpectedInputAndReturnsValue() throws {
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThread/__Snapshots__/ChatThreadVC_Tests/test_threadWithDraftReply_showsDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatThread/__Snapshots__/ChatThreadVC_Tests/test_threadWithDraftReply_showsDraftInComposer.default-light.png
new file mode 100644
index 00000000000..5cc4c786c48
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatThread/__Snapshots__/ChatThreadVC_Tests/test_threadWithDraftReply_showsDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThread/__Snapshots__/ChatThreadVC_Tests/test_threadWithDraftReply_whenDraftIsUpdatedFromEvent_updatesDraftInComposer.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatThread/__Snapshots__/ChatThreadVC_Tests/test_threadWithDraftReply_whenDraftIsUpdatedFromEvent_updatesDraftInComposer.default-light.png
new file mode 100644
index 00000000000..ed26ae11bc6
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatThread/__Snapshots__/ChatThreadVC_Tests/test_threadWithDraftReply_whenDraftIsUpdatedFromEvent_updatesDraftInComposer.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/ChatThreadListItemView_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/ChatThreadListItemView_Tests.swift
index 241c744c5bd..96377798d15 100644
--- a/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/ChatThreadListItemView_Tests.swift
+++ b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/ChatThreadListItemView_Tests.swift
@@ -138,6 +138,46 @@ final class ChatThreadListItemView_Tests: XCTestCase {
AssertSnapshot(view, variants: [.defaultLight])
}
+ func test_defaultAppearance_whenDraftMessage() {
+ let currentUser = mockVader
+ let thread = mockThread
+ .with(
+ parentMessage: .mock(text: "Parent", draftReply: .init(.mock(text: "Test"))),
+ latestReplies: [
+ .mock(text: "", author: mockYoda, attachments: [.dummy(type: .audio)])
+ ]
+ )
+
+ let view = threadItemView(
+ content: .init(
+ thread: thread,
+ currentUserId: currentUser.id
+ )
+ )
+
+ AssertSnapshot(view, variants: [.defaultLight])
+ }
+
+ func test_defaultAppearance_whenDraftMessage_withAttachment() {
+ let currentUser = mockVader
+ let thread = mockThread
+ .with(
+ parentMessage: .mock(text: "Parent", draftReply: .init(.mock(text: "", attachments: [.dummy(type: .image)]))),
+ latestReplies: [
+ .mock(text: "", author: mockYoda, attachments: [.dummy(type: .audio)])
+ ]
+ )
+
+ let view = threadItemView(
+ content: .init(
+ thread: thread,
+ currentUserId: currentUser.id
+ )
+ )
+
+ AssertSnapshot(view, variants: [.defaultLight])
+ }
+
private func threadItemView(
content: ChatThreadListItemView.Content?,
components: Components = .mock,
@@ -145,6 +185,7 @@ final class ChatThreadListItemView_Tests: XCTestCase {
) -> ChatThreadListItemView {
let view = ChatThreadListItemView().withoutAutoresizingMaskConstraints
view.components = components
+ view.components.isDraftMessagesEnabled = true
view.appearance = appearance
view.appearance.formatters.threadListMessageTimestamp = DefaultMessageTimestampFormatter()
view.content = content
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenAttachments.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenAttachments.default-light.png
index 08b1d7c2cd6..fb96441bbd8 100644
Binary files a/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenAttachments.default-light.png and b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenAttachments.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenDraftMessage.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenDraftMessage.default-light.png
new file mode 100644
index 00000000000..e9c96a5c77e
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenDraftMessage.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenDraftMessage_withAttachment.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenDraftMessage_withAttachment.default-light.png
new file mode 100644
index 00000000000..6bb86da13e4
Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatThreadList/__Snapshots__/ChatThreadListItemView_Tests/test_defaultAppearance_whenDraftMessage_withAttachment.default-light.png differ
diff --git a/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift
index d00e6fb0ede..7dafbe8d478 100644
--- a/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift
+++ b/Tests/StreamChatUITests/SnapshotTests/Composer/ComposerVC_Tests.swift
@@ -35,6 +35,8 @@ final class ComposerVC_Tests: XCTestCase {
)
composerVC = .init()
+ composerVC.components = .mock
+ composerVC.components.isDraftMessagesEnabled = true
composerVC.channelController = mockedChatChannelController
}
@@ -1106,6 +1108,79 @@ final class ComposerVC_Tests: XCTestCase {
XCTAssertTrue(containerViewController.view.subviews.last === floatingView)
}
+
+ // MARK: - Draft Message Handling
+
+ func test_viewWillDisappear_whenContentIsEmpty_andNoDraftExists() {
+ composerVC.content = .initial()
+ let mock = ChatChannelController_Mock.mock()
+ mock.channel_mock = .mock(cid: .unique, draftMessage: nil)
+ composerVC.channelController = mock
+
+ composerVC.viewWillDisappear(false)
+
+ XCTAssertEqual(mock.updateDraftMessage_callCount, 0)
+ XCTAssertEqual(mock.deleteDraftMessage_callCount, 0)
+ }
+
+ func test_viewWillDisappear_whenHasContent_updatesDraft() {
+ composerVC.content.draftMessage(.mock(text: "New draft text"))
+ let mock = ChatChannelController_Mock.mock()
+ mock.channel_mock = .mock(cid: .unique)
+ composerVC.channelController = mock
+
+ composerVC.viewWillDisappear(false)
+
+ XCTAssertEqual(mock.updateDraftMessage_callCount, 1)
+ XCTAssertEqual(mock.updateDraftMessage_text, "New draft text")
+ }
+
+ func test_viewWillDisappear_whenHasCommand_updatesDraft() {
+ composerVC.content.draftMessage(.mock(text: "", command: "giphy"))
+ let mock = ChatChannelController_Mock.mock()
+ mock.channel_mock = .mock(cid: .unique)
+ composerVC.channelController = mock
+
+ composerVC.viewWillDisappear(false)
+
+ XCTAssertEqual(mock.updateDraftMessage_callCount, 1)
+ }
+
+ func test_textViewDidChange_whenInputIsEmpty_whenHasDraft_deletesDraft() {
+ composerVC.content.text = "Hey"
+ let mock = ChatChannelController_Mock.mock()
+ mock.channel_mock = .mock(cid: .unique, draftMessage: .mock())
+ composerVC.channelController = mock
+
+ composerVC.composerView.inputMessageView.textView.text = ""
+ composerVC.textViewDidChange(composerVC.composerView.inputMessageView.textView)
+
+ XCTAssertEqual(mock.deleteDraftMessage_callCount, 1)
+ }
+
+ func test_textViewDidChange_whenInputIsEmpty_whenNoDraft_doesNotCallDeleteDraft() {
+ composerVC.content.text = "Hey"
+ let mock = ChatChannelController_Mock.mock()
+ mock.channel_mock = .mock(cid: .unique, draftMessage: nil)
+ composerVC.channelController = mock
+
+ composerVC.composerView.inputMessageView.textView.text = ""
+ composerVC.textViewDidChange(composerVC.composerView.inputMessageView.textView)
+
+ XCTAssertEqual(mock.deleteDraftMessage_callCount, 0)
+ }
+
+ func test_textViewDidChange_whenHasContent_whenHasDraft_doesNotCallDeleteDraft() {
+ composerVC.content.text = "Hey"
+ let mock = ChatChannelController_Mock.mock()
+ mock.channel_mock = .mock(cid: .unique, draftMessage: .mock())
+ composerVC.channelController = mock
+
+ composerVC.composerView.inputMessageView.textView.text = "Ahahah"
+ composerVC.textViewDidChange(composerVC.composerView.inputMessageView.textView)
+
+ XCTAssertEqual(mock.deleteDraftMessage_callCount, 0)
+ }
}
// MARK: - Helpers
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index d31ce23273c..ca0501d9d31 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -25,7 +25,7 @@ before_all do |lane|
if is_ci
setup_ci
setup_git_config
- xcversion(version: xcode_version) unless [:publish_release, :allure_launch, :allure_upload, :pod_lint, :sync_mock_server, :copyright].include?(lane)
+ xcversion(version: xcode_version) unless [:publish_release, :sonar_upload, :allure_launch, :allure_upload, :pod_lint, :sync_mock_server, :copyright].include?(lane)
end
end
@@ -336,6 +336,8 @@ lane :test do |options|
}
scan(scan_options)
+
+ slather
end
desc 'Starts Sinatra web server'
@@ -597,7 +599,7 @@ desc 'Runs ui tests in Debug config'
lane :test_ui do |options|
next unless is_check_required(sources: sources_matrix[:ui], force_check: @force_check)
- record_mode = !options[:record].to_s.empty?
+ record_mode = options[:record].to_s == 'true'
remove_snapshots if record_mode
update_testplan_on_ci(path: 'Tests/StreamChatUITests/StreamChatUITestPlan.xctestplan')
@@ -630,7 +632,7 @@ lane :test_ui do |options|
pr_create(
title: '[CI] Snapshots',
base_branch: current_branch,
- head_branch: "#{current_branch}-snapshots"
+ head_branch: "#{current_branch}-snapshots-#{Time.now.to_i}"
)
end
end
diff --git a/fastlane/Sonarfile b/fastlane/Sonarfile
index e35d69086fd..c9081508136 100755
--- a/fastlane/Sonarfile
+++ b/fastlane/Sonarfile
@@ -4,46 +4,23 @@ desc 'Get code coverage report and run complexity analysis for Sonar'
lane :sonar_upload do
next unless is_check_required(sources: sources_matrix[:unit], force_check: @force_check)
- sonar_args = ''
version_number = get_version_number(
xcodeproj: 'StreamChat.xcodeproj',
target: 'StreamChat'
)[/\d+\.\d+\.\d/]
- unless Dir.glob('test_output/*.xcresult').empty?
- slather
- lizard(
- source_folder: './Sources/',
- language: 'swift',
- export_type: 'xml',
- report_file: 'reports/lizard.xml'
- )
- sonar_args = '-Dsonar.coverageReportPaths="reports/sonarqube-generic-coverage.xml" ' \
- '-Dsonar.swift.lizard.report="reports/lizard.xml"'
- end
-
Dir.chdir('..') do
- sh("./fastlane/sonar/bin/sonar-scanner -Dsonar.projectVersion=#{version_number} -Dproject.settings=sonar-project.properties #{sonar_args}")
+ sh("./fastlane/sonar/bin/sonar-scanner " \
+ "-Dsonar.projectVersion=#{version_number} " \
+ "-Dproject.settings=sonar-project.properties " \
+ "-Dsonar.coverageReportPaths='reports/sonarqube-generic-coverage.xml'")
end
- next if sonar_args.empty? || ENV['GITHUB_EVENT_NAME'] == 'pull_request' || current_branch !~ /main|develop/
+ next if ENV['GITHUB_EVENT_NAME'] == 'pull_request' || current_branch !~ /main|develop/
slack_sonarcloud_metrics(version_number: version_number)
end
-desc 'Gets Sonar options'
-private_lane :sonar_options do |options|
- default_options = { sonar_login: ENV.fetch('SONAR_TOKEN', nil), sonar_runner_args: options[:sonar_args] }
-
- if ENV['GITHUB_EVENT_NAME'] == 'pull_request'
- default_options.merge(pull_request_branch: ENV.fetch('GITHUB_HEAD_REF', nil),
- pull_request_base: ENV.fetch('GITHUB_BASE_REF', nil),
- pull_request_key: ENV.fetch('GITHUB_PR_NUM', nil))
- else
- default_options.merge(branch_name: current_branch, project_version: options[:version_number])
- end
-end
-
desc 'Creates a report in Slack with SonarCloud analysis details'
private_lane :slack_sonarcloud_metrics do |options|
project_key = 'GetStream_stream-chat-swift'
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index adc06a4fa26..00000000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-lizard