From e28a75878082a811d6063cd57469fa5d48481960 Mon Sep 17 00:00:00 2001
From: Nuno Vieira
Date: Tue, 3 Jun 2025 21:53:38 +0100
Subject: [PATCH 01/11] Add snapshot postfix to v4.79.1
---
Sources/StreamChat/Generated/SystemEnvironment+Version.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
index a908f4e0b4d..cbf32e0ad2b 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.79.1"
+ public static let version: String = "4.79.1-SNAPSHOT"
}
From 0a8a46c2f0af5d444b41c23799a28c1c046814ed Mon Sep 17 00:00:00 2001
From: Alexey Alter-Pesotskiy
Date: Wed, 4 Jun 2025 17:15:31 +0100
Subject: [PATCH 02/11] [CI] Switch macos version to keep the support of Xcode
15.4 (#3689)
---
.github/workflows/cron-checks.yml | 8 ++++----
.github/workflows/smoke-checks.yml | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml
index 5b4ed3c8848..c300ab9d83c 100644
--- a/.github/workflows/cron-checks.yml
+++ b/.github/workflows/cron-checks.yml
@@ -28,7 +28,7 @@ jobs:
setup_runtime: false
- ios: 17.5
xcode: 15.4
- os: macos-15
+ os: macos-14
device: "iPhone 15 Pro"
setup_runtime: false
- ios: 16.4
@@ -101,7 +101,7 @@ jobs:
setup_runtime: false
- ios: 17.5
xcode: 15.4
- os: macos-15
+ os: macos-14
device: "iPhone 15 Pro"
setup_runtime: false
- ios: 16.4
@@ -148,7 +148,7 @@ jobs:
build-old-xcode:
name: Build LLC + UI (Xcode 15)
- runs-on: macos-15
+ runs-on: macos-14
env:
XCODE_VERSION: "15.4"
steps:
@@ -175,7 +175,7 @@ jobs:
automated-code-review:
name: Automated Code Review
- runs-on: macos-15
+ runs-on: macos-14
env:
XCODE_VERSION: "15.4"
steps:
diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index cb5b40a5881..fb04a5d08b9 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -46,7 +46,7 @@ jobs:
automated-code-review:
name: Automated Code Review
- runs-on: macos-15
+ runs-on: macos-14
env:
XCODE_VERSION: "15.4"
if: ${{ github.event.inputs.record_snapshots != 'true' }}
@@ -67,7 +67,7 @@ jobs:
build-old-xcode:
name: Build LLC + UI (Xcode 15)
- runs-on: macos-15
+ runs-on: macos-14
if: ${{ github.event.inputs.record_snapshots != 'true' }}
env:
XCODE_VERSION: "15.4"
From 5280649c49fdab40dd5561353f1d2712c5b9bee3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 5 Jun 2025 10:37:43 +0100
Subject: [PATCH 03/11] Bump rack from 3.1.14 to 3.1.16 (#3690)
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 80b48c8c459..b9688475b4d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -317,7 +317,7 @@ GEM
puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
- rack (3.1.14)
+ rack (3.1.16)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
From 12259e59bfa275638eb51d05f4adbe4f19f20b8d Mon Sep 17 00:00:00 2001
From: Nuno Vieira
Date: Thu, 5 Jun 2025 14:29:58 +0100
Subject: [PATCH 04/11] Add Message Reminders (#3623)
* Add new reminder endpoint paths
* Add new Message Reminder Endpoints
* Add MessageReminderListQuery
* Change the default baseURL of Frankfurt C2 to staging env
* Remove user_id from request payloads since this is server side only
* Add MessageReminder and MessageReminderDTO
* Add createReminder, updateReminder and deleteReminder actions
* Add `Message.reminder` and update it or delete it when needed
* Add demo app example to create, update and delete reminders
* Add highlighted message view when saved for later in the demo app
* Add test coverage to Message Controller
* Add test coverage to MessagUpdater
* Add test coverage to ReminderPayload parsing
* Expose `Filter.isNil` that wraps the`.exists` filter
* Rename `Filter+ChatChannel` to `Filter+predicate` so that it can be used everywhere
* Implementation of the Message Reminder Query
* Add Reminder List Demo App UI Component
* Move reminder MessageUpdater functions to RemindersRepository
* Handle Reminder Events
* Fix forgotten hardcoded test in reminder list query
* Fix reminder list query tests
* Add local push notification when reminder is expired
* Fix reminder with empty text in the Reminder List Demo App UI
* Add Demo App reminders feature flag
* Improve the Filter+predicate documentation
* Fix removeAllData tests
* Add reminders enabled by default on the demo app
* Remove local notification of reminders since it should come from the server
* Refactor reminders to have their own controller
* Move reminder endpoints to a separate file
* Create Reminder Payloads file
* Move reminder payloads to classes to reduce SDK size
* Update CHANGELOG.md
* Remove unnecessary notifications import
* Fix unit tests by missing inits of payloads
* Remove `messageId` from FilterKey since it is not really useful at the moment
* Fix reminder lists not updating when due date is reached by the fact that the current date is now an old date
* Fix forgotten setting of a delegate
* Remove unnecessary available iOS 13 macros
* Add "Message" prefix to Reminder Events
* Use new "converting" helper function to simply reminders repository
* Remove some unnecessary warnings
* Fix conflicts
* New push type
* Fix not able to parse reminders that do not have a channel locally
* Update Atlantis
* Fix not returning any reminders if one fails to be saved to local DB
* Handle reminder push notification in the NSE
* Update reminders to 1min only
* Expose `ChannelConfig.messageRemindersEnabled`
* Fix reminder events tests
* Fix small typo
* Update CHANGELOG.md
* Fix Demo App reminder due formatting inconsistency when 1min left
* Fix not updating upcoming reminders data if it was empty
---
CHANGELOG.md | 7 +
.../AppConfigViewController.swift | 12 +
DemoApp/Screens/DemoAppTabBarController.swift | 43 +-
DemoApp/Screens/DemoReminderListVC.swift | 765 ++++++++++++++++++
DemoApp/Shared/DemoUsers.swift | 13 +-
DemoApp/Shared/StreamChatWrapper.swift | 5 +
.../Components/DemoChatMessageActionsVC.swift | 108 +++
.../DemoChatMessageContentView.swift | 36 +
...DemoChatMessageLayoutOptionsResolver.swift | 5 +
.../DemoAppCoordinator+DemoApp.swift | 14 +-
DemoAppPush/NotificationService.swift | 15 +-
.../ChatRemoteNotificationHandler.swift | 58 +-
.../EndpointPath+OfflineRequest.swift | 2 +-
.../APIClient/Endpoints/EndpointPath.swift | 9 +-
.../Payloads/ChannelListPayload.swift | 6 +
.../Endpoints/Payloads/MessagePayloads.swift | 8 +-
.../Endpoints/Payloads/ReminderPayloads.swift | 79 ++
.../Endpoints/ReminderEndpoints.swift | 51 ++
.../StreamChat/ChatClient+Environment.swift | 7 +
Sources/StreamChat/ChatClient.swift | 11 +-
Sources/StreamChat/ChatClientFactory.swift | 1 +
.../CurrentUserController.swift | 7 +-
.../MessageController/MessageController.swift | 62 ++
...essageReminderListController+Combine.swift | 47 ++
.../MessageReminderListController.swift | 200 +++++
.../Database/DTOs/ChannelConfigDTO.swift | 3 +
.../StreamChat/Database/DTOs/MessageDTO.swift | 16 +-
.../Database/DTOs/MessageReminderDTO.swift | 172 ++++
.../StreamChat/Database/DatabaseSession.swift | 18 +
.../StreamChatModel.xcdatamodel/contents | 17 +-
Sources/StreamChat/Models/ChatMessage.swift | 14 +-
Sources/StreamChat/Models/DraftMessage.swift | 1 +
.../StreamChat/Models/MessageReminder.swift | 75 ++
...atChannel.swift => Filter+predicate.swift} | 60 +-
Sources/StreamChat/Query/Filter.swift | 11 +
.../Query/MessageReminderListQuery.swift | 126 +++
.../Repositories/RemindersRepository.swift | 226 ++++++
.../ReminderUpdaterMiddleware.swift | 42 +
.../WebSocketClient/Events/EventPayload.swift | 7 +-
.../WebSocketClient/Events/EventType.swift | 20 +-
.../Events/MessageEvents.swift | 3 +-
.../Events/ReminderEvents.swift | 201 +++++
StreamChat.xcodeproj/project.pbxproj | 170 +++-
.../Unique/ChatMessage+Unique.swift | 3 +-
.../Events/Reminder/ReminderCreated.json | 104 +++
.../Events/Reminder/ReminderDeleted.json | 14 +
.../JSONs/Events/Reminder/ReminderDue.json | 14 +
.../Events/Reminder/ReminderUpdated.json | 14 +
.../Fixtures/JSONs/ReminderPayload.json | 112 +++
.../ChatMessage_Mock.swift | 6 +-
.../MessageReminder_Mock.swift | 26 +
.../Mocks/StreamChat/ChatClient_Mock.swift | 6 +
.../Database/DatabaseSession_Mock.swift | 8 +
.../RemindersRepository_Mock.swift | 123 +++
.../TestData/DummyData/XCTestCase+Dummy.swift | 1 +
.../Endpoints/EndpointPath_Tests.swift | 8 +
.../Payloads/ReminderPayloads_Tests.swift | 112 +++
.../Endpoints/ReminderEndpoints_Tests.swift | 89 ++
.../MessageController+Reminders_Tests.swift | 207 +++++
...ReminderListController+Combine_Tests.swift | 82 ++
.../MessageReminderListController_Tests.swift | 278 +++++++
.../Database/DTOs/ChannelDTO_Tests.swift | 1 +
.../Database/DatabaseContainer_Tests.swift | 10 +
.../MessageReminderListQuery_Tests.swift | 112 +++
.../RemindersRepository_Tests.swift | 562 +++++++++++++
.../ReminderUpdaterMiddleware_Tests.swift | 212 +++++
.../Events/ReminderEvents_Tests.swift | 179 ++++
67 files changed, 4952 insertions(+), 84 deletions(-)
create mode 100644 DemoApp/Screens/DemoReminderListVC.swift
create mode 100644 Sources/StreamChat/APIClient/Endpoints/Payloads/ReminderPayloads.swift
create mode 100644 Sources/StreamChat/APIClient/Endpoints/ReminderEndpoints.swift
create mode 100644 Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController+Combine.swift
create mode 100644 Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController.swift
create mode 100644 Sources/StreamChat/Database/DTOs/MessageReminderDTO.swift
create mode 100644 Sources/StreamChat/Models/MessageReminder.swift
rename Sources/StreamChat/Query/{Filter+ChatChannel.swift => Filter+predicate.swift} (92%)
create mode 100644 Sources/StreamChat/Query/MessageReminderListQuery.swift
create mode 100644 Sources/StreamChat/Repositories/RemindersRepository.swift
create mode 100644 Sources/StreamChat/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware.swift
create mode 100644 Sources/StreamChat/WebSocketClient/Events/ReminderEvents.swift
create mode 100644 TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderCreated.json
create mode 100644 TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDeleted.json
create mode 100644 TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDue.json
create mode 100644 TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderUpdated.json
create mode 100644 TestTools/StreamChatTestTools/Fixtures/JSONs/ReminderPayload.json
create mode 100644 TestTools/StreamChatTestTools/Mocks/Models + Extensions/MessageReminder_Mock.swift
create mode 100644 TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/RemindersRepository_Mock.swift
create mode 100644 Tests/StreamChatTests/APIClient/Endpoints/Payloads/ReminderPayloads_Tests.swift
create mode 100644 Tests/StreamChatTests/APIClient/Endpoints/ReminderEndpoints_Tests.swift
create mode 100644 Tests/StreamChatTests/Controllers/MessageController/MessageController+Reminders_Tests.swift
create mode 100644 Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController+Combine_Tests.swift
create mode 100644 Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController_Tests.swift
create mode 100644 Tests/StreamChatTests/Query/MessageReminderListQuery_Tests.swift
create mode 100644 Tests/StreamChatTests/Repositories/RemindersRepository_Tests.swift
create mode 100644 Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware_Tests.swift
create mode 100644 Tests/StreamChatTests/WebSocketClient/Events/ReminderEvents_Tests.swift
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbd365161ce..cde2de29c2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
_June 03, 2025_
## StreamChat
+### ā
Added
+- Add new `Filter.isNil` to make it easier to query by nil values [#3623](https://github.com/GetStream/stream-chat-swift/pull/3623)
+- Add Message Reminders [#3623](https://github.com/GetStream/stream-chat-swift/pull/3623)
+ - Add `ChatMessageController.createReminder()`
+ - Add `ChatMessageController.updateReminder()`
+ - Add `ChatMessageController.deleteReminder()`
+ - Add `MessageReminderListController` and `MessageReminderListQuery`
### š Fixed
- Fix an issue where completion handler was called twice after waiting for token refresh [#3683](https://github.com/GetStream/stream-chat-swift/pull/3683)
- Fix message not marked as published if it was previously intercepted [#3687](https://github.com/GetStream/stream-chat-swift/pull/3687)
diff --git a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift
index d2be145a539..66f0b082382 100644
--- a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift
+++ b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift
@@ -22,6 +22,8 @@ struct DemoAppConfig {
var shouldShowConnectionBanner: Bool
/// A Boolean value to define if the premium member feature is enabled. This is to test custom member data.
var isPremiumMemberFeatureEnabled: Bool
+ /// A Boolean value to define if the reminders feature is enabled.
+ var isRemindersEnabled: Bool
/// A Boolean value to define if the poll should be deleted when the message is deleted.
var shouldDeletePollOnMessageDeletion: Bool
@@ -53,6 +55,7 @@ class AppConfig {
tokenRefreshDetails: nil,
shouldShowConnectionBanner: false,
isPremiumMemberFeatureEnabled: false,
+ isRemindersEnabled: true,
shouldDeletePollOnMessageDeletion: false
)
@@ -64,6 +67,7 @@ class AppConfig {
demoAppConfig.isHardDeleteEnabled = true
demoAppConfig.shouldShowConnectionBanner = true
demoAppConfig.isPremiumMemberFeatureEnabled = true
+ demoAppConfig.isRemindersEnabled = true
demoAppConfig.shouldDeletePollOnMessageDeletion = true
StreamRuntimeCheck.assertionsEnabled = true
}
@@ -177,6 +181,7 @@ class AppConfigViewController: UITableViewController {
case tokenRefreshDetails
case shouldShowConnectionBanner
case isPremiumMemberFeatureEnabled
+ case isRemindersEnabled
}
enum ComponentsConfigOption: String, CaseIterable {
@@ -342,6 +347,10 @@ class AppConfigViewController: UITableViewController {
cell.accessoryView = makeSwitchButton(demoAppConfig.isPremiumMemberFeatureEnabled) { [weak self] newValue in
self?.demoAppConfig.isPremiumMemberFeatureEnabled = newValue
}
+ case .isRemindersEnabled:
+ cell.accessoryView = makeSwitchButton(demoAppConfig.isRemindersEnabled) { [weak self] newValue in
+ self?.demoAppConfig.isRemindersEnabled = newValue
+ }
}
}
@@ -739,6 +748,9 @@ class AppConfigViewController: UITableViewController {
guard let selectedOption = options.first else { return }
apiKeyString = selectedOption.rawValue
StreamChatWrapper.replaceSharedInstance(apiKeyString: apiKeyString)
+ if let baseURL = selectedOption.customBaseURL {
+ self?.chatClientConfig.baseURL = .init(url: baseURL)
+ }
self?.tableView.reloadData()
}
diff --git a/DemoApp/Screens/DemoAppTabBarController.swift b/DemoApp/Screens/DemoAppTabBarController.swift
index 082f36de7f4..ec776b4baa5 100644
--- a/DemoApp/Screens/DemoAppTabBarController.swift
+++ b/DemoApp/Screens/DemoAppTabBarController.swift
@@ -6,22 +6,31 @@ import StreamChat
import StreamChatUI
import UIKit
-class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDelegate {
+class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDelegate, MessageReminderListControllerDelegate {
let channelListVC: UIViewController
let threadListVC: UIViewController
let draftListVC: UIViewController
+ let reminderListVC: UIViewController
let currentUserController: CurrentChatUserController
+ let allRemindersListController: MessageReminderListController
+
+ // Events controller for listening to chat events
+ private var eventsController: EventsController!
init(
channelListVC: UIViewController,
threadListVC: UIViewController,
draftListVC: UIViewController,
- currentUserController: CurrentChatUserController
+ reminderListVC: UIViewController,
+ currentUserController: CurrentChatUserController,
+ allRemindersListController: MessageReminderListController
) {
self.channelListVC = channelListVC
self.threadListVC = threadListVC
self.draftListVC = draftListVC
+ self.reminderListVC = reminderListVC
self.currentUserController = currentUserController
+ self.allRemindersListController = allRemindersListController
super.init(nibName: nil, bundle: nil)
}
@@ -52,6 +61,12 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
currentUserController.delegate = self
unreadCount = currentUserController.unreadCount
+ // Update reminders badge if the feature is enabled.
+ if AppConfig.shared.demoAppConfig.isRemindersEnabled {
+ allRemindersListController.delegate = self
+ updateRemindersBadge()
+ }
+
tabBar.backgroundColor = Appearance.default.colorPalette.background
tabBar.isTranslucent = true
@@ -65,14 +80,34 @@ class DemoAppTabBarController: UITabBarController, CurrentChatUserControllerDele
draftListVC.tabBarItem.title = "Drafts"
draftListVC.tabBarItem.image = UIImage(systemName: "bubble.and.pencil")
+
+ reminderListVC.tabBarItem.title = "Reminders"
+ reminderListVC.tabBarItem.image = UIImage(systemName: "bell")
- viewControllers = [channelListVC, threadListVC, draftListVC]
+ // Only show reminders tab if the feature is enabled
+ if AppConfig.shared.demoAppConfig.isRemindersEnabled {
+ viewControllers = [channelListVC, threadListVC, draftListVC, reminderListVC]
+ } else {
+ viewControllers = [channelListVC, threadListVC, draftListVC]
+ }
}
-
+
func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUserUnreadCount: UnreadCount) {
let unreadCount = didChangeCurrentUserUnreadCount
self.unreadCount = unreadCount
let totalUnreadBadge = unreadCount.channels + unreadCount.threads
UIApplication.shared.applicationIconBadgeNumber = totalUnreadBadge
}
+
+ func controller(
+ _ controller: MessageReminderListController,
+ didChangeReminders changes: [ListChange]
+ ) {
+ updateRemindersBadge()
+ }
+
+ private func updateRemindersBadge() {
+ let reminders = allRemindersListController.reminders
+ reminderListVC.tabBarItem.badgeValue = reminders.isEmpty ? nil : "\(reminders.count)"
+ }
}
diff --git a/DemoApp/Screens/DemoReminderListVC.swift b/DemoApp/Screens/DemoReminderListVC.swift
new file mode 100644
index 00000000000..0bca7c2f9e9
--- /dev/null
+++ b/DemoApp/Screens/DemoReminderListVC.swift
@@ -0,0 +1,765 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import StreamChat
+import StreamChatUI
+import UIKit
+
+class DemoReminderListVC: UIViewController, ThemeProvider {
+ var onLogout: (() -> Void)?
+ var onDisconnect: (() -> Void)?
+
+ private let currentUserController: CurrentChatUserController
+
+ private var activeController: MessageReminderListController
+ private var reminders: [MessageReminder] = []
+ private var isPaginatingReminders = false
+
+ private lazy var allRemindersController = FilterOption.all.makeController(client: currentUserController.client)
+ private lazy var upcomingRemindersController = FilterOption.upcoming.makeController(client: currentUserController.client)
+ private lazy var scheduledRemindersController = FilterOption.scheduled.makeController(client: currentUserController.client)
+ private lazy var laterRemindersController = FilterOption.later.makeController(client: currentUserController.client)
+ private lazy var overdueRemindersController = FilterOption.overdue.makeController(client: currentUserController.client)
+
+ private lazy var eventsController = currentUserController.client.eventsController()
+
+ // Timer for refreshing due dates on cells
+ private var refreshTimer: Timer?
+
+ // Filter options
+ enum FilterOption: Int, CaseIterable {
+ case all, overdue, upcoming, scheduled, later
+
+ var title: String {
+ switch self {
+ case .all: return "All"
+ case .scheduled: return "Scheduled"
+ case .overdue: return "Overdue"
+ case .upcoming: return "Upcoming"
+ case .later: return "Saved for later"
+ }
+ }
+
+ var query: MessageReminderListQuery {
+ switch self {
+ case .all:
+ return MessageReminderListQuery()
+ case .scheduled:
+ return MessageReminderListQuery(
+ filter: .withRemindAt,
+ sort: [.init(key: .remindAt, isAscending: true)]
+ )
+ case .later:
+ return MessageReminderListQuery(
+ filter: .withoutRemindAt,
+ sort: [.init(key: .createdAt, isAscending: false)]
+ )
+ case .overdue:
+ return MessageReminderListQuery(
+ filter: .overdue,
+ sort: [.init(key: .remindAt, isAscending: false)]
+ )
+ case .upcoming:
+ return MessageReminderListQuery(
+ filter: .upcoming,
+ sort: [.init(key: .remindAt, isAscending: true)]
+ )
+ }
+ }
+
+ func makeController(client: ChatClient) -> MessageReminderListController {
+ client.messageReminderListController(query: query)
+ }
+ }
+
+ private var selectedFilter: FilterOption = .all {
+ didSet {
+ if oldValue != selectedFilter {
+ switchToController(for: selectedFilter)
+ updateFilterPills()
+ }
+ }
+ }
+
+ 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(DemoReminderCell.self, forCellReuseIdentifier: "DemoReminderCell")
+ return tableView
+ }()
+
+ private lazy var filtersScrollView: UIScrollView = {
+ let scrollView = UIScrollView()
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
+ scrollView.showsHorizontalScrollIndicator = false
+ scrollView.contentInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
+ return scrollView
+ }()
+
+ private lazy var filtersStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+ stackView.axis = .horizontal
+ stackView.spacing = 8
+ stackView.alignment = .center
+ return stackView
+ }()
+
+ private lazy var loadingIndicator: UIActivityIndicatorView = {
+ let indicator = UIActivityIndicatorView(style: .large)
+ indicator.translatesAutoresizingMaskIntoConstraints = false
+ return indicator
+ }()
+
+ private var emptyStateLabel: UILabel = {
+ let label = UILabel()
+ label.textAlignment = .center
+ label.textColor = Appearance.default.colorPalette.subtitleText
+ label.font = Appearance.default.fonts.body
+ return label
+ }()
+
+ private lazy var emptyStateImageView: UIImageView = {
+ let imageView = UIImageView(image: UIImage(systemName: "bell.slash"))
+ imageView.contentMode = .scaleAspectFit
+ imageView.tintColor = Appearance.default.colorPalette.subtitleText
+ return imageView
+ }()
+
+ private lazy var emptyStateView: UIView = {
+ VContainer(spacing: 12, alignment: .center) {
+ emptyStateImageView
+ .width(48)
+ .height(48)
+ emptyStateLabel
+ }
+ }()
+
+ init(currentUserController: CurrentChatUserController) {
+ self.currentUserController = currentUserController
+ activeController = currentUserController.client.messageReminderListController()
+
+ 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 = "Reminders"
+
+ eventsController.delegate = self
+
+ userAvatarView.controller = currentUserController
+ userAvatarView.addTarget(self, action: #selector(didTapOnCurrentUserAvatar), for: .touchUpInside)
+ userAvatarView.translatesAutoresizingMaskIntoConstraints = false
+
+ navigationItem.backButtonTitle = ""
+ navigationItem.leftBarButtonItem = UIBarButtonItem(customView: userAvatarView)
+
+ setupViews()
+ setupFilterPills()
+ updateEmptyStateMessage()
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ loadReminders()
+ startRefreshTimer()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ stopRefreshTimer()
+ }
+
+ private func startRefreshTimer() {
+ // Cancel any existing timer first
+ stopRefreshTimer()
+
+ // Create a new timer that fires every 60 seconds
+ refreshTimer = Timer.scheduledTimer(
+ timeInterval: 60.0,
+ target: self,
+ selector: #selector(refreshVisibleCells),
+ userInfo: nil,
+ repeats: true
+ )
+
+ // Add to RunLoop to ensure it works while scrolling
+ if let timer = refreshTimer {
+ RunLoop.current.add(timer, forMode: .common)
+ }
+ }
+
+ private func stopRefreshTimer() {
+ refreshTimer?.invalidate()
+ refreshTimer = nil
+ }
+
+ @objc private func refreshVisibleCells() {
+ // Only refresh visible cells to avoid unnecessary work
+ guard let visibleIndexPaths = tableView.indexPathsForVisibleRows else { return }
+
+ for indexPath in visibleIndexPaths {
+ if indexPath.row < reminders.count,
+ let cell = tableView.cellForRow(at: indexPath) as? DemoReminderCell {
+ let reminder = reminders[indexPath.row]
+ cell.configure(with: reminder)
+ }
+ }
+ }
+
+ private func setupViews() {
+ view.backgroundColor = Appearance.default.colorPalette.background
+ tableView.backgroundColor = Appearance.default.colorPalette.background
+
+ let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 50))
+ headerView.backgroundColor = Appearance.default.colorPalette.background
+ headerView.addSubview(filtersScrollView)
+ filtersScrollView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ filtersScrollView.topAnchor.constraint(equalTo: headerView.topAnchor),
+ filtersScrollView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
+ filtersScrollView.trailingAnchor.constraint(equalTo: headerView.trailingAnchor),
+ filtersScrollView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor)
+ ])
+
+ filtersScrollView.addSubview(filtersStackView)
+ NSLayoutConstraint.activate([
+ filtersStackView.topAnchor.constraint(equalTo: filtersScrollView.topAnchor),
+ filtersStackView.leadingAnchor.constraint(equalTo: filtersScrollView.leadingAnchor),
+ filtersStackView.trailingAnchor.constraint(equalTo: filtersScrollView.trailingAnchor),
+ filtersStackView.bottomAnchor.constraint(equalTo: filtersScrollView.bottomAnchor),
+ filtersStackView.heightAnchor.constraint(equalTo: filtersScrollView.heightAnchor)
+ ])
+
+ view.addSubview(tableView)
+ tableView.tableHeaderView = headerView
+ tableView.addSubview(emptyStateView)
+ tableView.addSubview(loadingIndicator)
+
+ NSLayoutConstraint.activate([
+ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+
+ emptyStateView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
+ emptyStateView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor),
+ emptyStateView.widthAnchor.constraint(equalTo: tableView.widthAnchor),
+ emptyStateView.heightAnchor.constraint(equalToConstant: 100),
+
+ loadingIndicator.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
+ loadingIndicator.centerYAnchor.constraint(equalTo: tableView.centerYAnchor)
+ ])
+ }
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+
+ // Update header view width when view size changes
+ if let headerView = tableView.tableHeaderView {
+ let width = tableView.bounds.width
+ var frame = headerView.frame
+
+ // Only update if width changed
+ if frame.width != width {
+ frame.size.width = width
+ headerView.frame = frame
+ tableView.tableHeaderView = headerView
+ }
+ }
+ }
+
+ private func setupFilterPills() {
+ for filterOption in FilterOption.allCases {
+ let pillButton = createFilterPillButton(for: filterOption)
+ filtersStackView.addArrangedSubview(pillButton)
+ }
+ updateFilterPills()
+ }
+
+ private func createFilterPillButton(for filterOption: FilterOption) -> UIButton {
+ let button = UIButton(type: .system)
+ button.tag = filterOption.rawValue
+ button.setTitle(filterOption.title, for: .normal)
+ button.titleLabel?.font = Appearance.default.fonts.footnote
+ button.layer.cornerRadius = 12
+ button.layer.masksToBounds = true
+ button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
+ button.addTarget(self, action: #selector(didTapFilterPill), for: .touchUpInside)
+ return button
+ }
+
+ private func updateFilterPills() {
+ for subview in filtersStackView.arrangedSubviews {
+ guard let button = subview as? UIButton else { continue }
+
+ if button.tag == selectedFilter.rawValue {
+ button.backgroundColor = Appearance.default.colorPalette.accentPrimary
+ button.setTitleColor(.white, for: .normal)
+ } else {
+ button.backgroundColor = Appearance.default.colorPalette.background2
+ button.setTitleColor(Appearance.default.colorPalette.text, for: .normal)
+ }
+ }
+
+ // Update empty state message when filter changes
+ updateEmptyStateMessage()
+ }
+
+ private func updateEmptyStateMessage() {
+ switch selectedFilter {
+ case .all:
+ emptyStateLabel.text = "No reminders"
+ emptyStateImageView.image = UIImage(systemName: "bell.slash")
+ case .scheduled:
+ emptyStateLabel.text = "No scheduled reminders"
+ emptyStateImageView.image = UIImage(systemName: "bell.slash")
+ case .later:
+ emptyStateLabel.text = "No saved for later"
+ emptyStateImageView.image = UIImage(systemName: "bookmark.slash")
+ case .overdue:
+ emptyStateLabel.text = "No overdue reminders"
+ emptyStateImageView.image = UIImage(systemName: "bell.slash")
+ case .upcoming:
+ emptyStateLabel.text = "No upcoming reminders"
+ emptyStateImageView.image = UIImage(systemName: "bell.slash")
+ }
+ }
+
+ @objc private func didTapFilterPill(_ sender: UIButton) {
+ guard let filterOption = FilterOption(rawValue: sender.tag) else { return }
+ selectedFilter = filterOption
+ }
+
+ private func switchToController(for filter: FilterOption) {
+ switch filter {
+ case .all:
+ activeController = allRemindersController
+ case .overdue:
+ activeController = overdueRemindersController
+ case .upcoming:
+ activeController = upcomingRemindersController
+ case .scheduled:
+ activeController = scheduledRemindersController
+ case .later:
+ activeController = laterRemindersController
+ }
+ activeController.delegate = self
+
+ // Only load reminders if this controller hasn't loaded any yet
+ if activeController.reminders.isEmpty && !activeController.hasLoadedAllReminders {
+ loadReminders()
+ } else {
+ // Otherwise just update the UI with existing data
+ updateRemindersData()
+ }
+ }
+
+ private func loadReminders() {
+ let controller = activeController
+ controller.delegate = self
+
+ if reminders.isEmpty {
+ loadingIndicator.startAnimating()
+ emptyStateView.isHidden = true
+ }
+
+ controller.synchronize { [weak self] _ in
+ self?.loadingIndicator.stopAnimating()
+ self?.updateRemindersData()
+ }
+ }
+
+ private func loadMoreReminders() {
+ let controller = activeController
+ guard !isPaginatingReminders && !controller.hasLoadedAllReminders else {
+ return
+ }
+
+ isPaginatingReminders = true
+ controller.loadMoreReminders { [weak self] _ in
+ self?.isPaginatingReminders = false
+ }
+ }
+
+ @objc private func didTapOnCurrentUserAvatar(_ sender: Any) {
+ presentUserOptionsAlert(
+ onLogout: onLogout,
+ onDisconnect: onDisconnect,
+ client: currentUserController.client
+ )
+ }
+
+ private func showEditReminderOptions(for reminder: MessageReminder, at indexPath: IndexPath) {
+ let alert = UIAlertController(title: "Edit Reminder", message: nil, preferredStyle: .actionSheet)
+
+ alert.addAction(UIAlertAction(title: "Remind in 1 Minutes", style: .default) { [weak self] _ in
+ let date = Date().addingTimeInterval(1.05 * 60)
+ self?.updateReminderDate(for: reminder, newDate: date)
+ })
+
+ alert.addAction(UIAlertAction(title: "Remind in 1 Hour", style: .default) { [weak self] _ in
+ let date = Date().addingTimeInterval(60 * 60)
+ self?.updateReminderDate(for: reminder, newDate: date)
+ })
+
+ alert.addAction(UIAlertAction(title: "Remind tomorrow", style: .default) { [weak self] _ in
+ let date = Date().addingTimeInterval(24 * 60 * 60)
+ self?.updateReminderDate(for: reminder, newDate: date)
+ })
+
+ alert.addAction(UIAlertAction(title: "Clear due date", style: .default) { [weak self] _ in
+ self?.updateReminderDate(for: reminder, newDate: nil)
+ })
+
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
+
+ if let popoverController = alert.popoverPresentationController {
+ if let cell = tableView.cellForRow(at: indexPath) {
+ popoverController.sourceView = cell
+ popoverController.sourceRect = cell.bounds
+ }
+ }
+
+ present(alert, animated: true)
+ }
+
+ private func updateReminderDate(for reminder: MessageReminder, newDate: Date?) {
+ let messageController = currentUserController.client.messageController(
+ cid: reminder.channel.cid,
+ messageId: reminder.message.id
+ )
+
+ messageController.updateReminder(remindAt: newDate)
+ }
+
+ private func showErrorAlert(message: String) {
+ let alert = UIAlertController(
+ title: "Error",
+ message: message,
+ preferredStyle: .alert
+ )
+
+ alert.addAction(UIAlertAction(title: "OK", style: .default))
+ present(alert, animated: true)
+ }
+
+ private func updateRemindersData() {
+ reminders = Array(activeController.reminders)
+ tableView.reloadData()
+ updateEmptyStateMessage()
+ emptyStateView.isHidden = !reminders.isEmpty
+ }
+}
+
+// MARK: - MessageReminderListControllerDelegate
+
+extension DemoReminderListVC: MessageReminderListControllerDelegate, EventsControllerDelegate {
+ func controller(
+ _ controller: MessageReminderListController,
+ didChangeReminders changes: [ListChange]
+ ) {
+ // Only update UI if this is the active controller
+ guard controller === activeController else { return }
+ updateRemindersData()
+ }
+
+ func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
+ if event is MessageReminderDueEvent {
+ updateReminderListsWithNewNowDate()
+ }
+ }
+
+ /// Update the reminder lists with the new current date.
+ /// When the controllers are created, they use the current date to query the reminders.
+ /// When a reminder is due, we need to re-create the queries with the new current date.
+ /// Otherwise, the reminders will not be updated since the current date will be outdated.
+ private func updateReminderListsWithNewNowDate() {
+ upcomingRemindersController = FilterOption.upcoming.makeController(client: currentUserController.client)
+ overdueRemindersController = FilterOption.overdue.makeController(client: currentUserController.client)
+ scheduledRemindersController = FilterOption.scheduled.makeController(client: currentUserController.client)
+ if selectedFilter == .upcoming {
+ activeController = upcomingRemindersController
+ } else if selectedFilter == .overdue {
+ activeController = overdueRemindersController
+ } else if selectedFilter == .scheduled {
+ activeController = scheduledRemindersController
+ } else {
+ return
+ }
+ activeController.delegate = self
+ updateRemindersData()
+ }
+}
+
+// MARK: - UITableViewDataSource & UITableViewDelegate
+
+extension DemoReminderListVC: UITableViewDataSource, UITableViewDelegate {
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ reminders.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "DemoReminderCell", for: indexPath) as? DemoReminderCell
+ let reminder = reminders[indexPath.row]
+ cell?.configure(with: reminder)
+ return cell ?? .init()
+ }
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ // Only react to table view scrolling, not the filter scroll view
+ guard scrollView == tableView else { return }
+
+ let threshold: CGFloat = 100
+ let contentOffset = scrollView.contentOffset.y
+ let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
+
+ if maximumOffset - contentOffset <= threshold {
+ loadMoreReminders()
+ }
+ }
+
+ func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ let reminder = reminders[indexPath.row]
+
+ let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completion in
+ guard let self = self else { return }
+
+ let messageController = self.currentUserController.client.messageController(
+ cid: reminder.channel.cid,
+ messageId: reminder.message.id
+ )
+
+ messageController.deleteReminder { error in
+ if let error = error {
+ self.showErrorAlert(message: "Failed to delete reminder: \(error.localizedDescription)")
+ completion(false)
+ } else {
+ completion(true)
+ }
+ }
+ }
+
+ let editAction = UIContextualAction(style: .normal, title: "Edit") { [weak self] _, _, completion in
+ guard let self = self else { return }
+ self.showEditReminderOptions(for: reminder, at: indexPath)
+ completion(true)
+ }
+ editAction.backgroundColor = .systemBlue
+
+ return UISwipeActionsConfiguration(actions: [deleteAction, editAction])
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let reminder = reminders[indexPath.row]
+ let channelController = currentUserController.client.channelController(
+ for: ChannelQuery(
+ cid: reminder.channel.cid,
+ paginationParameter: .around(reminder.message.id)
+ )
+ )
+
+ let channelVC = DemoChatChannelVC()
+ channelVC.channelController = channelController
+ navigationController?.pushViewController(channelVC, animated: true)
+ }
+}
+
+// MARK: - Reminder Cell
+
+class DemoReminderCell: 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 dueDateContainer: UIView = {
+ let view = UIView()
+ view.layer.cornerRadius = 12
+ view.layer.masksToBounds = true
+ return view
+ }()
+
+ private let dueDateLabel: UILabel = {
+ let label = UILabel()
+ label.translatesAutoresizingMaskIntoConstraints = false
+ label.font = Appearance.default.fonts.footnoteBold
+ label.textColor = Appearance.default.colorPalette.staticColorText
+ label.textAlignment = .center
+ return label
+ }()
+
+ private let saveForLaterIconView: UIImageView = {
+ let imageView = UIImageView(image: UIImage(systemName: "bookmark.fill"))
+ imageView.contentMode = .scaleAspectFit
+ imageView.tintColor = Appearance.default.colorPalette.accentPrimary
+ 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() {
+ dueDateContainer.addSubview(dueDateLabel)
+ dueDateContainer.setContentCompressionResistancePriority(.required, for: .horizontal)
+ dueDateLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
+ messageLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+
+ NSLayoutConstraint.activate([
+ dueDateLabel.topAnchor.constraint(equalTo: dueDateContainer.topAnchor, constant: 4),
+ dueDateLabel.leadingAnchor.constraint(equalTo: dueDateContainer.leadingAnchor, constant: 8),
+ dueDateLabel.trailingAnchor.constraint(equalTo: dueDateContainer.trailingAnchor, constant: -8),
+ dueDateLabel.bottomAnchor.constraint(equalTo: dueDateContainer.bottomAnchor, constant: -4)
+ ])
+
+ VContainer(spacing: 8) {
+ HContainer(spacing: 4) {
+ channelNameLabel
+ Spacer()
+ saveForLaterIconView
+ dueDateContainer
+ }
+ messageLabel
+ .height(20)
+ }.embed(
+ in: contentView,
+ insets: .init(top: 8, leading: 16, bottom: 8, trailing: 16)
+ )
+ }
+
+ func configure(with reminder: MessageReminder) {
+ let channelName = Appearance.default.formatters.channelName.format(
+ channel: reminder.channel,
+ forCurrentUserId: StreamChatWrapper.shared.client?.currentUserId
+ ) ?? ""
+
+ if reminder.message.parentMessageId != nil {
+ channelNameLabel.text = "Thread in # \(channelName)"
+ } else {
+ channelNameLabel.text = "# \(channelName)"
+ }
+
+ if reminder.message.text.isEmpty {
+ let attachmentType = reminder.message.allAttachments.first?.type.rawValue.capitalized ?? ""
+ messageLabel.text = "š \(attachmentType)"
+ } else {
+ messageLabel.text = reminder.message.text
+ }
+
+ // Configure based on reminder type
+ if let remindAt = reminder.remindAt {
+ // Check if reminder is overdue
+ let now = Date()
+ if remindAt < now {
+ let timeInterval = now.timeIntervalSince(remindAt)
+ dueDateLabel.text = formatOverdueTime(timeInterval: timeInterval)
+ dueDateContainer.backgroundColor = Appearance.default.colorPalette.alert
+ } else {
+ let timeInterval = remindAt.timeIntervalSince(now)
+ dueDateLabel.text = "Due in \(formatDueTime(timeInterval: timeInterval))"
+ dueDateContainer.backgroundColor = Appearance.default.colorPalette.accentPrimary
+ }
+ dueDateContainer.isHidden = false
+ saveForLaterIconView.isHidden = true
+ } else {
+ saveForLaterIconView.isHidden = false
+ dueDateContainer.isHidden = true
+ }
+ }
+
+ private func formatOverdueTime(timeInterval: TimeInterval) -> String {
+ // Round to the nearest minute (30 seconds or more rounds up)
+ let roundedMinutes = ceil(timeInterval / 60 - 0.5)
+ let roundedInterval = roundedMinutes * 60
+
+ // If less than a minute, show "1 min" instead of "0 min"
+ if roundedInterval == 0 {
+ return "Overdue by 1 min"
+ }
+
+ let formatter = DateComponentsFormatter()
+
+ if roundedInterval < 3600 {
+ // For durations less than an hour, show only minutes
+ formatter.allowedUnits = [.minute]
+ formatter.unitsStyle = .abbreviated
+ formatter.maximumUnitCount = 1
+ } else {
+ // For longer durations, show days and hours, or hours and minutes
+ formatter.allowedUnits = [.day, .hour, .minute]
+ formatter.unitsStyle = .abbreviated
+ formatter.maximumUnitCount = 2
+ }
+
+ guard let formattedString = formatter.string(from: roundedInterval) else {
+ return "Overdue"
+ }
+
+ return "Overdue by \(formattedString)"
+ }
+
+ private func formatDueTime(timeInterval: TimeInterval) -> String {
+ // Round to the nearest minute (30 seconds or more rounds up)
+ let roundedMinutes = ceil(timeInterval / 60 - 0.5)
+ let roundedInterval = roundedMinutes * 60
+
+ // If less than a minute, show "1 min" instead of "0 min"
+ if roundedInterval == 0 {
+ return "1m"
+ }
+
+ let formatter = DateComponentsFormatter()
+
+ if roundedInterval < 3600 {
+ // For durations less than an hour, show only minutes
+ formatter.allowedUnits = [.minute]
+ formatter.unitsStyle = .abbreviated
+ formatter.maximumUnitCount = 1
+ } else {
+ // For longer durations, show days and hours, or hours and minutes
+ formatter.allowedUnits = [.day, .hour, .minute]
+ formatter.unitsStyle = .abbreviated
+ formatter.maximumUnitCount = 2
+ }
+
+ guard let formattedString = formatter.string(from: roundedInterval) else {
+ return "soon"
+ }
+
+ return formattedString
+ }
+}
diff --git a/DemoApp/Shared/DemoUsers.swift b/DemoApp/Shared/DemoUsers.swift
index a637e47b609..22b380010e4 100644
--- a/DemoApp/Shared/DemoUsers.swift
+++ b/DemoApp/Shared/DemoUsers.swift
@@ -257,18 +257,27 @@ struct DemoApiKeys: RawRepresentable, Equatable, Hashable {
}
static let frankfurtC1: DemoApiKeys = .init(rawValue: "8br4watad788") // UIKit default
- static let frankfurtC2: DemoApiKeys = .init(rawValue: "pd67s34fzpgw")
+ static let frankfurtC2: DemoApiKeys = .init(rawValue: "pd67s34fzpgw") // Frankfurt C2 Staging
static let usEastC6: DemoApiKeys = .init(rawValue: "zcgvnykxsfm8") // SwiftUI default
var appName: String? {
switch self {
case .frankfurtC1: return "UIKit"
- case .frankfurtC2: return nil
+ case .frankfurtC2: return "Frankfurt C2 Staging"
case .usEastC6: return "SwiftUI"
default: return nil
}
}
+ var customBaseURL: URL? {
+ switch self {
+ case .frankfurtC1: return nil
+ case .frankfurtC2: return URL(string: "https://chat-edge-frankfurt-ce1.stream-io-api.com/")
+ case .usEastC6: return nil
+ default: return nil
+ }
+ }
+
static func ~= (pattern: DemoApiKeys, value: DemoApiKeys) -> Bool {
value.rawValue == pattern.rawValue
}
diff --git a/DemoApp/Shared/StreamChatWrapper.swift b/DemoApp/Shared/StreamChatWrapper.swift
index c113aa538e8..2589941a864 100644
--- a/DemoApp/Shared/StreamChatWrapper.swift
+++ b/DemoApp/Shared/StreamChatWrapper.swift
@@ -37,6 +37,11 @@ final class StreamChatWrapper {
config.shouldShowShadowedMessages = true
config.applicationGroupIdentifier = applicationGroupIdentifier
config.urlSessionConfiguration.httpAdditionalHeaders = ["Custom": "Example"]
+
+ let apiKey = DemoApiKeys(rawValue: apiKeyString)
+ if let baseURL = apiKey.customBaseURL {
+ config.baseURL = .init(url: baseURL)
+ }
// Uncomment this to test model transformers
// config.modelsTransformer = CustomStreamModelsTransformer()
configureUI()
diff --git a/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift b/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
index e6daf818060..aee4601e71f 100644
--- a/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
+++ b/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
@@ -20,6 +20,13 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
if message?.isBounced == false {
actions.append(pinMessageActionItem())
actions.append(translateActionItem())
+
+ let isDemoAppRemindersEnabled = AppConfig.shared.demoAppConfig.isRemindersEnabled
+ let isChannelRemindersEnabled = channel?.config.messageRemindersEnabled ?? false
+ if isDemoAppRemindersEnabled && isChannelRemindersEnabled {
+ actions.append(reminderActionItem())
+ actions.append(saveForLaterActionItem())
+ }
}
if AppConfig.shared.demoAppConfig.isMessageDebuggerEnabled {
@@ -109,6 +116,67 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
)
}
+ func reminderActionItem() -> ChatMessageActionItem {
+ let hasReminder = message?.reminder != nil
+ return ReminderActionItem(
+ hasReminder: hasReminder,
+ action: { [weak self] _ in
+ guard let self = self else { return }
+
+ let alertController = UIAlertController(
+ title: "Select Reminder Time",
+ message: "When would you like to be reminded?",
+ preferredStyle: .alert
+ )
+
+ let actions = [
+ UIAlertAction(title: "1 Minute", style: .default) { _ in
+ let remindAt = Date().addingTimeInterval(61)
+ self.updateOrCreateReminder(remindAt: remindAt)
+ },
+ UIAlertAction(title: "30 Minutes", style: .default) { _ in
+ let remindAt = Date().addingTimeInterval(30 * 60)
+ self.updateOrCreateReminder(remindAt: remindAt)
+ },
+ UIAlertAction(title: "1 Hour", style: .default) { _ in
+ let remindAt = Date().addingTimeInterval(60 * 60)
+ self.updateOrCreateReminder(remindAt: remindAt)
+ },
+ UIAlertAction(title: "Cancel", style: .cancel) { _ in
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
+ ]
+ actions.forEach { alertController.addAction($0) }
+ self.present(alertController, animated: true)
+ }
+ )
+ }
+
+ private func updateOrCreateReminder(remindAt: Date) {
+ if message?.reminder != nil {
+ messageController.updateReminder(remindAt: remindAt)
+ } else {
+ messageController.createReminder(remindAt: remindAt)
+ }
+ delegate?.chatMessageActionsVCDidFinish(self)
+ }
+
+ func saveForLaterActionItem() -> ChatMessageActionItem {
+ let hasReminder = message?.reminder != nil
+ return SaveForLaterActionItem(
+ hasReminder: message?.reminder != nil,
+ action: { [weak self] _ in
+ guard let self = self else { return }
+ if hasReminder {
+ messageController.deleteReminder()
+ } else {
+ messageController.createReminder()
+ }
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
+ )
+ }
+
func messageDebugActionItem() -> ChatMessageActionItem {
MessageDebugActionItem { [weak self] _ in
guard let message = self?.message else { return }
@@ -171,4 +239,44 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
var icon: UIImage { UIImage(systemName: "ladybug")! }
var action: (ChatMessageActionItem) -> Void
}
+
+ struct ReminderActionItem: ChatMessageActionItem {
+ var title: String
+ var isDestructive: Bool { false }
+ let icon: UIImage
+ let action: (ChatMessageActionItem) -> Void
+
+ init(
+ hasReminder: Bool,
+ action: @escaping (ChatMessageActionItem) -> Void
+ ) {
+ title = hasReminder ? "Update Reminder" : "Remind Me"
+ self.action = action
+ if hasReminder {
+ icon = UIImage(systemName: "clock.badge.checkmark") ?? .init()
+ } else {
+ icon = UIImage(systemName: "clock") ?? .init()
+ }
+ }
+ }
+
+ struct SaveForLaterActionItem: ChatMessageActionItem {
+ var title: String
+ var isDestructive: Bool { false }
+ let icon: UIImage
+ let action: (ChatMessageActionItem) -> Void
+
+ init(
+ hasReminder: Bool,
+ action: @escaping (ChatMessageActionItem) -> Void
+ ) {
+ title = hasReminder ? "Remove from later" : "Save for later"
+ self.action = action
+ if hasReminder {
+ icon = UIImage(systemName: "bookmark.fill") ?? .init()
+ } else {
+ icon = UIImage(systemName: "bookmark") ?? .init()
+ }
+ }
+ }
}
diff --git a/DemoApp/StreamChat/Components/DemoChatMessageContentView.swift b/DemoApp/StreamChat/Components/DemoChatMessageContentView.swift
index b40c715bab3..3aa07dab9eb 100644
--- a/DemoApp/StreamChat/Components/DemoChatMessageContentView.swift
+++ b/DemoApp/StreamChat/Components/DemoChatMessageContentView.swift
@@ -9,6 +9,33 @@ import UIKit
final class DemoChatMessageContentView: ChatMessageContentView {
var pinInfoLabel: UILabel?
+ lazy var saveForLaterView: UIView = {
+ HContainer(spacing: 4) {
+ saveForLaterIcon
+ .height(12)
+ .width(12)
+ saveForLaterLabel
+ .height(30)
+ }
+ }()
+
+ lazy var saveForLaterIcon: UIImageView = {
+ let imageView = UIImageView()
+ imageView.contentMode = .scaleAspectFit
+ imageView.image = UIImage(systemName: "bookmark.fill")
+ imageView.tintColor = appearance.colorPalette.accentPrimary
+ return imageView
+ }()
+
+ lazy var saveForLaterLabel: UILabel = {
+ let label = UILabel()
+ label.text = "Saved for later"
+ label.translatesAutoresizingMaskIntoConstraints = false
+ label.font = appearance.fonts.footnote
+ label.textColor = appearance.colorPalette.accentPrimary
+ return label
+ }()
+
override func layout(options: ChatMessageLayoutOptions) {
super.layout(options: options)
@@ -19,6 +46,15 @@ final class DemoChatMessageContentView: ChatMessageContentView {
pinInfoLabel?.textColor = appearance.colorPalette.textLowEmphasis
bubbleThreadFootnoteContainer.insertArrangedSubview(pinInfoLabel!, at: 0)
}
+
+ if options.contains(.saveForLaterInfo) {
+ backgroundColor = appearance.colorPalette.highlightedAccentBackground1
+ bubbleThreadFootnoteContainer.insertArrangedSubview(saveForLaterView, at: 0)
+ saveForLaterView.topAnchor.constraint(
+ equalTo: bubbleThreadFootnoteContainer.topAnchor,
+ constant: 4
+ ).isActive = true
+ }
}
override func updateContent() {
diff --git a/DemoApp/StreamChat/Components/DemoChatMessageLayoutOptionsResolver.swift b/DemoApp/StreamChat/Components/DemoChatMessageLayoutOptionsResolver.swift
index 5c5399fd2e9..7c0be2d7f8e 100644
--- a/DemoApp/StreamChat/Components/DemoChatMessageLayoutOptionsResolver.swift
+++ b/DemoApp/StreamChat/Components/DemoChatMessageLayoutOptionsResolver.swift
@@ -8,6 +8,7 @@ import StreamChatUI
extension ChatMessageLayoutOption {
static let pinInfo: Self = "pinInfo"
+ static let saveForLaterInfo: Self = "saveForLaterInfo"
}
final class DemoChatMessageLayoutOptionsResolver: ChatMessageLayoutOptionsResolver {
@@ -28,6 +29,10 @@ final class DemoChatMessageLayoutOptionsResolver: ChatMessageLayoutOptionsResolv
options.insert(.pinInfo)
}
+ if AppConfig.shared.demoAppConfig.isRemindersEnabled && message.reminder != nil {
+ options.insert(.saveForLaterInfo)
+ }
+
return options
}
}
diff --git a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
index e63244d3ff0..604b0ab4278 100644
--- a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
+++ b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
@@ -57,12 +57,24 @@ extension DemoAppCoordinator {
draftsVC.onDisconnect = { [weak self] in
self?.disconnect()
}
+
+ let reminderListVC = DemoReminderListVC(
+ currentUserController: client.currentUserController()
+ )
+ reminderListVC.onLogout = { [weak self] in
+ self?.logOut()
+ }
+ reminderListVC.onDisconnect = { [weak self] in
+ self?.disconnect()
+ }
let tabBarViewController = DemoAppTabBarController(
channelListVC: chatVC,
threadListVC: UINavigationController(rootViewController: threadListVC),
draftListVC: UINavigationController(rootViewController: draftsVC),
- currentUserController: client.currentUserController()
+ reminderListVC: UINavigationController(rootViewController: reminderListVC),
+ currentUserController: client.currentUserController(),
+ allRemindersListController: client.messageReminderListController()
)
set(rootViewController: tabBarViewController, animated: animated)
DemoAppConfiguration.showPerformanceTracker()
diff --git a/DemoAppPush/NotificationService.swift b/DemoAppPush/NotificationService.swift
index 9ad2d8dede1..0b0b0488236 100644
--- a/DemoAppPush/NotificationService.swift
+++ b/DemoAppPush/NotificationService.swift
@@ -96,15 +96,24 @@ class NotificationService: UNNotificationServiceExtension {
let chatNotification = chatHandler.handleNotification { chatContent in
switch chatContent {
case let .message(messageNotification):
- content
- .title = (messageNotification.message.author.name ?? "somebody") +
- (" on \(messageNotification.channel?.name ?? "a conversation with you")")
+ if messageNotification.type == .reminderDue {
+ return contentHandler(content)
+ }
+
+ let authorName = messageNotification.message.author.name ?? "somebody"
+ let channelName = messageNotification.channel?.name ?? "a conversation with you"
+ content.title = "\(authorName) on \(channelName)"
content.subtitle = ""
content.body = messageNotification.message.text
self.addMessageAttachments(message: messageNotification.message, content: content) {
contentHandler($0)
}
default:
+ let streamPayload = content.userInfo["stream"] as? [String: String]
+ if streamPayload?["type"] == EventType.messageReminderDue.rawValue {
+ contentHandler(content)
+ return
+ }
content.title = "You received an update to one conversation"
contentHandler(content)
}
diff --git a/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift b/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift
index 35c9854143c..d3f0abd57a1 100644
--- a/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift
+++ b/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift
@@ -9,13 +9,39 @@ import UserNotifications
public class MessageNotificationContent {
public let message: ChatMessage
public let channel: ChatChannel?
+ public let type: PushNotificationType
- init(message: ChatMessage, channel: ChatChannel?) {
+ init(
+ message: ChatMessage,
+ channel: ChatChannel?,
+ type: PushNotificationType
+ ) {
self.message = message
self.channel = channel
+ self.type = type
}
}
+public struct PushNotificationType: Equatable {
+ public var name: String
+
+ init(name: String) {
+ self.name = name
+ }
+
+ init?(eventType: EventType) {
+ switch eventType {
+ case .messageNew, .messageReminderDue:
+ self.init(name: eventType.rawValue)
+ default:
+ return nil
+ }
+ }
+
+ public static var newMessage: PushNotificationType = .init(name: EventType.messageNew.rawValue)
+ public static var reminderDue: PushNotificationType = .init(name: EventType.messageReminderDue.rawValue)
+}
+
public class UnknownNotificationContent {
public let content: UNNotificationContent
@@ -98,20 +124,26 @@ public class ChatRemoteNotificationHandler {
return completion(.unknown(UnknownNotificationContent(content: content)))
}
- if EventType(rawValue: type) == .messageNew {
- guard let cid = dict["cid"], let id = dict["id"], let channelId = try? ChannelId(cid: cid) else {
- completion(.unknown(UnknownNotificationContent(content: content)))
+ guard let pushType = PushNotificationType(eventType: EventType(rawValue: type)) else {
+ return completion(.unknown(UnknownNotificationContent(content: content)))
+ }
+
+ guard let cid = dict["cid"], let id = dict["id"], let channelId = try? ChannelId(cid: cid) else {
+ completion(.unknown(UnknownNotificationContent(content: content)))
+ return
+ }
+
+ getContent(cid: channelId, messageId: id) { message, channel in
+ guard let message = message else {
+ completion(.unknown(UnknownNotificationContent(content: self.content)))
return
}
- getContent(cid: channelId, messageId: id) { message, channel in
- guard let message = message else {
- completion(.unknown(UnknownNotificationContent(content: self.content)))
- return
- }
- completion(.message(MessageNotificationContent(message: message, channel: channel)))
- }
- } else {
- completion(.unknown(UnknownNotificationContent(content: content)))
+ let content = MessageNotificationContent(
+ message: message,
+ channel: channel,
+ type: pushType
+ )
+ completion(.message(content))
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
index 232ab780388..8aa9624a78b 100644
--- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
@@ -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, .drafts:
+ .unread, .blockUser, .unblockUser, .drafts, .reminders, .reminder:
return false
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
index 8a31cc30a76..66f902cd124 100644
--- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
@@ -55,6 +55,10 @@ enum EndpointPath: Codable {
case drafts
case draftMessage(ChannelId)
+ // Reminders
+ case reminders
+ case reminder(MessageId)
+
case banMember
case flagUser(Bool)
case flagMessage(Bool)
@@ -135,6 +139,9 @@ enum EndpointPath: Codable {
case .drafts: return "drafts/query"
case let .draftMessage(channelId): return "channels/\(channelId.apiPath)/draft"
+ case .reminders: return "reminders/query"
+ case let .reminder(messageId): return "messages/\(messageId)/reminders"
+
case .banMember: return "moderation/ban"
case let .flagUser(flag): return "moderation/\(flag ? "flag" : "unflag")"
case let .flagMessage(flag): return "moderation/\(flag ? "flag" : "unflag")"
@@ -151,9 +158,9 @@ enum EndpointPath: Codable {
case let .poll(pollId: pollId): return "polls/\(pollId)"
case let .pollOption(pollId: pollId, optionId: optionId): return "polls/\(pollId)/options/\(optionId)"
case let .pollOptions(pollId: pollId): return "polls/\(pollId)/options"
+ case let .pollVotes(pollId: pollId): return "polls/\(pollId)/votes"
case let .pollVoteInMessage(messageId: messageId, pollId: pollId): return "messages/\(messageId)/polls/\(pollId)/vote"
case let .pollVote(messageId: messageId, pollId: pollId, voteId: voteId): return "messages/\(messageId)/polls/\(pollId)/vote/\(voteId)"
- case let .pollVotes(pollId: pollId): return "polls/\(pollId)/votes"
}
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
index 2d365f39cfb..ea0a43a87c1 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
@@ -232,6 +232,7 @@ public class ChannelConfig: Codable {
case createdAt = "created_at"
case updatedAt = "updated_at"
case skipLastMsgAtUpdateForSystemMsg = "skip_last_msg_update_for_system_msgs"
+ case messageRemindersEnabled = "user_message_reminders"
}
/// If users are allowed to add reactions to messages. Enabled by default.
@@ -268,6 +269,8 @@ public class ChannelConfig: Codable {
public let pollsEnabled: Bool
/// Determines if system messages should not update the last message at date.
public let skipLastMsgAtUpdateForSystemMsg: Bool
+ /// Determines if user message reminders are enabled.
+ public let messageRemindersEnabled: Bool
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@@ -293,6 +296,7 @@ public class ChannelConfig: Codable {
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
pollsEnabled = try container.decodeIfPresent(Bool.self, forKey: .pollsEnabled) ?? false
skipLastMsgAtUpdateForSystemMsg = try container.decodeIfPresent(Bool.self, forKey: .skipLastMsgAtUpdateForSystemMsg) ?? false
+ messageRemindersEnabled = try container.decodeIfPresent(Bool.self, forKey: .messageRemindersEnabled) ?? false
}
internal required init(
@@ -308,6 +312,7 @@ public class ChannelConfig: Codable {
pollsEnabled: Bool = false,
urlEnrichmentEnabled: Bool = false,
skipLastMsgAtUpdateForSystemMsg: Bool = false,
+ messageRemindersEnabled: Bool = false,
messageRetention: String = "",
maxMessageLength: Int = 0,
commands: [Command] = [],
@@ -331,5 +336,6 @@ public class ChannelConfig: Codable {
self.updatedAt = updatedAt
self.pollsEnabled = pollsEnabled
self.skipLastMsgAtUpdateForSystemMsg = skipLastMsgAtUpdateForSystemMsg
+ self.messageRemindersEnabled = messageRemindersEnabled
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
index 9a65842859b..8dc929e6f86 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
@@ -54,6 +54,7 @@ enum MessagePayloadsCodingKeys: String, CodingKey, CaseIterable {
case skipEnrichUrl = "skip_enrich_url"
case restrictedVisibility = "restricted_visibility"
case draft
+ case reminder
}
extension MessagePayload {
@@ -112,8 +113,8 @@ class MessagePayload: Decodable {
var pinExpires: Date?
var poll: PollPayload?
-
var draft: DraftPayload?
+ var reminder: ReminderPayload?
/// 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.
@@ -181,6 +182,7 @@ class MessagePayload: Decodable {
messageTextUpdatedAt = try container.decodeIfPresent(Date.self, forKey: .messageTextUpdatedAt)
poll = try container.decodeIfPresent(PollPayload.self, forKey: .poll)
draft = try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
+ reminder = try container.decodeIfPresent(ReminderPayload.self, forKey: .reminder)
}
init(
@@ -222,7 +224,8 @@ class MessagePayload: Decodable {
moderationDetails: MessageModerationDetailsPayload? = nil,
messageTextUpdatedAt: Date? = nil,
poll: PollPayload? = nil,
- draft: DraftPayload? = nil
+ draft: DraftPayload? = nil,
+ reminder: ReminderPayload? = nil
) {
self.id = id
self.cid = cid
@@ -263,6 +266,7 @@ class MessagePayload: Decodable {
self.messageTextUpdatedAt = messageTextUpdatedAt
self.poll = poll
self.draft = draft
+ self.reminder = reminder
}
}
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ReminderPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ReminderPayloads.swift
new file mode 100644
index 00000000000..b05af6b755b
--- /dev/null
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ReminderPayloads.swift
@@ -0,0 +1,79 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// An object describing a reminder JSON payload.
+class ReminderPayload: Decodable {
+ let channelCid: ChannelId
+ let channel: ChannelDetailPayload?
+ let messageId: MessageId
+ let message: MessagePayload?
+ let remindAt: Date?
+ let createdAt: Date
+ let updatedAt: Date
+
+ init(
+ channelCid: ChannelId,
+ messageId: MessageId,
+ message: MessagePayload? = nil,
+ channel: ChannelDetailPayload? = nil,
+ remindAt: Date?,
+ createdAt: Date,
+ updatedAt: Date
+ ) {
+ self.channelCid = channelCid
+ self.messageId = messageId
+ self.message = message
+ self.channel = channel
+ self.remindAt = remindAt
+ self.createdAt = createdAt
+ self.updatedAt = updatedAt
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case channelCid = "channel_cid"
+ case messageId = "message_id"
+ case message
+ case channel
+ case remindAt = "remind_at"
+ case createdAt = "created_at"
+ case updatedAt = "updated_at"
+ }
+}
+
+/// A request body for creating or updating a reminder
+class ReminderRequestBody: Encodable {
+ let remindAt: Date?
+
+ init(
+ remindAt: Date?
+ ) {
+ self.remindAt = remindAt
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case remindAt = "remind_at"
+ }
+}
+
+/// A response containing a list of reminders
+class RemindersQueryPayload: Decodable {
+ let reminders: [ReminderPayload]
+ let next: String?
+
+ init(reminders: [ReminderPayload], next: String?) {
+ self.reminders = reminders
+ self.next = next
+ }
+}
+
+/// A response containing a single reminder
+class ReminderResponsePayload: Decodable {
+ let reminder: ReminderPayload
+
+ init(reminder: ReminderPayload) {
+ self.reminder = reminder
+ }
+}
diff --git a/Sources/StreamChat/APIClient/Endpoints/ReminderEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/ReminderEndpoints.swift
new file mode 100644
index 00000000000..12252a7b396
--- /dev/null
+++ b/Sources/StreamChat/APIClient/Endpoints/ReminderEndpoints.swift
@@ -0,0 +1,51 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+extension Endpoint {
+ // Creates or updates a reminder for a message
+ static func createReminder(messageId: MessageId, request: ReminderRequestBody) -> Endpoint {
+ .init(
+ path: .reminder(messageId),
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: request
+ )
+ }
+
+ // Updates an existing reminder for a message
+ static func updateReminder(messageId: MessageId, request: ReminderRequestBody) -> Endpoint {
+ .init(
+ path: .reminder(messageId),
+ method: .patch,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: request
+ )
+ }
+
+ // Deletes a reminder for a message
+ static func deleteReminder(messageId: MessageId) -> Endpoint {
+ .init(
+ path: .reminder(messageId),
+ method: .delete,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: nil
+ )
+ }
+
+ // Queries reminders with the provided parameters
+ static func queryReminders(query: MessageReminderListQuery) -> Endpoint {
+ .init(
+ path: .reminders,
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: query
+ )
+ }
+}
diff --git a/Sources/StreamChat/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift
index 3fa6d723ce6..bc27e8b5974 100644
--- a/Sources/StreamChat/ChatClient+Environment.swift
+++ b/Sources/StreamChat/ChatClient+Environment.swift
@@ -146,6 +146,13 @@ extension ChatClient {
DraftMessagesRepository(database: $0, apiClient: $1)
}
+ var remindersRepositoryBuilder: (
+ _ database: DatabaseContainer,
+ _ apiClient: APIClient
+ ) -> RemindersRepository = {
+ RemindersRepository(database: $0, apiClient: $1)
+ }
+
var channelListUpdaterBuilder: (
_ database: DatabaseContainer,
_ apiClient: APIClient
diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift
index 403cbcbea3a..49333754134 100644
--- a/Sources/StreamChat/ChatClient.swift
+++ b/Sources/StreamChat/ChatClient.swift
@@ -78,7 +78,15 @@ public class ChatClient {
let pollsRepository: PollsRepository
- let draftMessagesRepository: DraftMessagesRepository
+ /// Repository for handling draft messages
+ lazy var draftMessagesRepository: DraftMessagesRepository = {
+ environment.draftMessagesRepositoryBuilder(databaseContainer, apiClient)
+ }()
+
+ /// Repository for handling message reminders
+ lazy var remindersRepository: RemindersRepository = {
+ environment.remindersRepositoryBuilder(databaseContainer, apiClient)
+ }()
let channelListUpdater: ChannelListUpdater
@@ -210,7 +218,6 @@ 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 47634239795..7c2697d129c 100644
--- a/Sources/StreamChat/ChatClientFactory.swift
+++ b/Sources/StreamChat/ChatClientFactory.swift
@@ -126,6 +126,7 @@ class ChatClientFactory {
),
ThreadUpdaterMiddleware(),
DraftUpdaterMiddleware(),
+ ReminderUpdaterMiddleware(),
UserTypingStateUpdaterMiddleware(),
ChannelTruncatedEventMiddleware(),
MemberEventMiddleware(),
diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
index 83ec7c2b45c..610b13b9f38 100644
--- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
+++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
@@ -85,6 +85,8 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
/// The worker used to update the current user member for a given channel.
private lazy var currentMemberUpdater = createMemberUpdater()
+ // MARK: - Drafts Properties
+
/// The query used for fetching the draft messages.
private var draftListQuery = DraftListQuery()
@@ -110,6 +112,8 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
return Array(observer.items)
}
+ // MARK: - Init
+
/// Creates a new `CurrentUserControllerGeneric`.
///
/// - Parameters:
@@ -120,9 +124,10 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
self.client = client
self.environment = environment
draftMessagesRepository = client.draftMessagesRepository
+ super.init()
}
- /// Synchronize local data with remote. Waits for the client to connect but doesnāt initiate the connection itself.
+ /// Synchronize local data with remote. Waits for the client to connect but doesn't initiate the connection itself.
/// This is to make sure the fetched local data is up-to-date, since the current user data is updated through WebSocket events.
///
/// - Parameter completion: Called when the controller has finished fetching the local data
diff --git a/Sources/StreamChat/Controllers/MessageController/MessageController.swift b/Sources/StreamChat/Controllers/MessageController/MessageController.swift
index 43fe09013c3..c592a2c2813 100644
--- a/Sources/StreamChat/Controllers/MessageController/MessageController.swift
+++ b/Sources/StreamChat/Controllers/MessageController/MessageController.swift
@@ -190,6 +190,9 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// The drafts repository.
private let draftsRepository: DraftMessagesRepository
+ /// The reminders repository.
+ private let remindersRepository: RemindersRepository
+
/// Creates a new `MessageControllerGeneric`.
/// - Parameters:
/// - client: The `Client` instance this controller belongs to.
@@ -210,6 +213,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
client.apiClient
)
draftsRepository = client.draftMessagesRepository
+ remindersRepository = client.remindersRepository
super.init()
setRepliesObserver()
@@ -933,6 +937,64 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
}
}
+
+ // MARK: - Reminder Actions
+
+ /// Creates a new reminder for this message.
+ /// - Parameters:
+ /// - remindAt: The date when the user should be reminded about this message.
+ /// If nil, this creates a "save for later" type reminder without a notification.
+ /// - completion: Called when the API call is finished with the result of the operation.
+ public func createReminder(
+ remindAt: Date? = nil,
+ completion: ((Result) -> Void)? = nil
+ ) {
+ remindersRepository.createReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: remindAt
+ ) { result in
+ self.callback {
+ completion?(result)
+ }
+ }
+ }
+
+ /// Updates the reminder for this message.
+ /// - Parameters:
+ /// - remindAt: The new date when the user should be reminded about this message.
+ /// If nil, this updates to a "save for later" type reminder without a notification.
+ /// - completion: Called when the API call is finished with the result of the operation.
+ public func updateReminder(
+ remindAt: Date?,
+ completion: ((Result) -> Void)? = nil
+ ) {
+ remindersRepository.updateReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: remindAt
+ ) { result in
+ self.callback {
+ completion?(result)
+ }
+ }
+ }
+
+ /// Deletes the reminder for this message.
+ /// - Parameter completion: Called when the API call is finished.
+ /// If request fails, the completion will be called with an error.
+ public func deleteReminder(
+ completion: ((Error?) -> Void)? = nil
+ ) {
+ remindersRepository.deleteReminder(
+ messageId: messageId,
+ cid: cid
+ ) { error in
+ self.callback {
+ completion?(error)
+ }
+ }
+ }
}
// MARK: - Environment
diff --git a/Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController+Combine.swift b/Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController+Combine.swift
new file mode 100644
index 00000000000..17c26ca55c0
--- /dev/null
+++ b/Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController+Combine.swift
@@ -0,0 +1,47 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Combine
+import Foundation
+
+extension MessageReminderListController {
+ /// A publisher emitting a new value every time the state of the controller changes.
+ public var statePublisher: AnyPublisher {
+ basePublishers.state.keepAlive(self)
+ }
+
+ /// A publisher emitting a new value every time the reminders change.
+ public var remindersChangesPublisher: AnyPublisher<[ListChange], Never> {
+ basePublishers.remindersChanges.keepAlive(self)
+ }
+
+ /// An internal backing object for all publicly available Combine publishers.
+ class BasePublishers {
+ /// A backing publisher for `statePublisher`.
+ let state: CurrentValueSubject
+
+ /// A backing publisher for `remindersChangesPublisher`.
+ let remindersChanges: PassthroughSubject<[ListChange], Never>
+
+ init(controller: MessageReminderListController) {
+ state = .init(controller.state)
+ remindersChanges = .init()
+
+ controller.multicastDelegate.add(additionalDelegate: self)
+ }
+ }
+}
+
+extension MessageReminderListController.BasePublishers: MessageReminderListControllerDelegate {
+ func controller(_ controller: DataController, didChangeState state: DataController.State) {
+ self.state.send(state)
+ }
+
+ func controller(
+ _ controller: MessageReminderListController,
+ didChangeReminders changes: [ListChange]
+ ) {
+ remindersChanges.send(changes)
+ }
+}
diff --git a/Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController.swift b/Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController.swift
new file mode 100644
index 00000000000..f87f63317fe
--- /dev/null
+++ b/Sources/StreamChat/Controllers/MessageReminderListController/MessageReminderListController.swift
@@ -0,0 +1,200 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import CoreData
+import Foundation
+
+public extension ChatClient {
+ /// Creates and returns a `MessageReminderListController` for the specified query.
+ ///
+ /// - Parameter query: The query object defining the criteria for retrieving the list of message reminders.
+ /// - Returns: A `MessageReminderListController` initialized with the provided query and client.
+ func messageReminderListController(query: MessageReminderListQuery = .init()) -> MessageReminderListController {
+ .init(query: query, client: self)
+ }
+}
+
+/// `MessageReminderListController` uses this protocol to communicate changes to its delegate.
+public protocol MessageReminderListControllerDelegate: DataControllerStateDelegate {
+ /// The controller changed the list of observed reminders.
+ ///
+ /// - Parameters:
+ /// - controller: The controller emitting the change callback.
+ /// - changes: The change to the list of reminders.
+ func controller(
+ _ controller: MessageReminderListController,
+ didChangeReminders changes: [ListChange]
+ )
+}
+
+/// A controller which allows querying and filtering message reminders.
+public class MessageReminderListController: DataController, DelegateCallable, DataStoreProvider {
+ /// The query specifying and filtering the list of reminders.
+ public let query: MessageReminderListQuery
+
+ /// The `ChatClient` instance this controller belongs to.
+ public let client: ChatClient
+
+ /// The message reminders the controller represents.
+ ///
+ /// To observe changes of the reminders, set your class as a delegate of this controller or use the provided
+ /// `Combine` publishers.
+ public var reminders: LazyCachedMapCollection {
+ startMessageRemindersObserverIfNeeded()
+ return messageRemindersObserver.items
+ }
+
+ /// A Boolean value that returns whether pagination is finished.
+ public private(set) var hasLoadedAllReminders: Bool = false
+
+ /// Set the delegate of `MessageReminderListController` to observe the changes in the system.
+ public weak var delegate: MessageReminderListControllerDelegate? {
+ get { multicastDelegate.mainDelegate }
+ set { multicastDelegate.set(mainDelegate: newValue) }
+ }
+
+ /// A type-erased delegate.
+ var multicastDelegate: MulticastDelegate = .init() {
+ didSet {
+ stateMulticastDelegate.set(mainDelegate: multicastDelegate.mainDelegate)
+ stateMulticastDelegate.set(additionalDelegates: multicastDelegate.additionalDelegates)
+
+ // After setting delegate local changes will be fetched and observed.
+ startMessageRemindersObserverIfNeeded()
+ }
+ }
+
+ /// Used for observing the database for changes.
+ private(set) lazy var messageRemindersObserver: BackgroundListDatabaseObserver = {
+ let request = MessageReminderDTO.remindersFetchRequest(query: query)
+
+ let observer = self.environment.createMessageReminderListDatabaseObserver(
+ client.databaseContainer,
+ request,
+ { try $0.asModel() }
+ )
+
+ observer.onDidChange = { [weak self] changes in
+ self?.delegateCallback { [weak self] in
+ guard let self = self else {
+ log.warning("Callback called while self is nil")
+ return
+ }
+
+ $0.controller(self, didChangeReminders: changes)
+ }
+ }
+
+ return observer
+ }()
+
+ 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,
+ /// and expose the published values by mapping them to a read-only `AnyPublisher` type.
+ var basePublishers: BasePublishers {
+ if let value = _basePublishers as? BasePublishers {
+ return value
+ }
+ _basePublishers = BasePublishers(controller: self)
+ return _basePublishers as? BasePublishers ?? .init(controller: self)
+ }
+
+ private let remindersRepository: RemindersRepository
+ private let environment: Environment
+ private var nextCursor: String?
+
+ /// Creates a new `MessageReminderListController`.
+ ///
+ /// - Parameters:
+ /// - query: The query used for filtering the reminders.
+ /// - client: The `Client` instance this controller belongs to.
+ init(query: MessageReminderListQuery, client: ChatClient, environment: Environment = .init()) {
+ self.client = client
+ self.query = query
+ self.environment = environment
+ remindersRepository = client.remindersRepository
+ super.init()
+ }
+
+ override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ startMessageRemindersObserverIfNeeded()
+
+ remindersRepository.loadReminders(query: query) { [weak self] result in
+ guard let self else { return }
+ if let value = result.value {
+ self.nextCursor = value.next
+ self.hasLoadedAllReminders = value.next == nil
+ }
+ if let error = result.error {
+ self.state = .remoteDataFetchFailed(ClientError(with: error))
+ } else {
+ self.state = .remoteDataFetched
+ }
+ self.callback { completion?(result.error) }
+ }
+ }
+
+ /// If the `state` of the controller is `initialized`, this method calls `startObserving` on the
+ /// `messageRemindersObserver` to fetch the local data and start observing the changes. It also changes
+ /// `state` based on the result.
+ private func startMessageRemindersObserverIfNeeded() {
+ guard state == .initialized else { return }
+ do {
+ try messageRemindersObserver.startObserving()
+ state = .localDataFetched
+ } catch {
+ state = .localDataFetchFailed(ClientError(with: error))
+ log.error("Failed to perform fetch request with error: \(error). This is an internal error.")
+ }
+ }
+
+ // MARK: - Actions
+
+ /// Loads more reminders.
+ ///
+ /// - Parameters:
+ /// - limit: Limit for the page size.
+ /// - completion: The completion callback.
+ public func loadMoreReminders(
+ limit: Int? = nil,
+ completion: ((Result<[MessageReminder], Error>) -> Void)? = nil
+ ) {
+ let limit = limit ?? query.pagination.pageSize
+ var updatedQuery = query
+ updatedQuery.pagination = Pagination(pageSize: limit, cursor: nextCursor)
+ remindersRepository.loadReminders(query: updatedQuery) { [weak self] result in
+ switch result {
+ case let .success(value):
+ self?.callback {
+ self?.nextCursor = value.next
+ self?.hasLoadedAllReminders = value.next == nil
+ completion?(.success(value.reminders))
+ }
+ case let .failure(error):
+ self?.callback {
+ completion?(.failure(error))
+ }
+ }
+ }
+ }
+}
+
+extension MessageReminderListController {
+ struct Environment {
+ var createMessageReminderListDatabaseObserver: (
+ _ database: DatabaseContainer,
+ _ fetchRequest: NSFetchRequest,
+ _ itemCreator: @escaping (MessageReminderDTO) throws -> MessageReminder
+ )
+ -> BackgroundListDatabaseObserver = {
+ BackgroundListDatabaseObserver(
+ database: $0,
+ fetchRequest: $1,
+ itemCreator: $2,
+ itemReuseKeyPaths: (\MessageReminder.id, \MessageReminderDTO.id)
+ )
+ }
+ }
+}
diff --git a/Sources/StreamChat/Database/DTOs/ChannelConfigDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelConfigDTO.swift
index d42ecf72584..258d9d81629 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelConfigDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelConfigDTO.swift
@@ -20,6 +20,7 @@ final class ChannelConfigDTO: NSManagedObject {
@NSManaged var pollsEnabled: Bool
@NSManaged var urlEnrichmentEnabled: Bool
@NSManaged var messageRetention: String
+ @NSManaged var messageRemindersEnabled: Bool
@NSManaged var maxMessageLength: Int32
@NSManaged var createdAt: DBDate
@NSManaged var updatedAt: DBDate
@@ -40,6 +41,7 @@ final class ChannelConfigDTO: NSManagedObject {
pollsEnabled: pollsEnabled,
urlEnrichmentEnabled: urlEnrichmentEnabled,
skipLastMsgAtUpdateForSystemMsg: skipLastMsgAtUpdateForSystemMsg,
+ messageRemindersEnabled: messageRemindersEnabled,
messageRetention: messageRetention,
maxMessageLength: Int(maxMessageLength),
commands: Array(Set(
@@ -80,6 +82,7 @@ extension ChannelConfig {
dto.commands = NSOrderedSet(array: commands.map { $0.asDTO(context: context) })
dto.pollsEnabled = pollsEnabled
dto.skipLastMsgAtUpdateForSystemMsg = skipLastMsgAtUpdateForSystemMsg
+ dto.messageRemindersEnabled = messageRemindersEnabled
return dto
}
}
diff --git a/Sources/StreamChat/Database/DTOs/MessageDTO.swift b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
index e3037ca88fb..7061a9b1673 100644
--- a/Sources/StreamChat/Database/DTOs/MessageDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
@@ -88,6 +88,8 @@ class MessageDTO: NSManagedObject {
@NSManaged var draftReply: MessageDTO?
@NSManaged var isDraft: Bool
+ @NSManaged var reminder: MessageReminderDTO?
+
/// 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`.
@@ -1043,6 +1045,13 @@ extension NSManagedObjectContext: MessageDatabaseSession {
dto.updateReadBy(withChannelReads: channelDTO.reads)
}
+ if let reminder = payload.reminder {
+ dto.reminder = try saveReminder(payload: reminder, cache: cache)
+ } else if let reminderDTO = dto.reminder {
+ delete(reminderDTO)
+ dto.reminder = nil
+ }
+
// Refetch channel preview if the current preview has changed.
//
// The current message can stop being a valid preview e.g.
@@ -1789,7 +1798,12 @@ private extension ChatMessage {
readBy: readBy,
poll: poll,
textUpdatedAt: textUpdatedAt,
- draftReply: draftReply.map(DraftMessage.init)
+ draftReply: draftReply.map(DraftMessage.init),
+ reminder: dto.reminder.map { .init(
+ remindAt: $0.remindAt?.bridgeDate,
+ createdAt: $0.createdAt.bridgeDate,
+ updatedAt: $0.updatedAt.bridgeDate
+ ) }
)
if let transformer = chatClientConfig?.modelsTransformer {
diff --git a/Sources/StreamChat/Database/DTOs/MessageReminderDTO.swift b/Sources/StreamChat/Database/DTOs/MessageReminderDTO.swift
new file mode 100644
index 00000000000..1a983ea0096
--- /dev/null
+++ b/Sources/StreamChat/Database/DTOs/MessageReminderDTO.swift
@@ -0,0 +1,172 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import CoreData
+import Foundation
+
+@objc(MessageReminderDTO)
+class MessageReminderDTO: NSManagedObject {
+ @NSManaged var id: String
+ @NSManaged var createdAt: DBDate
+ @NSManaged var updatedAt: DBDate
+ @NSManaged var remindAt: DBDate?
+
+ // An helper property that is used for sorting the reminders when `remindAt` is not set.
+ @NSManaged var sortingRemindAt: DBDate?
+
+ // Relationships
+ @NSManaged var message: MessageDTO
+ @NSManaged var channel: ChannelDTO
+
+ override func willSave() {
+ super.willSave()
+
+ let newSortingRemindAt = remindAt ?? .distantFuture.bridgeDate
+ if sortingRemindAt != newSortingRemindAt {
+ sortingRemindAt = newSortingRemindAt
+ }
+ }
+
+ /// Returns a fetch request for a message reminder with the provided message ID.
+ static func fetchRequest(messageId: MessageId) -> NSFetchRequest {
+ let request = NSFetchRequest(entityName: MessageReminderDTO.entityName)
+ request.predicate = NSPredicate(format: "message.id == %@", messageId)
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageReminderDTO.createdAt, ascending: false)]
+ return request
+ }
+
+ /// Returns a fetch request for message reminders based on the provided query.
+ static func remindersFetchRequest(query: MessageReminderListQuery) -> NSFetchRequest {
+ let request = NSFetchRequest(entityName: MessageReminderDTO.entityName)
+ MessageReminderDTO.applyPrefetchingState(to: request)
+
+ // Apply sort descriptors from the query
+ var sortDescriptors: [NSSortDescriptor] = []
+ for sorting in query.sort {
+ switch sorting.key {
+ case .remindAt:
+ sortDescriptors.append(NSSortDescriptor(keyPath: \MessageReminderDTO.sortingRemindAt, ascending: sorting.isAscending))
+ case .createdAt:
+ sortDescriptors.append(NSSortDescriptor(keyPath: \MessageReminderDTO.createdAt, ascending: sorting.isAscending))
+ case .updatedAt:
+ sortDescriptors.append(NSSortDescriptor(keyPath: \MessageReminderDTO.updatedAt, ascending: sorting.isAscending))
+ default:
+ continue
+ }
+ }
+ // Apply default sort if none provided
+ if sortDescriptors.isEmpty {
+ sortDescriptors = [NSSortDescriptor(keyPath: \MessageReminderDTO.sortingRemindAt, ascending: true)]
+ }
+ request.sortDescriptors = sortDescriptors
+
+ if let filter = query.filter, let predicate = filter.predicate {
+ request.predicate = predicate
+ }
+
+ return request
+ }
+
+ /// Loads a reminder with the specified message ID from the context.
+ static func load(messageId: MessageId, context: NSManagedObjectContext) -> MessageReminderDTO? {
+ let request = fetchRequest(messageId: messageId)
+ return load(by: request, context: context).first
+ }
+
+ /// Loads or creates a reminder with the specified message ID.
+ static func loadOrCreate(
+ messageId: MessageId,
+ context: NSManagedObjectContext,
+ cache: PreWarmedCache?
+ ) -> MessageReminderDTO {
+ // Try to reuse existing object if available
+ if let existing = load(messageId: messageId, context: context) {
+ return existing
+ }
+
+ let request = fetchRequest(messageId: messageId)
+ let new = NSEntityDescription.insertNewObject(into: context, for: request)
+ return new
+ }
+
+ /// Loads a message reminder DTO with the specified id.
+ /// - Parameters:
+ /// - id: The reminder id to look for.
+ /// - context: NSManagedObjectContext to fetch from.
+ /// - Returns: The message reminder DTO with the specified id, if exists.
+ static func load(id: String, context: NSManagedObjectContext) -> MessageReminderDTO? {
+ let request = NSFetchRequest(entityName: MessageReminderDTO.entityName)
+ request.predicate = NSPredicate(format: "id == %@", id)
+ return try? context.fetch(request).first
+ }
+}
+
+extension NSManagedObjectContext: ReminderDatabaseSession {
+ /// Creates or updates a reminder for a message.
+ func saveReminder(
+ payload: ReminderPayload,
+ cache: PreWarmedCache?
+ ) throws -> MessageReminderDTO {
+ let channelDTO: ChannelDTO
+ if let existingChannel = ChannelDTO.load(cid: payload.channelCid, context: self) {
+ channelDTO = existingChannel
+ } else if let channelPayload = payload.channel {
+ channelDTO = try saveChannel(payload: channelPayload, query: nil, cache: nil)
+ } else {
+ throw ClientError.ChannelDoesNotExist(cid: payload.channelCid)
+ }
+
+ let messageDTO: MessageDTO
+ if let existingMessage = MessageDTO.load(id: payload.messageId, context: self) {
+ messageDTO = existingMessage
+ } else if let messagePayload = payload.message {
+ messageDTO = try saveMessage(payload: messagePayload, for: payload.channelCid, cache: cache)
+ } else {
+ throw ClientError.MessageDoesNotExist(messageId: payload.messageId)
+ }
+
+ let reminderDTO = MessageReminderDTO.loadOrCreate(
+ messageId: payload.messageId,
+ context: self,
+ cache: cache
+ )
+
+ reminderDTO.id = payload.messageId
+ reminderDTO.remindAt = payload.remindAt?.bridgeDate
+ reminderDTO.createdAt = payload.createdAt.bridgeDate
+ reminderDTO.updatedAt = payload.updatedAt.bridgeDate
+ reminderDTO.message = messageDTO
+ reminderDTO.channel = channelDTO
+
+ return reminderDTO
+ }
+
+ /// Deletes a reminder for the specified message ID.
+ func deleteReminder(messageId: MessageId) {
+ let message = message(id: messageId)
+ guard let reminderDTO = message?.reminder else {
+ return
+ }
+ delete(reminderDTO)
+ message?.reminder = nil
+ }
+}
+
+// MARK: - Converting to domain model
+
+extension MessageReminderDTO {
+ /// Snapshots the current state of `MessageReminderDTO` and returns an immutable model object from it.
+ /// - Returns: A `MessageReminder` instance created from the DTO data.
+ /// - Throws: An error when the underlying data is inconsistent.
+ func asModel() throws -> MessageReminder {
+ MessageReminder(
+ id: id,
+ remindAt: remindAt?.bridgeDate,
+ message: try message.asModel(),
+ channel: try channel.asModel(),
+ createdAt: createdAt.bridgeDate,
+ updatedAt: updatedAt.bridgeDate
+ )
+ }
+}
diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift
index a8de9165056..71ff789b263 100644
--- a/Sources/StreamChat/Database/DatabaseSession.swift
+++ b/Sources/StreamChat/Database/DatabaseSession.swift
@@ -542,6 +542,23 @@ protocol ThreadReadDatabaseSession {
)
}
+protocol ReminderDatabaseSession {
+ /// Saves a reminder with the provided payload.
+ /// - Parameters:
+ /// - payload: The `ReminderPayload` containing the details of the reminder to be saved.
+ /// - cache: An optional `PreWarmedCache` to optimize the save operation.
+ /// - Returns: A `MessageReminderDTO` representing the saved reminder.
+ /// - Throws: An error if the save operation fails.
+ @discardableResult
+ func saveReminder(
+ payload: ReminderPayload,
+ cache: PreWarmedCache?
+ ) throws -> MessageReminderDTO
+
+ /// Deletes a reminder for the specified message ID.
+ func deleteReminder(messageId: MessageId)
+}
+
protocol PollDatabaseSession {
/// Saves a poll with the provided payload.
/// - Parameters:
@@ -669,6 +686,7 @@ protocol DatabaseSession: UserDatabaseSession,
QueuedRequestDatabaseSession,
ThreadDatabaseSession,
ThreadReadDatabaseSession,
+ ReminderDatabaseSession,
PollDatabaseSession {}
extension DatabaseSession {
diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
index 97ecb5c8c87..84628c805c4 100644
--- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
+++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
@@ -20,6 +20,7 @@
+
@@ -75,6 +76,7 @@
+
@@ -261,6 +263,7 @@
+
@@ -319,6 +322,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift
index 1aaa575ec65..df634576534 100644
--- a/Sources/StreamChat/Models/ChatMessage.swift
+++ b/Sources/StreamChat/Models/ChatMessage.swift
@@ -67,6 +67,9 @@ public struct ChatMessage {
/// The draft reply to this message. Applies only for the messages of the current user.
public let draftReply: DraftMessage?
+ /// The reminder information for this message if it has been added to reminders.
+ public let reminder: MessageReminderInfo?
+
/// A flag indicating whether the message was bounced due to moderation.
public let isBounced: Bool
@@ -217,7 +220,8 @@ public struct ChatMessage {
readBy: Set,
poll: Poll?,
textUpdatedAt: Date?,
- draftReply: DraftMessage?
+ draftReply: DraftMessage?,
+ reminder: MessageReminderInfo?
) {
self.id = id
self.cid = cid
@@ -259,6 +263,7 @@ public struct ChatMessage {
_attachments = attachments
_quotedMessage = { quotedMessage }
self.draftReply = draftReply
+ self.reminder = reminder
}
/// Returns a new `ChatMessage` with the provided data changed.
@@ -315,7 +320,8 @@ public struct ChatMessage {
readBy: readBy,
poll: poll,
textUpdatedAt: textUpdatedAt,
- draftReply: draftReply
+ draftReply: draftReply,
+ reminder: reminder
)
}
@@ -427,7 +433,8 @@ public struct ChatMessage {
readBy: readBy,
poll: poll,
textUpdatedAt: textUpdatedAt,
- draftReply: draftReply
+ draftReply: draftReply,
+ reminder: reminder
)
}
}
@@ -559,6 +566,7 @@ extension ChatMessage: Hashable {
guard lhs.translations == rhs.translations else { return false }
guard lhs.type == rhs.type else { return false }
guard lhs.draftReply == rhs.draftReply else { return false }
+ guard lhs.reminder == rhs.reminder else { return false }
return true
}
diff --git a/Sources/StreamChat/Models/DraftMessage.swift b/Sources/StreamChat/Models/DraftMessage.swift
index 8742794a357..fb909bbf8ea 100644
--- a/Sources/StreamChat/Models/DraftMessage.swift
+++ b/Sources/StreamChat/Models/DraftMessage.swift
@@ -162,5 +162,6 @@ extension ChatMessage {
poll = nil
textUpdatedAt = nil
draftReply = nil
+ reminder = nil
}
}
diff --git a/Sources/StreamChat/Models/MessageReminder.swift b/Sources/StreamChat/Models/MessageReminder.swift
new file mode 100644
index 00000000000..6e76d5e87cf
--- /dev/null
+++ b/Sources/StreamChat/Models/MessageReminder.swift
@@ -0,0 +1,75 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// A type representing a message reminder.
+public struct MessageReminder {
+ /// A unique identifier of the reminder, based on the message ID.
+ public let id: String
+
+ /// The date when the user should be reminded about this message.
+ /// If nil, this is a bookmark type reminder without a notification.
+ public let remindAt: Date?
+
+ /// The message that has been marked for reminder.
+ public let message: ChatMessage
+
+ /// The channel where the message belongs to.
+ public let channel: ChatChannel
+
+ /// Date when the reminder was created on the server.
+ public let createdAt: Date
+
+ /// A date when the reminder was updated last time.
+ public let updatedAt: Date
+
+ init(
+ id: String,
+ remindAt: Date?,
+ message: ChatMessage,
+ channel: ChatChannel,
+ createdAt: Date,
+ updatedAt: Date
+ ) {
+ self.id = id
+ self.remindAt = remindAt
+ self.message = message
+ self.channel = channel
+ self.createdAt = createdAt
+ self.updatedAt = updatedAt
+ }
+}
+
+extension MessageReminder: Hashable {
+ public static func == (lhs: Self, rhs: Self) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+}
+
+/// A type representing the reminder information.
+///
+/// Does not contain any reference to the message or channel so that
+/// it can be used in these models without creating a circular reference.
+public struct MessageReminderInfo: Equatable {
+ /// The date when the user should be reminded about this message.
+ /// If nil, this is a bookmark type reminder without a notification.
+ public let remindAt: Date?
+
+ /// Date when the reminder was created on the server.
+ public let createdAt: Date
+
+ /// A date when the reminder was updated last time.
+ public let updatedAt: Date
+
+ init(remindAt: Date?, createdAt: Date, updatedAt: Date) {
+ self.remindAt = remindAt
+ self.createdAt = createdAt
+ self.updatedAt = updatedAt
+ }
+}
diff --git a/Sources/StreamChat/Query/Filter+ChatChannel.swift b/Sources/StreamChat/Query/Filter+predicate.swift
similarity index 92%
rename from Sources/StreamChat/Query/Filter+ChatChannel.swift
rename to Sources/StreamChat/Query/Filter+predicate.swift
index 63b258b5421..62c4e8b560c 100644
--- a/Sources/StreamChat/Query/Filter+ChatChannel.swift
+++ b/Sources/StreamChat/Query/Filter+predicate.swift
@@ -4,43 +4,12 @@
import Foundation
-extension Filter where Scope == ChannelListFilterScope {
- /// If a valueMapper was provided, then here we will try to transform the value
- /// using the mapper.
- ///
- /// If the mapper returns nil, the original value will be returned
- var mappedValue: FilterValue {
- valueMapper?(value) ?? value
- }
-
- /// If the mappedValues is an array of FilterValues, we will try to transform them using the valueMapper
- /// to ensure that both parts of the comparison are of the same type.
- ///
- /// If the value is not an array, this value will return nil.
- /// If the valueMapper isn't provided or the value mapper returns nil, the original value will be included
- /// in the array.
- var mappedArrayValue: [FilterValue]? {
- guard let filterArray = mappedValue as? [FilterValue] else {
- return nil
- }
- return filterArray.map { valueMapper?($0) ?? $0 }
- }
-
- /// If it can be translated, this will return
- /// an NSPredicate instance that is equivalent
- /// to the current filter.
- ///
- /// For now it's limited to ChannelList as it's not
- /// needed anywhere else
+extension Filter {
+ /// Converts the current filter into an NSPredicate if it can be translated.
///
- /// The predicate will be automatically be used
- /// by the ChannelDTO to create the
- /// fetchRequest.
+ /// This is useful to make sure the backend filters can be used to filter data in CoreData.
///
- /// - Important:
- /// The behaviour of the ChannelDTO, to include or not
- /// the predicate in the fetchRequest, it's controlled by
- /// `ChatClientConfig.isChannelAutomaticFilteringEnabled`
+ /// **Note:** Extra data properties will be ignored since they are stored in binary format.
var predicate: NSPredicate? {
guard let op = FilterOperator(rawValue: `operator`) else {
return nil
@@ -234,6 +203,27 @@ extension Filter where Scope == ChannelListFilterScope {
return nil
}
}
+
+ /// If a valueMapper was provided, then here we will try to transform the value
+ /// using the mapper.
+ ///
+ /// If the mapper returns nil, the original value will be returned
+ var mappedValue: FilterValue {
+ valueMapper?(value) ?? value
+ }
+
+ /// If the mappedValues is an array of FilterValues, we will try to transform them using the valueMapper
+ /// to ensure that both parts of the comparison are of the same type.
+ ///
+ /// If the value is not an array, this value will return nil.
+ /// If the valueMapper isn't provided or the value mapper returns nil, the original value will be included
+ /// in the array.
+ var mappedArrayValue: [FilterValue]? {
+ guard let filterArray = mappedValue as? [FilterValue] else {
+ return nil
+ }
+ return filterArray.map { valueMapper?($0) ?? $0 }
+ }
}
extension String {
diff --git a/Sources/StreamChat/Query/Filter.swift b/Sources/StreamChat/Query/Filter.swift
index aa3a633a661..eeeed7d4998 100644
--- a/Sources/StreamChat/Query/Filter.swift
+++ b/Sources/StreamChat/Query/Filter.swift
@@ -427,6 +427,17 @@ public extension Filter {
)
}
+ /// Matches values where the given property is nil.
+ static func isNil(_ key: FilterKey) -> Filter {
+ .init(
+ operator: .exists,
+ key: key,
+ value: false,
+ valueMapper: key.valueMapper,
+ keyPathString: key.keyPathString
+ )
+ }
+
/// Matches if the key contains the given value.
static func contains(_ key: FilterKey, value: String) -> Filter {
.init(
diff --git a/Sources/StreamChat/Query/MessageReminderListQuery.swift b/Sources/StreamChat/Query/MessageReminderListQuery.swift
new file mode 100644
index 00000000000..5dfa8f6922a
--- /dev/null
+++ b/Sources/StreamChat/Query/MessageReminderListQuery.swift
@@ -0,0 +1,126 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// A namespace for the `FilterKey`s suitable to be used for `MessageReminderListQuery`. This scope is not aware of any extra data types.
+public protocol AnyMessageReminderListFilterScope {}
+
+/// An extra-data-specific namespace for the `FilterKey`s suitable to be used for `MessageReminderListQuery`.
+public struct MessageReminderListFilterScope: FilterScope, AnyMessageReminderListFilterScope {}
+
+/// Filter keys for message reminder list.
+public extension FilterKey where Scope: AnyMessageReminderListFilterScope {
+ /// A filter key for matching the `channel_cid` value.
+ /// Supported operators: `in`, `equal`
+ static var cid: FilterKey { .init(
+ rawValue: "channel_cid",
+ keyPathString: #keyPath(MessageReminderDTO.channel.cid),
+ valueMapper: { $0.rawValue }
+ ) }
+
+ /// A filter key for matching the `remind_at` value.
+ /// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`
+ static var remindAt: FilterKey { .init(rawValue: "remind_at", keyPathString: #keyPath(MessageReminderDTO.remindAt)) }
+
+ /// A filter key for matching the `created_at` value.
+ /// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`
+ static var createdAt: FilterKey { .init(rawValue: "created_at", keyPathString: #keyPath(MessageReminderDTO.createdAt)) }
+}
+
+public extension Filter where Scope: AnyMessageReminderListFilterScope {
+ /// Returns a filter that matches message reminders without a due date.
+ static var withoutRemindAt: Filter {
+ .isNil(.remindAt)
+ }
+
+ /// Returns a filter that matches message reminders with a due date.
+ static var withRemindAt: Filter {
+ .exists(.remindAt)
+ }
+
+ /// Returns a filter that matches message reminders that are overdue.
+ static var overdue: Filter {
+ .lessOrEqual(.remindAt, than: Date())
+ }
+
+ /// Returns a filter that matches message reminders that are upcoming.
+ static var upcoming: Filter {
+ .greaterOrEqual(.remindAt, than: Date())
+ }
+}
+
+/// The type describing a value that can be used for sorting when querying message reminders.
+public struct MessageReminderListSortingKey: RawRepresentable, Hashable, SortingKey {
+ public let rawValue: String
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+}
+
+/// The supported sorting keys for message reminders.
+public extension MessageReminderListSortingKey {
+ /// Sorts reminders by `remind_at` field.
+ static let remindAt = Self(rawValue: "remind_at")
+
+ /// Sorts reminders by `created_at` field.
+ static let createdAt = Self(rawValue: "created_at")
+
+ /// Sorts reminders by `updated_at` field.
+ static let updatedAt = Self(rawValue: "updated_at")
+}
+
+/// A query is used for querying specific message reminders from backend.
+/// You can specify filter, sorting, and pagination options.
+public struct MessageReminderListQuery: Encodable {
+ private enum CodingKeys: String, CodingKey {
+ case filter
+ case sort
+ }
+
+ /// A filter for the query (see `Filter`).
+ public let filter: Filter?
+ /// A sorting for the query (see `Sorting`).
+ public let sort: [Sorting]
+ /// A pagination.
+ public var pagination: Pagination
+
+ /// Init a message reminders query.
+ /// - Parameters:
+ /// - filter: a reminders filter.
+ /// - sort: a sorting list for reminders.
+ /// - pageSize: a page size for pagination.
+ /// - next: a token for fetching the next page.
+ public init(
+ filter: Filter? = nil,
+ sort: [Sorting] = [.init(key: .remindAt, isAscending: true)],
+ pageSize: Int = 25,
+ next: String? = nil
+ ) {
+ self.filter = filter
+ self.sort = sort
+ pagination = Pagination(pageSize: pageSize, cursor: next)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ if let filter = filter {
+ try container.encode(filter, forKey: .filter)
+ }
+
+ if !sort.isEmpty {
+ try container.encode(sort, forKey: .sort)
+ }
+
+ try pagination.encode(to: encoder)
+ }
+}
+
+extension MessageReminderListQuery: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ "Filter: \(String(describing: filter)) | Sort: \(sort)"
+ }
+}
diff --git a/Sources/StreamChat/Repositories/RemindersRepository.swift b/Sources/StreamChat/Repositories/RemindersRepository.swift
new file mode 100644
index 00000000000..02769eddea2
--- /dev/null
+++ b/Sources/StreamChat/Repositories/RemindersRepository.swift
@@ -0,0 +1,226 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import CoreData
+
+/// A response containing a list of reminders and pagination information.
+struct ReminderListResponse {
+ var reminders: [MessageReminder]
+ var next: String?
+}
+
+/// Repository for handling message reminders.
+class RemindersRepository {
+ /// The database container for local storage operations.
+ private let database: DatabaseContainer
+
+ /// The API client for remote operations.
+ private let apiClient: APIClient
+
+ /// Creates a new RemindersRepository instance.
+ /// - Parameters:
+ /// - database: The database container for local storage operations.
+ /// - apiClient: The API client for remote operations.
+ init(database: DatabaseContainer, apiClient: APIClient) {
+ self.database = database
+ self.apiClient = apiClient
+ }
+
+ /// Loads reminders based on the provided query.
+ /// - Parameters:
+ /// - query: The query containing filtering and sorting parameters.
+ /// - completion: Called when the operation completes.
+ func loadReminders(
+ query: MessageReminderListQuery,
+ completion: @escaping (Result) -> Void
+ ) {
+ apiClient.request(endpoint: .queryReminders(query: query)) { [weak self] result in
+ switch result {
+ case .success(let response):
+ self?.database.write(
+ converting: { session in
+ let reminders = response.reminders.compactMap { payload in
+ do {
+ let reminderDTO = try session.saveReminder(payload: payload, cache: nil)
+ return try reminderDTO.asModel()
+ } catch {
+ log.error("Failed to convert reminder payload to model: \(error.localizedDescription)")
+ return nil
+ }
+ }
+ return ReminderListResponse(reminders: reminders, next: response.next)
+ },
+ completion: completion
+ )
+ case .failure(let error):
+ completion(.failure(error))
+ }
+ }
+ }
+
+ /// Creates a new reminder for a message.
+ /// - Parameters:
+ /// - messageId: The message identifier to create a reminder for.
+ /// - cid: The channel identifier the message belongs to.
+ /// - remindAt: The date when the user should be reminded about this message.
+ /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
+ func createReminder(
+ messageId: MessageId,
+ cid: ChannelId,
+ remindAt: Date?,
+ completion: @escaping ((Result) -> Void)
+ ) {
+ let requestBody = ReminderRequestBody(remindAt: remindAt)
+ let endpoint: Endpoint = .createReminder(
+ messageId: messageId,
+ request: requestBody
+ )
+
+ // First optimistically create the reminder locally
+ database.write { session in
+ let now = Date()
+ let reminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ message: nil,
+ remindAt: remindAt,
+ createdAt: now,
+ updatedAt: now
+ )
+ try session.saveReminder(payload: reminderPayload, cache: nil)
+ } completion: { _ in
+ // Make the API call to create the reminder
+ self.apiClient.request(endpoint: endpoint) { [weak self] result in
+ switch result {
+ case .success(let payload):
+ self?.database.write(converting: {
+ try $0.saveReminder(payload: payload.reminder, cache: nil).asModel()
+ }, completion: completion)
+ case .failure(let error):
+ // Rollback the optimistic update if the API call fails
+ self?.database.write({ session in
+ session.deleteReminder(messageId: messageId)
+ }, completion: { _ in
+ completion(.failure(error))
+ })
+ }
+ }
+ }
+ }
+
+ /// Updates an existing reminder for a message.
+ /// - Parameters:
+ /// - messageId: The message identifier for the reminder to update.
+ /// - cid: The channel identifier the message belongs to.
+ /// - remindAt: The new date when the user should be reminded about this message.
+ /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
+ func updateReminder(
+ messageId: MessageId,
+ cid: ChannelId,
+ remindAt: Date?,
+ completion: @escaping ((Result) -> Void)
+ ) {
+ let requestBody = ReminderRequestBody(remindAt: remindAt)
+ let endpoint: Endpoint = .updateReminder(messageId: messageId, request: requestBody)
+
+ // Save current data for potential rollback
+ var originalRemindAt: Date?
+
+ // First optimistically update the reminder locally
+ database.write { session in
+ // Verify the message exists
+ guard let messageDTO = session.message(id: messageId) else {
+ log.warning("Failed to find message with id: \(messageId) for updating reminder")
+ return
+ }
+
+ originalRemindAt = messageDTO.reminder?.remindAt?.bridgeDate
+
+ messageDTO.reminder?.remindAt = remindAt?.bridgeDate
+ } completion: { _ in
+ // Make the API call to update the reminder
+ self.apiClient.request(endpoint: endpoint) { [weak self] result in
+ switch result {
+ case .success(let payload):
+ self?.database.write(converting: {
+ try $0.saveReminder(payload: payload.reminder, cache: nil).asModel()
+ }, completion: completion)
+ case .failure(let error):
+ self?.database.write({ session in
+ // Restore original value
+ guard let messageDTO = session.message(id: messageId) else {
+ return
+ }
+ messageDTO.reminder?.remindAt = originalRemindAt?.bridgeDate
+ }, completion: { _ in
+ completion(.failure(error))
+ })
+ }
+ }
+ }
+ }
+
+ /// Deletes a reminder for a message.
+ /// - Parameters:
+ /// - messageId: The message identifier for the reminder to delete.
+ /// - cid: The channel identifier the message belongs to.
+ /// - completion: Called when the API call is finished. Called with an error if the remote update fails.
+ func deleteReminder(
+ messageId: MessageId,
+ cid: ChannelId,
+ completion: @escaping ((Error?) -> Void)
+ ) {
+ let endpoint: Endpoint = .deleteReminder(messageId: messageId)
+
+ // Save data for potential rollback
+ var originalPayload: ReminderPayload?
+
+ // First optimistically delete the reminder locally
+ database.write { session in
+ // Verify the message exists
+ guard let messageDTO = session.message(id: messageId) else {
+ log.warning("Failed to find message with id: \(messageId) for deleting reminder")
+ return
+ }
+
+ // Get original reminder data for potential rollback
+ if let reminderDTO = messageDTO.reminder {
+ // Store the original state for potential rollback
+ originalPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ message: nil,
+ remindAt: reminderDTO.remindAt?.bridgeDate,
+ createdAt: reminderDTO.createdAt.bridgeDate,
+ updatedAt: reminderDTO.updatedAt.bridgeDate
+ )
+ }
+
+ // Delete optimistically
+ session.deleteReminder(messageId: messageId)
+ } completion: { _ in
+ // Make the API call to delete the reminder
+ self.apiClient.request(endpoint: endpoint) { [weak self] result in
+ switch result {
+ case .success:
+ completion(nil)
+
+ case .failure(let error):
+ // Rollback the optimistic delete if the API call fails
+ guard let originalPayload = originalPayload else {
+ completion(error)
+ return
+ }
+
+ self?.database.write({ session in
+ // Restore original reminder
+ try session.saveReminder(payload: originalPayload, cache: nil)
+ }, completion: { _ in
+ completion(error)
+ })
+ }
+ }
+ }
+ }
+}
diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware.swift
new file mode 100644
index 00000000000..1091afefca8
--- /dev/null
+++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware.swift
@@ -0,0 +1,42 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+struct ReminderUpdaterMiddleware: EventMiddleware {
+ func handle(event: Event, session: DatabaseSession) -> Event? {
+ switch event {
+ case let event as ReminderCreatedEventDTO:
+ guard let reminder = event.payload.reminder else { break }
+ do {
+ try session.saveReminder(payload: reminder, cache: nil)
+ } catch {
+ log.error("Failed to save reminder: \(error)")
+ }
+
+ case let event as ReminderUpdatedEventDTO:
+ guard let reminder = event.payload.reminder else { break }
+ do {
+ try session.saveReminder(payload: reminder, cache: nil)
+ } catch {
+ log.error("Failed to update reminder: \(error)")
+ }
+
+ case let event as ReminderDueNotificationEventDTO:
+ guard let reminder = event.payload.reminder else { break }
+ do {
+ try session.saveReminder(payload: reminder, cache: nil)
+ } catch {
+ log.error("Failed to update reminder in due notification: \(error)")
+ }
+
+ case let event as ReminderDeletedEventDTO:
+ let messageId = event.messageId
+ session.deleteReminder(messageId: messageId)
+ default:
+ break
+ }
+ return event
+ }
+}
diff --git a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
index c760f6358d4..689d6e61952 100644
--- a/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/EventPayload.swift
@@ -40,6 +40,7 @@ class EventPayload: Decodable {
case messageId = "message_id"
case aiMessage = "ai_message"
case draft
+ case reminder
}
let eventType: EventType
@@ -77,6 +78,7 @@ class EventPayload: Decodable {
let messageId: String?
let aiMessage: String?
let draft: DraftPayload?
+ let reminder: ReminderPayload?
init(
eventType: EventType,
@@ -109,7 +111,8 @@ class EventPayload: Decodable {
aiState: String? = nil,
messageId: String? = nil,
aiMessage: String? = nil,
- draft: DraftPayload? = nil
+ draft: DraftPayload? = nil,
+ reminder: ReminderPayload? = nil
) {
self.eventType = eventType
self.connectionId = connectionId
@@ -142,6 +145,7 @@ class EventPayload: Decodable {
self.messageId = messageId
self.aiMessage = aiMessage
self.draft = draft
+ self.reminder = reminder
}
required init(from decoder: Decoder) throws {
@@ -179,6 +183,7 @@ class EventPayload: Decodable {
messageId = try container.decodeIfPresent(String.self, forKey: .messageId)
aiMessage = try container.decodeIfPresent(String.self, forKey: .aiMessage)
draft = try container.decodeIfPresent(DraftPayload.self, forKey: .draft)
+ reminder = try container.decodeIfPresent(ReminderPayload.self, forKey: .reminder)
}
func event() throws -> Event {
diff --git a/Sources/StreamChat/WebSocketClient/Events/EventType.swift b/Sources/StreamChat/WebSocketClient/Events/EventType.swift
index 1268e2b955e..504ef2e2502 100644
--- a/Sources/StreamChat/WebSocketClient/Events/EventType.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/EventType.swift
@@ -155,13 +155,27 @@ public extension EventType {
// When an AI typing indicator has been stopped.
static let aiTypingIndicatorStop: Self = "ai_indicator.stop"
- // MARK: Drafts
+ // MARK: - Drafts
/// When a draft was updated.
static let draftUpdated: Self = "draft.updated"
/// When a draft was deleted.
static let draftDeleted: Self = "draft.deleted"
+
+ // MARK: - Reminders
+
+ /// When a reminder was created.
+ static let messageReminderCreated: Self = "reminder.created"
+
+ /// When a reminder was updated.
+ static let messageReminderUpdated: Self = "reminder.updated"
+
+ /// When a reminder was deleted.
+ static let messageReminderDeleted: Self = "reminder.deleted"
+
+ /// When a reminder is due.
+ static let messageReminderDue: Self = "notification.reminder_due"
}
extension EventType {
@@ -232,6 +246,10 @@ extension EventType {
case .aiTypingIndicatorStop: return try AIIndicatorStopEventDTO(from: response)
case .draftUpdated: return try DraftUpdatedEventDTO(from: response)
case .draftDeleted: return try DraftDeletedEventDTO(from: response)
+ case .messageReminderCreated: return try ReminderCreatedEventDTO(from: response)
+ case .messageReminderUpdated: return try ReminderUpdatedEventDTO(from: response)
+ case .messageReminderDeleted: return try ReminderDeletedEventDTO(from: response)
+ case .messageReminderDue: return try ReminderDueNotificationEventDTO(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 78cb41587a4..a90e7adb296 100644
--- a/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift
+++ b/Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift
@@ -294,7 +294,8 @@ private extension MessagePayload {
readBy: [],
poll: nil,
textUpdatedAt: messageTextUpdatedAt,
- draftReply: nil
+ draftReply: nil,
+ reminder: nil
)
}
}
diff --git a/Sources/StreamChat/WebSocketClient/Events/ReminderEvents.swift b/Sources/StreamChat/WebSocketClient/Events/ReminderEvents.swift
new file mode 100644
index 00000000000..865d6f0e12a
--- /dev/null
+++ b/Sources/StreamChat/WebSocketClient/Events/ReminderEvents.swift
@@ -0,0 +1,201 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+
+/// Triggered when a message reminder is created.
+public class MessageReminderCreatedEvent: Event {
+ /// The message ID associated with the reminder.
+ public let messageId: MessageId
+
+ /// The reminder that was created.
+ public let reminder: MessageReminder
+
+ /// The channel identifier where the reminder was created.
+ public var cid: ChannelId { reminder.channel.cid }
+
+ /// The event timestamp.
+ public let createdAt: Date
+
+ init(messageId: MessageId, reminder: MessageReminder, createdAt: Date) {
+ self.messageId = messageId
+ self.reminder = reminder
+ self.createdAt = createdAt
+ }
+}
+
+class ReminderCreatedEventDTO: EventDTO {
+ let messageId: MessageId
+ let reminder: ReminderPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ messageId = try response.value(at: \.messageId)
+ reminder = try response.value(at: \.reminder)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: any DatabaseSession) -> Event? {
+ guard
+ let reminderDTO = try? session.saveReminder(payload: reminder, cache: nil),
+ let reminderModel = try? reminderDTO.asModel()
+ else { return nil }
+
+ return MessageReminderCreatedEvent(
+ messageId: messageId,
+ reminder: reminderModel,
+ createdAt: createdAt
+ )
+ }
+}
+
+/// Triggered when a message reminder is updated.
+public class MessageReminderUpdatedEvent: Event {
+ /// The message ID associated with the reminder.
+ public let messageId: MessageId
+
+ /// The reminder that was updated.
+ public let reminder: MessageReminder
+
+ /// The channel identifier where the reminder was updated.
+ public var cid: ChannelId { reminder.channel.cid }
+
+ /// The event timestamp.
+ public let createdAt: Date
+
+ init(messageId: MessageId, reminder: MessageReminder, createdAt: Date) {
+ self.messageId = messageId
+ self.reminder = reminder
+ self.createdAt = createdAt
+ }
+}
+
+class ReminderUpdatedEventDTO: EventDTO {
+ let messageId: MessageId
+ let reminder: ReminderPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ messageId = try response.value(at: \.messageId)
+ reminder = try response.value(at: \.reminder)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: any DatabaseSession) -> Event? {
+ guard
+ let reminderDTO = try? session.saveReminder(payload: reminder, cache: nil),
+ let reminderModel = try? reminderDTO.asModel()
+ else { return nil }
+
+ return MessageReminderUpdatedEvent(
+ messageId: messageId,
+ reminder: reminderModel,
+ createdAt: createdAt
+ )
+ }
+}
+
+/// Triggered when a message reminder is deleted.
+public class MessageReminderDeletedEvent: Event {
+ /// The message ID associated with the reminder.
+ public let messageId: MessageId
+
+ /// The reminder information before deletion.
+ public let reminder: MessageReminder
+
+ /// The channel identifier where the reminder was deleted.
+ public var cid: ChannelId { reminder.channel.cid }
+
+ /// The event timestamp.
+ public let createdAt: Date
+
+ init(messageId: MessageId, reminder: MessageReminder, createdAt: Date) {
+ self.messageId = messageId
+ self.reminder = reminder
+ self.createdAt = createdAt
+ }
+}
+
+class ReminderDeletedEventDTO: EventDTO {
+ let messageId: MessageId
+ let reminder: ReminderPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ messageId = try response.value(at: \.messageId)
+ reminder = try response.value(at: \.reminder)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: any DatabaseSession) -> Event? {
+ // For deletion events, we need to construct the reminder model before deleting it
+ guard
+ let reminderDTO = try? session.saveReminder(payload: reminder, cache: nil),
+ let reminderModel = try? reminderDTO.asModel()
+ else { return nil }
+
+ // Delete the reminder from the database
+ session.deleteReminder(messageId: messageId)
+
+ return MessageReminderDeletedEvent(
+ messageId: messageId,
+ reminder: reminderModel,
+ createdAt: createdAt
+ )
+ }
+}
+
+/// Triggered when a reminder is due and a notification should be shown.
+public class MessageReminderDueEvent: Event {
+ /// The message ID associated with the reminder.
+ public let messageId: MessageId
+
+ /// The reminder that is due.
+ public let reminder: MessageReminder
+
+ /// The channel identifier where the reminder is due.
+ public var cid: ChannelId { reminder.channel.cid }
+
+ /// The event timestamp.
+ public let createdAt: Date
+
+ init(messageId: MessageId, reminder: MessageReminder, createdAt: Date) {
+ self.messageId = messageId
+ self.reminder = reminder
+ self.createdAt = createdAt
+ }
+}
+
+class ReminderDueNotificationEventDTO: EventDTO {
+ let messageId: MessageId
+ let reminder: ReminderPayload
+ let createdAt: Date
+ let payload: EventPayload
+
+ init(from response: EventPayload) throws {
+ messageId = try response.value(at: \.messageId)
+ reminder = try response.value(at: \.reminder)
+ createdAt = try response.value(at: \.createdAt)
+ payload = response
+ }
+
+ func toDomainEvent(session: any DatabaseSession) -> Event? {
+ guard
+ let reminderDTO = try? session.saveReminder(payload: reminder, cache: nil),
+ let reminderModel = try? reminderDTO.asModel()
+ else { return nil }
+
+ return MessageReminderDueEvent(
+ messageId: messageId,
+ reminder: reminderModel,
+ createdAt: createdAt
+ )
+ }
+}
diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj
index b20d78df1c0..f694bd64e7b 100644
--- a/StreamChat.xcodeproj/project.pbxproj
+++ b/StreamChat.xcodeproj/project.pbxproj
@@ -1674,6 +1674,19 @@
ADA3572F269C807A004AD8E9 /* ChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA3572D269C562A004AD8E9 /* ChatChannelHeaderView.swift */; };
ADA5A0F8276790C100E1C465 /* ChatMessageListDateSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA5A0F7276790C100E1C465 /* ChatMessageListDateSeparatorView.swift */; };
ADA5A0F9276790C100E1C465 /* ChatMessageListDateSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA5A0F7276790C100E1C465 /* ChatMessageListDateSeparatorView.swift */; };
+ ADA83B3E2D974DCC003B3928 /* MessageReminderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B3C2D974DCB003B3928 /* MessageReminderListController.swift */; };
+ ADA83B3F2D974DCC003B3928 /* MessageReminderListController+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B3D2D974DCB003B3928 /* MessageReminderListController+Combine.swift */; };
+ ADA83B402D974DCC003B3928 /* MessageReminderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B3C2D974DCB003B3928 /* MessageReminderListController.swift */; };
+ ADA83B412D974DCC003B3928 /* MessageReminderListController+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B3D2D974DCB003B3928 /* MessageReminderListController+Combine.swift */; };
+ ADA83B452D97511E003B3928 /* MessageReminderListController+Combine_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B432D97511E003B3928 /* MessageReminderListController+Combine_Tests.swift */; };
+ ADA83B472D976D9C003B3928 /* MessageReminderListController_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B462D976D9C003B3928 /* MessageReminderListController_Tests.swift */; };
+ ADA83B492D976ED7003B3928 /* MessageReminder_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B482D976EC7003B3928 /* MessageReminder_Mock.swift */; };
+ ADA83B4B2D977D59003B3928 /* ReminderEndpoints_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B4A2D977D59003B3928 /* ReminderEndpoints_Tests.swift */; };
+ ADA83B4D2D977D64003B3928 /* ReminderEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B4C2D977D64003B3928 /* ReminderEndpoints.swift */; };
+ ADA83B4E2D977D64003B3928 /* ReminderEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B4C2D977D64003B3928 /* ReminderEndpoints.swift */; };
+ ADA83B502D978050003B3928 /* ReminderPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B4F2D978050003B3928 /* ReminderPayloads.swift */; };
+ ADA83B512D978050003B3928 /* ReminderPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B4F2D978050003B3928 /* ReminderPayloads.swift */; };
+ ADA83B532D97805A003B3928 /* ReminderPayloads_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA83B522D97805A003B3928 /* ReminderPayloads_Tests.swift */; };
ADA8EBE928CFD52F00DB9B03 /* TextViewUserMentionsHandler_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA8EBE828CFD52F00DB9B03 /* TextViewUserMentionsHandler_Mock.swift */; };
ADA8EBEB28CFD82C00DB9B03 /* ChatMessageContentViewDelegate_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA8EBEA28CFD82C00DB9B03 /* ChatMessageContentViewDelegate_Mock.swift */; };
ADA9DB892BCEF06B00C4AE3B /* ThreadReadDTO_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA9DB882BCEF06B00C4AE3B /* ThreadReadDTO_Tests.swift */; };
@@ -1683,9 +1696,33 @@
ADAA377125E43C3700C31528 /* ChatSuggestionsVC_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD5A9E725DE8AF6006DC88A /* ChatSuggestionsVC_Tests.swift */; };
ADAA9F412B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADAA9F402B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift */; };
ADAC47AA275A7C960027B672 /* ChatMessageContentView_Documentation_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADAC47A9275A7C960027B672 /* ChatMessageContentView_Documentation_Tests.swift */; };
+ ADB2087F2D849184003F1059 /* MessageReminderListQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB2087E2D849184003F1059 /* MessageReminderListQuery.swift */; };
+ ADB208802D849184003F1059 /* MessageReminderListQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB2087E2D849184003F1059 /* MessageReminderListQuery.swift */; };
+ ADB208822D8494F0003F1059 /* MessageReminderListQuery_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB208812D8494F0003F1059 /* MessageReminderListQuery_Tests.swift */; };
ADB22F7C25F1626200853C92 /* OnlineIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB22F7A25F1626200853C92 /* OnlineIndicatorView.swift */; };
ADB22F7D25F1626200853C92 /* ChatPresenceAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB22F7B25F1626200853C92 /* ChatPresenceAvatarView.swift */; };
ADB4166C26208F1C00E623E3 /* AttachmentPreviewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB4165026208C7900E623E3 /* AttachmentPreviewProvider.swift */; };
+ ADB8B8EA2D8890B900549C95 /* MessageReminderDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8E92D8890B900549C95 /* MessageReminderDTO.swift */; };
+ ADB8B8EB2D8890B900549C95 /* MessageReminderDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8E92D8890B900549C95 /* MessageReminderDTO.swift */; };
+ ADB8B8ED2D8890E000549C95 /* MessageReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8EC2D8890E000549C95 /* MessageReminder.swift */; };
+ ADB8B8EE2D8890E000549C95 /* MessageReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8EC2D8890E000549C95 /* MessageReminder.swift */; };
+ ADB8B8F02D8A493900549C95 /* ReminderPayload.json in Resources */ = {isa = PBXBuildFile; fileRef = ADB8B8EF2D8A493900549C95 /* ReminderPayload.json */; };
+ ADB8B8F22D8ADA0700549C95 /* RemindersRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8F12D8ADA0700549C95 /* RemindersRepository.swift */; };
+ ADB8B8F32D8ADA0700549C95 /* RemindersRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8F12D8ADA0700549C95 /* RemindersRepository.swift */; };
+ ADB8B8F52D8ADC9400549C95 /* DemoReminderListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8F42D8ADC9400549C95 /* DemoReminderListVC.swift */; };
+ ADB8B8F72D8B846D00549C95 /* RemindersRepository_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8F62D8B846D00549C95 /* RemindersRepository_Tests.swift */; };
+ ADB8B8F92D8B8A0C00549C95 /* RemindersRepository_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8F82D8B8A0C00549C95 /* RemindersRepository_Mock.swift */; };
+ ADB8B8FB2D8B904D00549C95 /* MessageController+Reminders_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B8FA2D8B904D00549C95 /* MessageController+Reminders_Tests.swift */; };
+ ADB8B9022D8C701000549C95 /* ReminderUpdated.json in Resources */ = {isa = PBXBuildFile; fileRef = ADB8B9012D8C700800549C95 /* ReminderUpdated.json */; };
+ ADB8B9042D8C701500549C95 /* ReminderCreated.json in Resources */ = {isa = PBXBuildFile; fileRef = ADB8B9032D8C701500549C95 /* ReminderCreated.json */; };
+ ADB8B9062D8C702A00549C95 /* ReminderDeleted.json in Resources */ = {isa = PBXBuildFile; fileRef = ADB8B9052D8C701F00549C95 /* ReminderDeleted.json */; };
+ ADB8B9082D8C703300549C95 /* ReminderDue.json in Resources */ = {isa = PBXBuildFile; fileRef = ADB8B9072D8C702E00549C95 /* ReminderDue.json */; };
+ ADB8B90A2D8C756600549C95 /* ReminderEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B9092D8C756600549C95 /* ReminderEvents.swift */; };
+ ADB8B90B2D8C756600549C95 /* ReminderEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B9092D8C756600549C95 /* ReminderEvents.swift */; };
+ ADB8B90D2D8C784500549C95 /* ReminderEvents_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B90C2D8C784500549C95 /* ReminderEvents_Tests.swift */; };
+ ADB8B90F2D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B90E2D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift */; };
+ ADB8B9102D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B90E2D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift */; };
+ ADB8B9122D8C7B2D00549C95 /* ReminderUpdaterMiddleware_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB8B9112D8C7B2D00549C95 /* ReminderUpdaterMiddleware_Tests.swift */; };
ADB951A1291BD7CC00800554 /* UploadedAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB951A0291BD7CC00800554 /* UploadedAttachment.swift */; };
ADB951A2291BD7CC00800554 /* UploadedAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB951A0291BD7CC00800554 /* UploadedAttachment.swift */; };
ADB951AB291C1DE400800554 /* AttachmentUploader_Spy.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB951A8291C1DDC00800554 /* AttachmentUploader_Spy.swift */; };
@@ -2456,8 +2493,8 @@
C1FC2F8C27416E1F0062530F /* UIImage+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13C74D4273932D200F93B34 /* UIImage+SwiftyGif.swift */; };
C1FC2F8D27416E1F0062530F /* NSImageView+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13C74D6273932D200F93B34 /* NSImageView+SwiftyGif.swift */; };
C1FC2F8E27416E1F0062530F /* NSImage+SwiftyGif.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13C74D3273932D200F93B34 /* NSImage+SwiftyGif.swift */; };
- C1FFD9F927ECC7C7008A6848 /* Filter+ChatChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFD9F827ECC7C7008A6848 /* Filter+ChatChannel.swift */; };
- C1FFD9FA27ECC7C7008A6848 /* Filter+ChatChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFD9F827ECC7C7008A6848 /* Filter+ChatChannel.swift */; };
+ C1FFD9F927ECC7C7008A6848 /* Filter+predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFD9F827ECC7C7008A6848 /* Filter+predicate.swift */; };
+ C1FFD9FA27ECC7C7008A6848 /* Filter+predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFD9F827ECC7C7008A6848 /* Filter+predicate.swift */; };
CF01EB7B288A2B7200B426B8 /* ChatChannelListLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF01EB7A288A2B7200B426B8 /* ChatChannelListLoadingView.swift */; };
CF01EB7C288A2B7200B426B8 /* ChatChannelListLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF01EB7A288A2B7200B426B8 /* ChatChannelListLoadingView.swift */; };
CF14397D2886374900898ECA /* ChatChannelListLoadingViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF14397C2886374900898ECA /* ChatChannelListLoadingViewCell.swift */; };
@@ -4389,6 +4426,15 @@
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 = ""; };
+ ADA83B3C2D974DCB003B3928 /* MessageReminderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminderListController.swift; sourceTree = ""; };
+ ADA83B3D2D974DCB003B3928 /* MessageReminderListController+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReminderListController+Combine.swift"; sourceTree = ""; };
+ ADA83B432D97511E003B3928 /* MessageReminderListController+Combine_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReminderListController+Combine_Tests.swift"; sourceTree = ""; };
+ ADA83B462D976D9C003B3928 /* MessageReminderListController_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminderListController_Tests.swift; sourceTree = ""; };
+ ADA83B482D976EC7003B3928 /* MessageReminder_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminder_Mock.swift; sourceTree = ""; };
+ ADA83B4A2D977D59003B3928 /* ReminderEndpoints_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderEndpoints_Tests.swift; sourceTree = ""; };
+ ADA83B4C2D977D64003B3928 /* ReminderEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderEndpoints.swift; sourceTree = ""; };
+ ADA83B4F2D978050003B3928 /* ReminderPayloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderPayloads.swift; sourceTree = ""; };
+ ADA83B522D97805A003B3928 /* ReminderPayloads_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderPayloads_Tests.swift; sourceTree = ""; };
ADA8EBE828CFD52F00DB9B03 /* TextViewUserMentionsHandler_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewUserMentionsHandler_Mock.swift; sourceTree = ""; };
ADA8EBEA28CFD82C00DB9B03 /* ChatMessageContentViewDelegate_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageContentViewDelegate_Mock.swift; sourceTree = ""; };
ADA9DB882BCEF06B00C4AE3B /* ThreadReadDTO_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReadDTO_Tests.swift; sourceTree = ""; };
@@ -4397,9 +4443,27 @@
ADAA10EA2B90D589007AB03F /* FakeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTimer.swift; sourceTree = ""; };
ADAA9F402B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewMentionedUsersHandler_Tests.swift; sourceTree = ""; };
ADAC47A9275A7C960027B672 /* ChatMessageContentView_Documentation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageContentView_Documentation_Tests.swift; sourceTree = ""; };
+ ADB2087E2D849184003F1059 /* MessageReminderListQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminderListQuery.swift; sourceTree = ""; };
+ ADB208812D8494F0003F1059 /* MessageReminderListQuery_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminderListQuery_Tests.swift; sourceTree = ""; };
ADB22F7A25F1626200853C92 /* OnlineIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnlineIndicatorView.swift; sourceTree = ""; };
ADB22F7B25F1626200853C92 /* ChatPresenceAvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresenceAvatarView.swift; sourceTree = ""; };
ADB4165026208C7900E623E3 /* AttachmentPreviewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewProvider.swift; sourceTree = ""; };
+ ADB8B8E92D8890B900549C95 /* MessageReminderDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminderDTO.swift; sourceTree = ""; };
+ ADB8B8EC2D8890E000549C95 /* MessageReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReminder.swift; sourceTree = ""; };
+ ADB8B8EF2D8A493900549C95 /* ReminderPayload.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ReminderPayload.json; sourceTree = ""; };
+ ADB8B8F12D8ADA0700549C95 /* RemindersRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersRepository.swift; sourceTree = ""; };
+ ADB8B8F42D8ADC9400549C95 /* DemoReminderListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoReminderListVC.swift; sourceTree = ""; };
+ ADB8B8F62D8B846D00549C95 /* RemindersRepository_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersRepository_Tests.swift; sourceTree = ""; };
+ ADB8B8F82D8B8A0C00549C95 /* RemindersRepository_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersRepository_Mock.swift; sourceTree = ""; };
+ ADB8B8FA2D8B904D00549C95 /* MessageController+Reminders_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageController+Reminders_Tests.swift"; sourceTree = ""; };
+ ADB8B9012D8C700800549C95 /* ReminderUpdated.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ReminderUpdated.json; sourceTree = ""; };
+ ADB8B9032D8C701500549C95 /* ReminderCreated.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ReminderCreated.json; sourceTree = ""; };
+ ADB8B9052D8C701F00549C95 /* ReminderDeleted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ReminderDeleted.json; sourceTree = ""; };
+ ADB8B9072D8C702E00549C95 /* ReminderDue.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ReminderDue.json; sourceTree = ""; };
+ ADB8B9092D8C756600549C95 /* ReminderEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderEvents.swift; sourceTree = ""; };
+ ADB8B90C2D8C784500549C95 /* ReminderEvents_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderEvents_Tests.swift; sourceTree = ""; };
+ ADB8B90E2D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderUpdaterMiddleware.swift; sourceTree = ""; };
+ ADB8B9112D8C7B2D00549C95 /* ReminderUpdaterMiddleware_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderUpdaterMiddleware_Tests.swift; sourceTree = ""; };
ADB951A0291BD7CC00800554 /* UploadedAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadedAttachment.swift; sourceTree = ""; };
ADB951A8291C1DDC00800554 /* AttachmentUploader_Spy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploader_Spy.swift; sourceTree = ""; };
ADB951AC291C22DB00800554 /* UploadedAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadedAttachment.swift; sourceTree = ""; };
@@ -4638,7 +4702,7 @@
C1EE53A827BA662B00B1A6CA /* QueuedRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueuedRequestDTO.swift; sourceTree = ""; };
C1EFF3F2285E459C0057B91B /* IdentifiableModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableModel.swift; sourceTree = ""; };
C1EFF3F728633B5D0057B91B /* IdentifiableModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableModel_Tests.swift; sourceTree = ""; };
- C1FFD9F827ECC7C7008A6848 /* Filter+ChatChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Filter+ChatChannel.swift"; sourceTree = ""; };
+ C1FFD9F827ECC7C7008A6848 /* Filter+predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Filter+predicate.swift"; sourceTree = ""; };
CF01EB7A288A2B7200B426B8 /* ChatChannelListLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListLoadingView.swift; sourceTree = ""; };
CF14397C2886374900898ECA /* ChatChannelListLoadingViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListLoadingViewCell.swift; sourceTree = ""; };
CF14397F288637AD00898ECA /* ChatChannelListLoadingViewCellContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListLoadingViewCellContentView.swift; sourceTree = ""; };
@@ -5539,6 +5603,7 @@
79280F402484F4DD00CDEB89 /* Events */ = {
isa = PBXGroup;
children = (
+ ADB8B9092D8C756600549C95 /* ReminderEvents.swift */,
79280F46248515FA00CDEB89 /* ChannelEvents.swift */,
79280F4A248523C000CDEB89 /* ConnectionEvents.swift */,
79280F412484F4EC00CDEB89 /* Event.swift */,
@@ -5582,6 +5647,7 @@
792A4F18247EA97000EAF71D /* DTOs */ = {
isa = PBXGroup;
children = (
+ ADB8B8E92D8890B900549C95 /* MessageReminderDTO.swift */,
DABC6AC7254707CB00A8FC78 /* AttachmentDTO.swift */,
AD52A2182804850700D0157E /* ChannelConfigDTO.swift */,
799C942A247D2FB9001F1104 /* ChannelDTO.swift */,
@@ -5663,7 +5729,7 @@
isa = PBXGroup;
children = (
792A4F432480107A00EAF71D /* Filter.swift */,
- C1FFD9F827ECC7C7008A6848 /* Filter+ChatChannel.swift */,
+ C1FFD9F827ECC7C7008A6848 /* Filter+predicate.swift */,
792A4F4C248011E500EAF71D /* ChannelListQuery.swift */,
882C5745252C6FDF00E60C44 /* ChannelMemberListQuery.swift */,
792A4F422480107A00EAF71D /* ChannelQuery.swift */,
@@ -5671,6 +5737,7 @@
AD6E32A02BBC50110073831B /* ThreadListQuery.swift */,
AD6E32A32BBC502D0073831B /* ThreadQuery.swift */,
AD545E622D528271008FD399 /* DraftListQuery.swift */,
+ ADB2087E2D849184003F1059 /* MessageReminderListQuery.swift */,
AD0CC0112BDBC1BF005E2C66 /* ReactionListQuery.swift */,
7978FBB926E15A58002CA2DF /* MessageSearchQuery.swift */,
792A4F442480107A00EAF71D /* Pagination.swift */,
@@ -5745,6 +5812,8 @@
796610B7248E64EC00761629 /* EventMiddlewares */ = {
isa = PBXGroup;
children = (
+ ADB8B90E2D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift */,
+ AD545E732D5A79B5008FD399 /* DraftUpdaterMiddleware.swift */,
79896D63250A62EE00BA8F1C /* ChannelReadUpdaterMiddleware.swift */,
79158CF325F133FB00186102 /* ChannelTruncatedEventMiddleware.swift */,
79AF43B32632AF1B00E75CDA /* ChannelVisibilityEventMiddleware.swift */,
@@ -5766,6 +5835,7 @@
79682C4724BF37550071578E /* Payloads */ = {
isa = PBXGroup;
children = (
+ ADA83B4F2D978050003B3928 /* ReminderPayloads.swift */,
ADF2BBEA2B9B622B0069D467 /* AppSettingsPayload.swift */,
DA9985ED24E175AA000E9885 /* ChannelCodingKeys.swift */,
DAFAD6A224DD8E1A0043ED06 /* ChannelEditDetailPayload.swift */,
@@ -5827,6 +5897,7 @@
79877A122498E4EE00015F8B /* Endpoints */ = {
isa = PBXGroup;
children = (
+ ADA83B4C2D977D64003B3928 /* ReminderEndpoints.swift */,
88E26D7C2580F95300F55AB5 /* AttachmentEndpoints.swift */,
82C18FDB2C10C8E600C5283C /* BlockedUserPayload.swift */,
79877A132498E4EE00015F8B /* ChannelEndpoints.swift */,
@@ -5935,6 +6006,7 @@
79877A022498E4BB00015F8B /* Device.swift */,
79877A032498E4BB00015F8B /* Member.swift */,
AD70DC3B2ADEF09C00CFC3B7 /* MessageModerationDetails.swift */,
+ ADB8B8EC2D8890E000549C95 /* MessageReminder.swift */,
AD7AC98B260A94C6004AADA5 /* MessagePinning.swift */,
8899BC52254318CC003CB98B /* MessageReaction.swift */,
AD8258A22BD2939500B9ED74 /* MessageReactionGroup.swift */,
@@ -5982,6 +6054,7 @@
DAE566F22500F97E00E39431 /* ChannelController */,
DAE566F32500F98D00E39431 /* ChannelListController */,
79C5CBF925F671AE00D98001 /* ChannelWatcherListController */,
+ ADA83B352D9742CD003B3928 /* MessageReminderListController */,
AD9490552BF3BA8000E69224 /* ThreadListController */,
ADF34F6925CD6A0100AD637C /* ConnectionController */,
DAE566F42500F99900E39431 /* CurrentUserController */,
@@ -6602,6 +6675,7 @@
8A62705F24BE31B20040BFD6 /* Events */ = {
isa = PBXGroup;
children = (
+ ADB8B8FE2D8C6FED00549C95 /* Reminder */,
AD545E8A2D5D8095008FD399 /* Draft */,
84E46A332CFA1B73000CBDDE /* AIIndicator */,
ADE57B802C3C5C4600DD6B88 /* Thread */,
@@ -6785,6 +6859,7 @@
A3227ECA284A607D00EBE6CC /* Screens */ = {
isa = PBXGroup;
children = (
+ ADB8B8F42D8ADC9400549C95 /* DemoReminderListVC.swift */,
AD545E682D5531B9008FD399 /* DemoDraftMessageListVC.swift */,
ADD328592C04DD8300BAD0E9 /* DemoAppTabBarController.swift */,
C10B5C712A1F794A006A5BCB /* MembersViewController.swift */,
@@ -6822,6 +6897,7 @@
A344074F27D753530044F150 /* Models + Extensions */ = {
isa = PBXGroup;
children = (
+ ADA83B482D976EC7003B3928 /* MessageReminder_Mock.swift */,
ADA03A242D65041300DFE048 /* DraftMessage_Mock.swift */,
A344075027D753530044F150 /* ChannelUnreadCount_Mock.swift */,
A344075127D753530044F150 /* ChatChannel_Mock.swift */,
@@ -6995,6 +7071,8 @@
A364D08D27D0BD8E0029857A /* EventMiddlewares */ = {
isa = PBXGroup;
children = (
+ ADB8B9112D8C7B2D00549C95 /* ReminderUpdaterMiddleware_Tests.swift */,
+ AD545E8D2D5D827B008FD399 /* DraftUpdaterMiddleware_Tests.swift */,
79896D65250A6D1500BA8F1C /* ChannelReadUpdaterMiddleware_Tests.swift */,
79158CEA25F0EADF00186102 /* ChannelTruncatedEventMiddleware_Tests.swift */,
79342EEB2632C7770018F0F7 /* ChannelVisibilityEventMiddleware_Tests.swift */,
@@ -7016,6 +7094,7 @@
A364D08E27D0BDB20029857A /* Events */ = {
isa = PBXGroup;
children = (
+ ADB8B90C2D8C784500549C95 /* ReminderEvents_Tests.swift */,
AD545E862D5D805A008FD399 /* DraftEvents_Tests.swift */,
8A62706B24BF3DBC0040BFD6 /* ChannelEvents_Tests.swift */,
84A1D2EF26AB10DB00014712 /* EventDecoder_Tests.swift */,
@@ -7069,6 +7148,7 @@
A364D09327D0BF330029857A /* Endpoints */ = {
isa = PBXGroup;
children = (
+ ADA83B4A2D977D59003B3928 /* ReminderEndpoints_Tests.swift */,
AD545E762D5BB3D6008FD399 /* DraftEndpoints_Tests.swift */,
AD8C7C652BA46A4A00260715 /* AppEndpoints_Tests.swift */,
88381E8625825A240047A6A3 /* AttachmentEndpoints_Tests.swift */,
@@ -7096,6 +7176,7 @@
A364D09427D0BF3A0029857A /* Payloads */ = {
isa = PBXGroup;
children = (
+ ADA83B522D97805A003B3928 /* ReminderPayloads_Tests.swift */,
AD545E6A2D5650B5008FD399 /* DraftPayloads_Tests.swift */,
AD8C7C622BA464E600260715 /* AppSettingsPayload_Tests.swift */,
AD6E32952BBB10890073831B /* ThreadListPayload_Tests.swift */,
@@ -7316,6 +7397,7 @@
A364D0A327D126490029857A /* Repositories */ = {
isa = PBXGroup;
children = (
+ ADB8B8F62D8B846D00549C95 /* RemindersRepository_Tests.swift */,
AD545E782D5BC14E008FD399 /* DraftMessagesRepository_Tests.swift */,
C11BAA4C2907EC7B004C5EA4 /* AuthenticationRepository_Tests.swift */,
C18514FC292E34E10033387E /* ConnectionRepository_Tests.swift */,
@@ -7332,6 +7414,7 @@
A364D0A527D127E00029857A /* Controllers */ = {
isa = PBXGroup;
children = (
+ ADA83B442D97511E003B3928 /* MessageReminderListController */,
AD94905E2BF65CC500E69224 /* ThreadListController */,
A364D0A827D128650029857A /* ChannelController */,
A364D0A927D128830029857A /* ChannelListController */,
@@ -7464,6 +7547,7 @@
isa = PBXGroup;
children = (
AD545E802D5D0006008FD399 /* MessageController+Drafts_Tests.swift */,
+ ADB8B8FA2D8B904D00549C95 /* MessageController+Reminders_Tests.swift */,
F649B2362500F785008F98C8 /* MessageController_Tests.swift */,
DAF1BED625066128003CEDC0 /* MessageController+Combine_Tests.swift */,
DAF1BED225066107003CEDC0 /* MessageController+SwiftUI_Tests.swift */,
@@ -7513,6 +7597,7 @@
A364D0B727D12A520029857A /* Query */ = {
isa = PBXGroup;
children = (
+ ADB208812D8494F0003F1059 /* MessageReminderListQuery_Tests.swift */,
AD545E842D5D7591008FD399 /* DraftListQuery_Tests.swift */,
A3C7BAD027E4E02700BBF4FA /* ChannelListFilterScope_Tests.swift */,
799F611A2530B62C007F218C /* ChannelListQuery_Tests.swift */,
@@ -8140,6 +8225,7 @@
A3C729552840BA4800FFE8B4 /* JSONs */ = {
isa = PBXGroup;
children = (
+ ADB8B8EF2D8A493900549C95 /* ReminderPayload.json */,
AD545E6C2D565316008FD399 /* DraftMessage.json */,
798779F72498E47700015F8B /* Channel.json */,
AD6E32972BBB13650073831B /* Thread.json */,
@@ -8775,6 +8861,24 @@
path = TitleContainerView;
sourceTree = "";
};
+ ADA83B352D9742CD003B3928 /* MessageReminderListController */ = {
+ isa = PBXGroup;
+ children = (
+ ADA83B3C2D974DCB003B3928 /* MessageReminderListController.swift */,
+ ADA83B3D2D974DCB003B3928 /* MessageReminderListController+Combine.swift */,
+ );
+ path = MessageReminderListController;
+ sourceTree = "";
+ };
+ ADA83B442D97511E003B3928 /* MessageReminderListController */ = {
+ isa = PBXGroup;
+ children = (
+ ADA83B462D976D9C003B3928 /* MessageReminderListController_Tests.swift */,
+ ADA83B432D97511E003B3928 /* MessageReminderListController+Combine_Tests.swift */,
+ );
+ path = MessageReminderListController;
+ sourceTree = "";
+ };
ADAA10E92B90D554007AB03F /* FakeTimer */ = {
isa = PBXGroup;
children = (
@@ -8801,6 +8905,17 @@
path = QuotedChatMessageView;
sourceTree = "";
};
+ ADB8B8FE2D8C6FED00549C95 /* Reminder */ = {
+ isa = PBXGroup;
+ children = (
+ ADB8B9072D8C702E00549C95 /* ReminderDue.json */,
+ ADB8B9052D8C701F00549C95 /* ReminderDeleted.json */,
+ ADB8B9012D8C700800549C95 /* ReminderUpdated.json */,
+ ADB8B9032D8C701500549C95 /* ReminderCreated.json */,
+ );
+ path = Reminder;
+ sourceTree = "";
+ };
ADB951A3291BD7F700800554 /* CDNClient */ = {
isa = PBXGroup;
children = (
@@ -9136,6 +9251,7 @@
C12D0A5E28FD58CE0099895A /* Repositories */ = {
isa = PBXGroup;
children = (
+ ADB8B8F82D8B8A0C00549C95 /* RemindersRepository_Mock.swift */,
AD545E7E2D5CFC36008FD399 /* DraftMessagesRepository_Mock.swift */,
C12D0A5F28FD59B60099895A /* AuthenticationRepository_Mock.swift */,
A344074E27D753530044F150 /* ConnectionRepository_Mock.swift */,
@@ -9270,6 +9386,7 @@
C1E8AD59278DDC500041B775 /* Repositories */ = {
isa = PBXGroup;
children = (
+ ADB8B8F12D8ADA0700549C95 /* RemindersRepository.swift */,
C135A1CA28F45F6B0058EFB6 /* AuthenticationRepository.swift */,
88206FC325B18C88009D086A /* ConnectionRepository.swift */,
C1B0B38527BFE8AB00C8207D /* MessageRepository.swift */,
@@ -10236,6 +10353,7 @@
A311B41227E8B9B900CFCF6D /* UserStopTyping.json in Resources */,
A311B3F427E8B99800CFCF6D /* ChannelUpdated.json in Resources */,
A311B42627E8B9CE00CFCF6D /* MessageReactionPayload+DefaultExtraData.json in Resources */,
+ ADB8B9082D8C703300549C95 /* ReminderDue.json in Resources */,
A311B3FC27E8B9A800CFCF6D /* MessageUpdated.json in Resources */,
C1616DB228DC4D7F00FF993B /* UserGloballyBanned.json in Resources */,
A311B3DC27E8B98C00CFCF6D /* MessagePayload.json in Resources */,
@@ -10265,6 +10383,7 @@
A311B3DA27E8B98C00CFCF6D /* CurrentUser.json in Resources */,
A311B3D527E8B98C00CFCF6D /* ChannelPayload.json in Resources */,
A368E71627F33E16009063C1 /* MissingEventsPayload-IncompleteChannel.json in Resources */,
+ ADB8B9022D8C701000549C95 /* ReminderUpdated.json in Resources */,
A311B40B27E8B9AD00CFCF6D /* NotificationMessageNew.json in Resources */,
A311B41027E8B9B300CFCF6D /* ReactionNew.json in Resources */,
C1616DB128DC4D7F00FF993B /* UserGloballyUnbanned.json in Resources */,
@@ -10280,11 +10399,13 @@
A311B3E527E8B98C00CFCF6D /* MessageWithBrokenAttachments.json in Resources */,
A311B3ED27E8B99800CFCF6D /* ChannelHidden+HistoryCleared.json in Resources */,
A3D9D68827EDE3B900725066 /* chewbacca.jpg in Resources */,
+ ADB8B8F02D8A493900549C95 /* ReminderPayload.json in Resources */,
A311B3CE27E8B98C00CFCF6D /* CurrentUserCustomRole.json in Resources */,
A311B3D227E8B98C00CFCF6D /* UserPayload.json in Resources */,
A311B3EE27E8B99800CFCF6D /* ChannelTruncated.json in Resources */,
A311B41327E8B9B900CFCF6D /* UserBanned.json in Resources */,
A311B41527E8B9B900CFCF6D /* UserStartTyping.json in Resources */,
+ ADB8B9042D8C701500549C95 /* ReminderCreated.json in Resources */,
A311B42A27E8B9D800CFCF6D /* UserUpdateResponse+MissingUser.json in Resources */,
A311B3D827E8B98C00CFCF6D /* Devices.json in Resources */,
A311B3D627E8B98C00CFCF6D /* Member.json in Resources */,
@@ -10305,6 +10426,7 @@
A311B3D927E8B98C00CFCF6D /* ChannelPayloadWithCustom.json in Resources */,
AD545E6D2D565316008FD399 /* DraftMessage.json in Resources */,
A311B42027E8B9C400CFCF6D /* FlagUserPayload+NoExtraData.json in Resources */,
+ ADB8B9062D8C702A00549C95 /* ReminderDeleted.json in Resources */,
ADF3EEF62C00FC7B00DB36D6 /* NotificationMarkUnread+MissingFields.json in Resources */,
A311B3E627E8B99200CFCF6D /* AttachmentPayloadImage.json in Resources */,
A311B3FA27E8B9A800CFCF6D /* MessageDeletedHard.json in Resources */,
@@ -11104,6 +11226,7 @@
A3227E59284A484300EBE6CC /* UIImage+Resized.swift in Sources */,
79B8B64B285CBDC00059FB2D /* DemoChatMessageLayoutOptionsResolver.swift in Sources */,
AD053BA32B335A13003612B6 /* LocationAttachmentPayload+AttachmentViewProvider.swift in Sources */,
+ ADB8B8F52D8ADC9400549C95 /* DemoReminderListVC.swift in Sources */,
AD053B9D2B3358E2003612B6 /* LocationAttachmentPayload.swift in Sources */,
AD053BA12B3359DD003612B6 /* DemoAttachmentViewCatalog.swift in Sources */,
AD053B9F2B335929003612B6 /* LocationAttachmentViewInjector.swift in Sources */,
@@ -11216,6 +11339,7 @@
A3C3BC8327E8AB6200224761 /* URLSessionConfiguration+Equatable.swift in Sources */,
A3C3BC3C27E87F5100224761 /* RetryStrategy_Spy.swift in Sources */,
A344078F27D753530044F150 /* ChatChannelListController_Mock.swift in Sources */,
+ ADB8B8F92D8B8A0C00549C95 /* RemindersRepository_Mock.swift in Sources */,
A311B43427E8BC8400CFCF6D /* MessageSearchController_Delegate.swift in Sources */,
A3C3BC7427E8AA4300224761 /* TestError.swift in Sources */,
A311B43327E8BC8400CFCF6D /* ConnectionController_Delegate.swift in Sources */,
@@ -11261,6 +11385,7 @@
A3C3BC4227E87F5C00224761 /* UserListUpdater_Mock.swift in Sources */,
A344078227D753530044F150 /* NSManagedObject+ContextChange.swift in Sources */,
8459C9EA2BFB39DC00F0D235 /* PollController_Mock.swift in Sources */,
+ ADA83B492D976ED7003B3928 /* MessageReminder_Mock.swift in Sources */,
A3C3BC2627E87F2000224761 /* TestAttachmentEnvelope.swift in Sources */,
A344077E27D753530044F150 /* ChatMessageLinkAttachment_Mock.swift in Sources */,
A3C3BC9427E8AC0600224761 /* RequestEncoder_Spy.swift in Sources */,
@@ -11328,6 +11453,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ ADA83B502D978050003B3928 /* ReminderPayloads.swift in Sources */,
4F97F2672BA83146001C4D66 /* UserList.swift in Sources */,
82BE0ACD2C009A17008DA9DC /* BlockedUserDetails.swift in Sources */,
DA8407032524F7E6005A0F62 /* UserListUpdater.swift in Sources */,
@@ -11382,6 +11508,8 @@
79877A0A2498E4BC00015F8B /* Device.swift in Sources */,
841BAA0D2BCE9F44000C73E4 /* UpdatePollOptionRequestBody.swift in Sources */,
841BA9F82BCE80FF000C73E4 /* PollsPayloads.swift in Sources */,
+ ADA83B3E2D974DCC003B3928 /* MessageReminderListController.swift in Sources */,
+ ADA83B3F2D974DCC003B3928 /* MessageReminderListController+Combine.swift in Sources */,
DABC6AC8254707CB00A8FC78 /* AttachmentDTO.swift in Sources */,
40789D1329F6AC500018C2BB /* AudioPlaybackContext.swift in Sources */,
22692C9725D1841E007C41D0 /* ChatMessageFileAttachment.swift in Sources */,
@@ -11466,6 +11594,7 @@
AD7BE16A2C209888000A5756 /* ThreadEvents.swift in Sources */,
88BDCA8A2642B02D0099AD74 /* ChatMessageAttachment.swift in Sources */,
841BAA4E2BD1CD76000C73E4 /* PollOptionDTO.swift in Sources */,
+ ADB208802D849184003F1059 /* MessageReminderListQuery.swift in Sources */,
88E26D6E2580F34B00F55AB5 /* AttachmentQueueUploader.swift in Sources */,
4F312D0E2C905A2E0073A1BC /* FlagRequestBody.swift in Sources */,
88A00DD02525F08000259AB4 /* ModerationEndpoints.swift in Sources */,
@@ -11481,6 +11610,7 @@
8413D2F22BDDAAEE005ADA4E /* PollVoteListController+Combine.swift in Sources */,
8A0CC9F124C606EF00705CF9 /* ReactionEvents.swift in Sources */,
C143788D27BBEBB700E23965 /* OfflineRequestsRepository.swift in Sources */,
+ ADB8B90B2D8C756600549C95 /* ReminderEvents.swift in Sources */,
79877A0F2498E4BC00015F8B /* ChannelId.swift in Sources */,
AD0CC0312BDC1964005E2C66 /* ReactionListQueryDTO.swift in Sources */,
882C5760252C7CC400E60C44 /* ChannelMemberListQueryDTO.swift in Sources */,
@@ -11544,12 +11674,13 @@
AD9C92712DD4DD070013A7E6 /* SendMessageInterceptor.swift in Sources */,
DA0BB1612513B5F200CAEFBD /* StringInterpolation+Extensions.swift in Sources */,
64C8C86E26934C6100329F82 /* UserInfo.swift in Sources */,
- C1FFD9F927ECC7C7008A6848 /* Filter+ChatChannel.swift in Sources */,
+ C1FFD9F927ECC7C7008A6848 /* Filter+predicate.swift in Sources */,
4F1BEE7C2BE3851200B6685C /* ReactionListState+Observer.swift in Sources */,
AD9632E12C0A43630073B814 /* ThreadUpdaterMiddleware.swift in Sources */,
799C9479247E3DEA001F1104 /* StreamChatModel.xcdatamodeld in Sources */,
888E8C55252B525300195E03 /* MemberController.swift in Sources */,
79877A1C2498E4EE00015F8B /* Endpoint.swift in Sources */,
+ ADB8B90F2D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift in Sources */,
882C5759252C794900E60C44 /* MemberEndpoints.swift in Sources */,
DA640FC12535CFA100D32944 /* ChannelMemberListSortingKey.swift in Sources */,
7900452625374CA20096ECA1 /* User+SwiftUI.swift in Sources */,
@@ -11599,6 +11730,7 @@
C1E8AD5E278EF5F30041B775 /* AsyncOperation.swift in Sources */,
88D85DA7252F3C1D00AE1030 /* MemberListController.swift in Sources */,
79280F4B248523C000CDEB89 /* ConnectionEvents.swift in Sources */,
+ ADB8B8F32D8ADA0700549C95 /* RemindersRepository.swift in Sources */,
79158CF425F133FB00186102 /* ChannelTruncatedEventMiddleware.swift in Sources */,
882C574A252C767E00E60C44 /* ChannelMemberListPayload.swift in Sources */,
DAFAD6A324DD8E1A0043ED06 /* ChannelEditDetailPayload.swift in Sources */,
@@ -11634,6 +11766,7 @@
792A4F482480107A00EAF71D /* Pagination.swift in Sources */,
79AF43B42632AF1C00E75CDA /* ChannelVisibilityEventMiddleware.swift in Sources */,
DAF1BED525066114003CEDC0 /* MessageController+Combine.swift in Sources */,
+ ADB8B8EA2D8890B900549C95 /* MessageReminderDTO.swift in Sources */,
A36C39F52860680A0004EB7E /* URL+EnrichedURL.swift in Sources */,
8A0C3BBC24C0947400CAFD19 /* UserEvents.swift in Sources */,
DA4EE5B2252B67F500CB26D4 /* UserListController+SwiftUI.swift in Sources */,
@@ -11646,6 +11779,7 @@
A30C3F20276B428F00DA5968 /* UnknownUserEvent.swift in Sources */,
40789D2129F6AC500018C2BB /* AppStateObserving.swift in Sources */,
4F427F6C2BA2F53200D92238 /* ConnectedUserState+Observer.swift in Sources */,
+ ADB8B8EE2D8890E000549C95 /* MessageReminder.swift in Sources */,
4F83FA462BA43DC3008BD8CD /* MemberList.swift in Sources */,
7963BD6926B0208900281F8C /* ChatMessageAudioAttachment.swift in Sources */,
697C6F90260CFA37000E9023 /* Deprecations.swift in Sources */,
@@ -11657,6 +11791,7 @@
40789D1929F6AC500018C2BB /* AudioPlayerObserving.swift in Sources */,
84A43CAF26A9A25000302763 /* UnknownChannelEvent.swift in Sources */,
C1B0B38327BFC08900C8207D /* EndpointPath+OfflineRequest.swift in Sources */,
+ ADA83B4E2D977D64003B3928 /* ReminderEndpoints.swift in Sources */,
AD0CC0372BDC4B5A005E2C66 /* ReactionListController+SwiftUI.swift in Sources */,
841BAA042BCE94F8000C73E4 /* QueryPollsRequestBody.swift in Sources */,
C1EFF3F3285E459C0057B91B /* IdentifiableModel.swift in Sources */,
@@ -11776,6 +11911,7 @@
8459C9EE2BFB673E00F0D235 /* PollVoteListController+Combine_Tests.swift in Sources */,
8836FFC325408210009FDF73 /* FlagUserPayload_Tests.swift in Sources */,
C18514FD292E34E10033387E /* ConnectionRepository_Tests.swift in Sources */,
+ ADB8B8F72D8B846D00549C95 /* RemindersRepository_Tests.swift in Sources */,
4FE56B902D5E002A00589F9A /* MarkdownParser_Tests.swift in Sources */,
7964F3AA249A19EA002A09EC /* Filter_Tests.swift in Sources */,
4F5151962BC3DEA1001B7152 /* UserSearch_Tests.swift in Sources */,
@@ -11830,6 +11966,7 @@
791C0B6324EEBDF40013CA2F /* MessageSender_Tests.swift in Sources */,
797EEA4824FFB4C200C81203 /* DataStore_Tests.swift in Sources */,
882C574E252C76A400E60C44 /* ChannelMemberListPayload_Tests.swift in Sources */,
+ ADB8B9122D8C7B2D00549C95 /* ReminderUpdaterMiddleware_Tests.swift in Sources */,
4042966929FA6B4B0089126D /* StreamAudioRecorder_Tests.swift in Sources */,
4F14F1282BBD2D8700B1074E /* ChannelList_Tests.swift in Sources */,
88D85D9D252F16A300AE1030 /* MemberController+SwiftUI_Tests.swift in Sources */,
@@ -11857,6 +11994,7 @@
C1EFF3F828633B5D0057B91B /* IdentifiableModel_Tests.swift in Sources */,
40789D4829F6C1DC0018C2BB /* StreamAppStateObserver_Tests.swift in Sources */,
C143789727BE6D4800E23965 /* OfflineRequestsRepository_Tests.swift in Sources */,
+ ADA83B4B2D977D59003B3928 /* ReminderEndpoints_Tests.swift in Sources */,
84355D8B2AB3440E00FD5838 /* FileEndpoints_Tests.swift in Sources */,
84D5BC5B277B18AF00A65C75 /* PinnedMessagesQuery_Tests.swift in Sources */,
DA4EE5BB252B69FD00CB26D4 /* UserListController+Combine_Tests.swift in Sources */,
@@ -11878,6 +12016,7 @@
8A0D64AE24E5853F0017A3C0 /* DataController_Tests.swift in Sources */,
8486CAF926FA51EE00A9AD96 /* EventDTOConverterMiddleware_Tests.swift in Sources */,
ADEDA1FA2B2BC46C00020460 /* RepeatingTimer_Tests.swift in Sources */,
+ ADA83B472D976D9C003B3928 /* MessageReminderListController_Tests.swift in Sources */,
C14A46562845064E00EF498E /* ThreadSafeWeakCollection_Tests.swift in Sources */,
841BAA152BD01901000C73E4 /* PollPayload_Tests.swift in Sources */,
ADC40C3226E26E9F005B616C /* UserSearchController_Tests.swift in Sources */,
@@ -11897,6 +12036,7 @@
792FCB4724A33CC2000290C7 /* EventDataProcessorMiddleware_Tests.swift in Sources */,
797EEA4A24FFC37600C81203 /* ConnectionStatus_Tests.swift in Sources */,
79877A2B2498E51500015F8B /* UserDTO_Tests.swift in Sources */,
+ ADB208822D8494F0003F1059 /* MessageReminderListQuery_Tests.swift in Sources */,
799F611B2530B62C007F218C /* ChannelListQuery_Tests.swift in Sources */,
792921C524C0479700116BBB /* ChannelListUpdater_Tests.swift in Sources */,
A3F65E3727EB7161003F6256 /* WebSocketClient_Tests.swift in Sources */,
@@ -12019,12 +12159,14 @@
79CD959624F9414700E87377 /* ChannelListController+SwiftUI_Tests.swift in Sources */,
DAE566F02500140300E39431 /* ChannelController+SwiftUI_Tests.swift in Sources */,
AD45334E25D153E500CD9D47 /* ConnectionController+Combine_Tests.swift in Sources */,
+ ADB8B90D2D8C784500549C95 /* ReminderEvents_Tests.swift in Sources */,
DAD5C8372502842C0045117A /* CurrentUserController+Combine_Tests.swift in Sources */,
7952B3B524D45DA300AC53D4 /* ChannelUpdater_Tests.swift in Sources */,
40B345F629C46AE500B96027 /* AudioPlaybackContext_Tests.swift in Sources */,
C186BFAA27AA979B0099CCA6 /* SyncRepository_Tests.swift in Sources */,
DAE566F12500F3C800E39431 /* CurrentUserController+SwiftUI_Tests.swift in Sources */,
F69C4BC424F664A700A3D740 /* EventNotificationCenter_Tests.swift in Sources */,
+ ADA83B532D97805A003B3928 /* ReminderPayloads_Tests.swift in Sources */,
4FD94FC52BCD5EF00084FEDF /* ConnectedUser_Tests.swift in Sources */,
AD0AD6C02A25140A00CB96CB /* MessagesPaginationState_Tests.swift in Sources */,
C12DBE612A67E2D60045D9F0 /* SortingValue_Tests.swift in Sources */,
@@ -12045,6 +12187,7 @@
C111B5B628CF3B1200C79D53 /* BackgroundListDatabaseObserver_Tests.swift in Sources */,
A3C7BAD327E4E05300BBF4FA /* MemberListFilterScope_Tests.swift in Sources */,
8A0D649D24E579F70017A3C0 /* GuestUserTokenPayload_Tests.swift in Sources */,
+ ADA83B452D97511E003B3928 /* MessageReminderListController+Combine_Tests.swift in Sources */,
AD9490622BF66D1E00E69224 /* ThreadsRepository_Mock.swift in Sources */,
8A0C3BE224C1F74200CAFD19 /* MessageEvents_Tests.swift in Sources */,
4F6A77042D2FD0A00019CAF8 /* AppSettings_Tests.swift in Sources */,
@@ -12068,6 +12211,7 @@
AD0F7F1C2B616DD000914C4C /* TextLinkDetector_Tests.swift in Sources */,
BCE486580F913CFFDB3B5ECD /* JSONEncoder_Tests.swift in Sources */,
BCE48639FD7B6B05CD63A6AF /* FilterDecoding_Tests.swift in Sources */,
+ ADB8B8FB2D8B904D00549C95 /* MessageController+Reminders_Tests.swift in Sources */,
C152F5FE27C65C18003B4805 /* MessageRepository_Tests.swift in Sources */,
BCE484BA1EE03FF336034250 /* FilterEncoding_Tests.swift in Sources */,
);
@@ -12300,6 +12444,7 @@
C121E822274544AD00023E4C /* WebSocketEngine.swift in Sources */,
C1B0B38427BFC08900C8207D /* EndpointPath+OfflineRequest.swift in Sources */,
C121E823274544AD00023E4C /* URLSessionWebSocketEngine.swift in Sources */,
+ ADB2087F2D849184003F1059 /* MessageReminderListQuery.swift in Sources */,
79D5CDD527EA1BE300BE7D8B /* MessageTranslationsPayload.swift in Sources */,
C121E825274544AD00023E4C /* BackgroundTaskScheduler.swift in Sources */,
C121E826274544AD00023E4C /* WebSocketClient.swift in Sources */,
@@ -12309,6 +12454,8 @@
C121E829274544AD00023E4C /* WebSocketConnectPayload.swift in Sources */,
4F8E53172B7F58C1008C0F9F /* ChatClient+Factory.swift in Sources */,
C121E82A274544AD00023E4C /* ConnectionStatus.swift in Sources */,
+ ADA83B402D974DCC003B3928 /* MessageReminderListController.swift in Sources */,
+ ADA83B412D974DCC003B3928 /* MessageReminderListController+Combine.swift in Sources */,
C121E82B274544AD00023E4C /* APIPathConvertible.swift in Sources */,
AD8FEE5C2AA8E1E400273F88 /* ChatClientFactory.swift in Sources */,
40789D2E29F6AC500018C2BB /* AudioRecordingState.swift in Sources */,
@@ -12379,6 +12526,7 @@
C121E852274544AE00023E4C /* ModerationEndpoints.swift in Sources */,
C121E853274544AE00023E4C /* WebSocketConnectEndpoint.swift in Sources */,
4F73F39F2B91C7BF00563CD9 /* MessageState+Observer.swift in Sources */,
+ ADB8B8F22D8ADA0700549C95 /* RemindersRepository.swift in Sources */,
C121E854274544AE00023E4C /* MemberEndpoints.swift in Sources */,
C121E855274544AE00023E4C /* AttachmentEndpoints.swift in Sources */,
C121E856274544AE00023E4C /* ChatRemoteNotificationHandler.swift in Sources */,
@@ -12398,6 +12546,7 @@
C121E85F274544AE00023E4C /* UserUpdater.swift in Sources */,
AD0CC02C2BDC01A2005E2C66 /* ReactionListController.swift in Sources */,
C121E860274544AE00023E4C /* ChannelMemberListUpdater.swift in Sources */,
+ ADB8B8ED2D8890E000549C95 /* MessageReminder.swift in Sources */,
4042969029FBCE1D0089126D /* AudioSamplesExtractor_Tests.swift in Sources */,
C121E861274544AE00023E4C /* ChannelMemberUpdater.swift in Sources */,
4F427F6D2BA2F53200D92238 /* ConnectedUserState+Observer.swift in Sources */,
@@ -12535,6 +12684,7 @@
C121E8A7274544B000023E4C /* MemberListController.swift in Sources */,
C121E8A8274544B000023E4C /* MemberListController+SwiftUI.swift in Sources */,
8413D2ED2BDC63FA005ADA4E /* PollVoteListQuery.swift in Sources */,
+ ADB8B8EB2D8890B900549C95 /* MessageReminderDTO.swift in Sources */,
C121E8A9274544B000023E4C /* MemberListController+Combine.swift in Sources */,
C121E8AA274544B000023E4C /* ChatChannelWatcherListController.swift in Sources */,
C143788E27BBEBB900E23965 /* OfflineRequestsRepository.swift in Sources */,
@@ -12568,6 +12718,7 @@
ADE40044291B1A510000C98B /* AttachmentUploader.swift in Sources */,
841BAA022BCE9394000C73E4 /* UpdatePollRequestBody.swift in Sources */,
C121E8B9274544B000023E4C /* MessageController.swift in Sources */,
+ ADA83B512D978050003B3928 /* ReminderPayloads.swift in Sources */,
AD483B972A2658970004B406 /* ChannelMemberUnbanRequestPayload.swift in Sources */,
841BA9FF2BCE8E6D000C73E4 /* CreatePollRequestBody.swift in Sources */,
C121E8BA274544B100023E4C /* MessageController+SwiftUI.swift in Sources */,
@@ -12580,6 +12731,7 @@
C121E8C0274544B100023E4C /* EventsController+Combine.swift in Sources */,
C121E8C1274544B100023E4C /* EventsController+SwiftUI.swift in Sources */,
C121E8C2274544B100023E4C /* ChannelEventsController.swift in Sources */,
+ ADB8B90A2D8C756600549C95 /* ReminderEvents.swift in Sources */,
C121E8C3274544B100023E4C /* ListChange.swift in Sources */,
C15C8839286C7BF300E6A72C /* BackgroundListDatabaseObserver.swift in Sources */,
848849B62CEE01070010E7CA /* AITypingEvents.swift in Sources */,
@@ -12602,9 +12754,11 @@
C121E8CA274544B100023E4C /* QueryOptions.swift in Sources */,
C121E8CB274544B100023E4C /* ChannelQuery.swift in Sources */,
C121E8CC274544B100023E4C /* Filter.swift in Sources */,
+ ADA83B4D2D977D64003B3928 /* ReminderEndpoints.swift in Sources */,
C121E8CD274544B100023E4C /* Pagination.swift in Sources */,
C121E8CE274544B100023E4C /* Sorting.swift in Sources */,
C121E8CF274544B100023E4C /* ChannelListSortingKey.swift in Sources */,
+ ADB8B9102D8C7B2500549C95 /* ReminderUpdaterMiddleware.swift in Sources */,
AD37D7C82BC98A4400800D8C /* ThreadParticipantDTO.swift in Sources */,
C121E8D0274544B100023E4C /* UserListSortingKey.swift in Sources */,
C121E8D1274544B100023E4C /* ChannelMemberListSortingKey.swift in Sources */,
@@ -12666,7 +12820,7 @@
4F427F672BA2F43200D92238 /* ConnectedUser.swift in Sources */,
AD37D7C52BC979B000800D8C /* ThreadDTO.swift in Sources */,
40789D1829F6AC500018C2BB /* AudioPlaybackContextAccessor.swift in Sources */,
- C1FFD9FA27ECC7C7008A6848 /* Filter+ChatChannel.swift in Sources */,
+ C1FFD9FA27ECC7C7008A6848 /* Filter+predicate.swift in Sources */,
4FE6E1AE2BAC7A1B00C80AF1 /* UserListState+Observer.swift in Sources */,
4FE6E1AB2BAC79F400C80AF1 /* MemberListState+Observer.swift in Sources */,
AD8FEE592AA8E1A100273F88 /* ChatClient+Environment.swift in Sources */,
@@ -15413,8 +15567,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ProxymanApp/atlantis";
requirement = {
- kind = exactVersion;
- version = 1.18.0;
+ kind = upToNextMajorVersion;
+ minimumVersion = 1.28.0;
};
};
C1B49B39282283C100F4E89E /* XCRemoteSwiftPackageReference "GDPerformanceView-Swift" */ = {
diff --git a/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift b/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
index 246c4612360..228f021f63c 100644
--- a/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
+++ b/TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
@@ -53,7 +53,8 @@ extension ChatMessage {
readBy: [],
poll: nil,
textUpdatedAt: nil,
- draftReply: nil
+ draftReply: nil,
+ reminder: nil
)
}
}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderCreated.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderCreated.json
new file mode 100644
index 00000000000..0904eb72b21
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderCreated.json
@@ -0,0 +1,104 @@
+{
+ "created_at": "2025-03-20T15:50:09.884009Z",
+ "message_id": "f7af18f2-0a46-431d-8901-19c105de7f0a",
+ "reminder": {
+ "channel": {
+ "auto_translation_language": "",
+ "cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "config": {
+ "automod": "disabled",
+ "automod_behavior": "flag",
+ "commands": [],
+ "connect_events": true,
+ "created_at": "2025-03-14T09:56:35.247111552Z",
+ "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-03-14T09:56:35.247111667Z",
+ "uploads": true,
+ "url_enrichment": true,
+ "user_message_reminders": true
+ },
+ "created_at": "2024-07-10T11:47:43.591964Z",
+ "created_by": {
+ "banned": false,
+ "birthland": "Corellia",
+ "created_at": "2024-07-09T10:25:12.255599Z",
+ "id": "han_solo",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png",
+ "last_active": "2025-03-20T15:43:24.605958109Z",
+ "last_engaged_at": "2025-03-20T00:24:49.453063Z",
+ "name": "Han Solo",
+ "online": true,
+ "role": "user",
+ "updated_at": "2025-02-25T13:47:31.92961Z"
+ },
+ "disabled": false,
+ "frozen": false,
+ "id": "!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "last_message_at": "2025-03-19T16:22:51.617418Z",
+ "member_count": 2,
+ "type": "messaging",
+ "updated_at": "2024-07-10T11:47:43.591964Z"
+ },
+ "channel_cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "created_at": "2025-03-20T15:50:09.878366305Z",
+ "message": {
+ "attachments": [],
+ "cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "created_at": "2025-03-19T16:22:51.617418Z",
+ "deleted_reply_count": 0,
+ "html": "aaa
\n",
+ "id": "f7af18f2-0a46-431d-8901-19c105de7f0a",
+ "latest_reactions": [],
+ "mentioned_users": [],
+ "own_reactions": [],
+ "pin_expires": null,
+ "pinned": false,
+ "pinned_at": null,
+ "pinned_by": null,
+ "reaction_counts": {},
+ "reaction_groups": null,
+ "reaction_scores": {},
+ "reply_count": 0,
+ "restricted_visibility": [],
+ "shadowed": false,
+ "silent": false,
+ "text": "aaa",
+ "type": "regular",
+ "updated_at": "2025-03-19T16:22:51.617418Z",
+ "user": {
+ "banned": false,
+ "birthland": "Polis Massa",
+ "created_at": "2024-07-05T14:04:50.791858Z",
+ "id": "leia_organa",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png",
+ "last_active": "2025-03-19T16:25:45.642205Z",
+ "last_engaged_at": "2025-03-19T16:22:46.182706Z",
+ "name": "Leia Organa",
+ "online": false,
+ "role": "user",
+ "updated_at": "2024-07-05T14:04:50.82618Z"
+ }
+ },
+ "message_id": "f7af18f2-0a46-431d-8901-19c105de7f0a",
+ "remind_at": null,
+ "updated_at": "2025-03-20T15:50:09.878366305Z",
+ "user_id": "han_solo"
+ },
+ "type": "reminder.created",
+ "user_id": "han_solo"
+}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDeleted.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDeleted.json
new file mode 100644
index 00000000000..98c0738f3f9
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDeleted.json
@@ -0,0 +1,14 @@
+{
+ "created_at": "2025-03-20T15:49:25.236274751Z",
+ "message_id": "477172a9-a59b-48dc-94a3-9aec4dc181bb",
+ "reminder": {
+ "channel_cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "created_at": "2025-03-19T17:37:14.737404Z",
+ "message_id": "477172a9-a59b-48dc-94a3-9aec4dc181bb",
+ "remind_at": "2025-03-20T15:50:58.1Z",
+ "updated_at": "2025-03-20T15:48:58.664435Z",
+ "user_id": "han_solo"
+ },
+ "type": "reminder.deleted",
+ "user_id": "han_solo"
+}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDue.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDue.json
new file mode 100644
index 00000000000..01be2033c82
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderDue.json
@@ -0,0 +1,14 @@
+{
+ "created_at": "2025-03-20T15:48:58.670372602Z",
+ "message_id": "477172a9-a59b-48dc-94a3-9aec4dc181bb",
+ "reminder": {
+ "channel_cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "created_at": "2025-03-19T17:37:14.737404Z",
+ "message_id": "477172a9-a59b-48dc-94a3-9aec4dc181bb",
+ "remind_at": "2025-03-20T15:50:58.1Z",
+ "updated_at": "2025-03-20T15:48:58.664435Z",
+ "user_id": "han_solo"
+ },
+ "type": "notification.reminder_due",
+ "user_id": "han_solo"
+}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderUpdated.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderUpdated.json
new file mode 100644
index 00000000000..e72f3d6c3e9
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/Events/Reminder/ReminderUpdated.json
@@ -0,0 +1,14 @@
+{
+ "created_at": "2025-03-20T15:48:58.670372602Z",
+ "message_id": "477172a9-a59b-48dc-94a3-9aec4dc181bb",
+ "reminder": {
+ "channel_cid": "messaging:!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E",
+ "created_at": "2025-03-19T17:37:14.737404Z",
+ "message_id": "477172a9-a59b-48dc-94a3-9aec4dc181bb",
+ "remind_at": "2025-03-20T15:50:58.1Z",
+ "updated_at": "2025-03-20T15:48:58.664435Z",
+ "user_id": "han_solo"
+ },
+ "type": "reminder.updated",
+ "user_id": "han_solo"
+}
diff --git a/TestTools/StreamChatTestTools/Fixtures/JSONs/ReminderPayload.json b/TestTools/StreamChatTestTools/Fixtures/JSONs/ReminderPayload.json
new file mode 100644
index 00000000000..86757bf4a4f
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Fixtures/JSONs/ReminderPayload.json
@@ -0,0 +1,112 @@
+{
+ "channel": {
+ "auto_translation_language": "",
+ "cid": "messaging:26D82FB1-5",
+ "config": {
+ "automod": "disabled",
+ "automod_behavior": "flag",
+ "commands": [],
+ "connect_events": true,
+ "created_at": "2025-03-14T09:56:35.247111552Z",
+ "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-03-14T09:56:35.247111667Z",
+ "uploads": true,
+ "url_enrichment": true,
+ "user_message_reminders": true
+ },
+ "created_at": "2025-02-25T10:29:54.133746Z",
+ "created_by": {
+ "banned": false,
+ "birthland": "Corellia",
+ "created_at": "2024-07-09T10:25:12.255599Z",
+ "id": "han_solo",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png",
+ "last_active": "2025-03-19T00:33:30.712641Z",
+ "last_engaged_at": "2025-03-19T00:04:12.127289Z",
+ "name": "Han Solo",
+ "online": false,
+ "role": "user",
+ "updated_at": "2025-02-25T13:47:31.92961Z"
+ },
+ "disabled": false,
+ "frozen": false,
+ "id": "26D82FB1-5",
+ "last_message_at": "2025-03-07T14:54:03.383299Z",
+ "member_count": 3,
+ "name": "Yo",
+ "type": "messaging",
+ "updated_at": "2025-02-25T10:29:54.133746Z"
+ },
+ "channel_cid": "messaging:26D82FB1-5",
+ "created_at": "2025-03-19T00:38:38.697482729Z",
+ "message": {
+ "attachments": [],
+ "cid": "messaging:26D82FB1-5",
+ "created_at": "2025-03-04T14:33:10.628163Z",
+ "deleted_reply_count": 0,
+ "html": "4
\n",
+ "id": "lando_calrissian-8tnV2qn0umMogef2WjR4k",
+ "latest_reactions": [],
+ "mentioned_users": [],
+ "own_reactions": [],
+ "pin_expires": null,
+ "pinned": false,
+ "pinned_at": null,
+ "pinned_by": null,
+ "reaction_counts": {},
+ "reaction_groups": null,
+ "reaction_scores": {},
+ "reply_count": 0,
+ "restricted_visibility": [],
+ "shadowed": false,
+ "silent": false,
+ "text": "4",
+ "type": "regular",
+ "updated_at": "2025-03-04T14:33:10.628163Z",
+ "user": {
+ "banned": false,
+ "birthland": "Socorro",
+ "created_at": "2025-02-07T16:49:34.490544Z",
+ "id": "lando_calrissian",
+ "image": "./static/media/photo-1546820389-44d77e1f3b31.879865aecaba94eb1e8d.jpeg",
+ "last_active": "2025-03-19T00:11:43.92248973Z",
+ "last_engaged_at": "2025-03-18T00:22:43.872028Z",
+ "name": "lando_calrissian",
+ "online": false,
+ "role": "user",
+ "updated_at": "2025-03-14T17:35:12.761069Z"
+ }
+ },
+ "message_id": "lando_calrissian-8tnV2qn0umMogef2WjR4k",
+ "remind_at": null,
+ "updated_at": "2025-03-19T00:38:38.697482729Z",
+ "user": {
+ "banned": false,
+ "birthland": "Corellia",
+ "created_at": "2024-07-09T10:25:12.255599Z",
+ "id": "han_solo",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/e/e2/TFAHanSolo.png",
+ "last_active": "2025-03-19T00:33:30.712641Z",
+ "last_engaged_at": "2025-03-19T00:04:12.127289Z",
+ "name": "Han Solo",
+ "online": false,
+ "role": "user",
+ "updated_at": "2025-02-25T13:47:31.92961Z"
+ },
+ "user_id": "han_solo"
+}
diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift
index 8851e5383eb..ac865de9882 100644
--- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift
@@ -50,7 +50,8 @@ public extension ChatMessage {
underlyingContext: NSManagedObjectContext? = nil,
textUpdatedAt: Date? = nil,
poll: Poll? = nil,
- draftReply: DraftMessage? = nil
+ draftReply: DraftMessage? = nil,
+ reminder: MessageReminderInfo? = nil
) -> Self {
.init(
id: id,
@@ -91,7 +92,8 @@ public extension ChatMessage {
readBy: readBy,
poll: poll,
textUpdatedAt: textUpdatedAt,
- draftReply: draftReply
+ draftReply: draftReply,
+ reminder: reminder
)
}
}
diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/MessageReminder_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/MessageReminder_Mock.swift
new file mode 100644
index 00000000000..e2ec5bfe7cc
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/MessageReminder_Mock.swift
@@ -0,0 +1,26 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+@testable import StreamChat
+
+public extension MessageReminder {
+ static func mock(
+ id: String = .unique,
+ remindAt: Date? = nil,
+ message: ChatMessage = .mock(),
+ channel: ChatChannel = .mock(cid: .unique),
+ createdAt: Date = .init(),
+ updatedAt: Date = .init()
+ ) -> MessageReminder {
+ .init(
+ id: id,
+ remindAt: remindAt,
+ message: message,
+ channel: channel,
+ createdAt: createdAt,
+ updatedAt: updatedAt
+ )
+ }
+}
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift
index 2cc0cca1bd8..55f553d9ce5 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/ChatClient_Mock.swift
@@ -152,6 +152,7 @@ extension ChatClient {
syncRepositoryBuilder: SyncRepository_Mock.init,
pollsRepositoryBuilder: PollsRepository_Mock.init,
draftMessagesRepositoryBuilder: DraftMessagesRepository_Mock.init,
+ remindersRepositoryBuilder: RemindersRepository_Mock.init,
channelListUpdaterBuilder: ChannelListUpdater_Spy.init,
messageRepositoryBuilder: MessageRepository_Mock.init,
offlineRequestsRepositoryBuilder: OfflineRequestsRepository_Mock.init
@@ -209,6 +210,10 @@ extension ChatClient {
draftMessagesRepository as! DraftMessagesRepository_Mock
}
+ var mockRemindersRepository: RemindersRepository_Mock {
+ remindersRepository as! RemindersRepository_Mock
+ }
+
func simulateProvidedConnectionId(connectionId: ConnectionId?) {
guard let connectionId = connectionId else {
webSocketClient(
@@ -247,6 +252,7 @@ extension ChatClient.Environment {
syncRepositoryBuilder: SyncRepository_Mock.init,
pollsRepositoryBuilder: PollsRepository_Mock.init,
draftMessagesRepositoryBuilder: DraftMessagesRepository_Mock.init,
+ remindersRepositoryBuilder: RemindersRepository_Mock.init,
channelListUpdaterBuilder: ChannelListUpdater_Spy.init,
messageRepositoryBuilder: MessageRepository_Mock.init,
offlineRequestsRepositoryBuilder: OfflineRequestsRepository_Mock.init
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
index 3c1de1017e7..b4bf9c799e0 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
@@ -62,6 +62,14 @@ class DatabaseSession_Mock: DatabaseSession {
return try underlyingSession.saveQuery(query: query)
}
+ func saveReminder(payload: ReminderPayload, cache: PreWarmedCache?) throws -> MessageReminderDTO {
+ return try underlyingSession.saveReminder(payload: payload, cache: cache)
+ }
+
+ func deleteReminder(messageId: MessageId) {
+ underlyingSession.deleteReminder(messageId: messageId)
+ }
+
func saveChannel(
payload: ChannelDetailPayload,
query: ChannelListQuery?,
diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/RemindersRepository_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/RemindersRepository_Mock.swift
new file mode 100644
index 00000000000..b0eedd0a9fe
--- /dev/null
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Repositories/RemindersRepository_Mock.swift
@@ -0,0 +1,123 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Foundation
+@testable import StreamChat
+import XCTest
+
+/// Mock implementation of RemindersRepository
+final class RemindersRepository_Mock: RemindersRepository {
+ var loadReminders_callCount: Int = 0
+ var loadReminders_query: MessageReminderListQuery?
+ var loadReminders_completion: ((Result) -> Void)?
+ var loadReminders_completion_result: Result?
+
+ var createReminder_messageId: MessageId?
+ var createReminder_cid: ChannelId?
+ var createReminder_remindAt: Date?
+ var createReminder_completion: ((Result) -> Void)?
+ var createReminder_completion_result: Result?
+
+ var updateReminder_messageId: MessageId?
+ var updateReminder_cid: ChannelId?
+ var updateReminder_remindAt: Date?
+ var updateReminder_completion: ((Result) -> Void)?
+ var updateReminder_completion_result: Result?
+
+ var deleteReminder_messageId: MessageId?
+ var deleteReminder_cid: ChannelId?
+ var deleteReminder_completion: ((Error?) -> Void)?
+ var deleteReminder_error: Error?
+
+ /// Default initializer
+ override init(database: DatabaseContainer, apiClient: APIClient) {
+ super.init(database: database, apiClient: apiClient)
+ }
+
+ /// Convenience initializer
+ init() {
+ super.init(database: DatabaseContainer_Spy(), apiClient: APIClient_Spy())
+ }
+
+ // Cleans up all recorded values
+ func cleanUp() {
+ loadReminders_query = nil
+ loadReminders_completion = nil
+ loadReminders_completion_result = nil
+
+ createReminder_messageId = nil
+ createReminder_cid = nil
+ createReminder_remindAt = nil
+ createReminder_completion = nil
+ createReminder_completion_result = nil
+
+ updateReminder_messageId = nil
+ updateReminder_cid = nil
+ updateReminder_remindAt = nil
+ updateReminder_completion = nil
+ updateReminder_completion_result = nil
+
+ deleteReminder_messageId = nil
+ deleteReminder_cid = nil
+ deleteReminder_completion = nil
+ deleteReminder_error = nil
+ }
+
+ override func loadReminders(
+ query: MessageReminderListQuery,
+ completion: @escaping ((Result) -> Void)
+ ) {
+ loadReminders_callCount += 1
+ loadReminders_query = query
+ loadReminders_completion = completion
+
+ if let result = loadReminders_completion_result {
+ completion(result)
+ }
+ }
+
+ override func createReminder(
+ messageId: MessageId,
+ cid: ChannelId,
+ remindAt: Date?,
+ completion: @escaping ((Result) -> Void)
+ ) {
+ createReminder_messageId = messageId
+ createReminder_cid = cid
+ createReminder_remindAt = remindAt
+ createReminder_completion = completion
+
+ if let result = createReminder_completion_result {
+ completion(result)
+ }
+ }
+
+ override func updateReminder(
+ messageId: MessageId,
+ cid: ChannelId,
+ remindAt: Date?,
+ completion: @escaping ((Result) -> Void)
+ ) {
+ updateReminder_messageId = messageId
+ updateReminder_cid = cid
+ updateReminder_remindAt = remindAt
+ updateReminder_completion = completion
+
+ if let result = updateReminder_completion_result {
+ completion(result)
+ }
+ }
+
+ override func deleteReminder(
+ messageId: MessageId,
+ cid: ChannelId,
+ completion: @escaping ((Error?) -> Void)
+ ) {
+ deleteReminder_messageId = messageId
+ deleteReminder_cid = cid
+ deleteReminder_completion = completion
+
+ completion(deleteReminder_error)
+ }
+}
diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
index 3ba151a564c..c03a8f3bc0f 100644
--- a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
+++ b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift
@@ -139,6 +139,7 @@ extension XCTestCase {
searchEnabled: true,
mutesEnabled: true,
urlEnrichmentEnabled: true,
+ messageRemindersEnabled: true,
messageRetention: "1000",
maxMessageLength: 100,
commands: [
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
index 852df46419c..5993c05233b 100644
--- a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
+++ b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift
@@ -66,6 +66,11 @@ final class EndpointPathTests: XCTestCase {
XCTAssertFalse(EndpointPath.pollVote(messageId: "test_message", pollId: "test_poll", voteId: "test_vote").shouldBeQueuedOffline)
}
+ func test_reminders_shouldNOTBeQueuedOffline() {
+ XCTAssertFalse(EndpointPath.reminders.shouldBeQueuedOffline)
+ XCTAssertFalse(EndpointPath.reminder("test_message").shouldBeQueuedOffline)
+ }
+
func test_unread_shouldNOTBeQueuedOffline() {
XCTAssertFalse(EndpointPath.unread.shouldBeQueuedOffline)
}
@@ -146,6 +151,9 @@ final class EndpointPathTests: XCTestCase {
assertResultEncodingAndDecoding(.drafts)
assertResultEncodingAndDecoding(.draftMessage(ChannelId(type: .messaging, id: "test_channel")))
+
+ assertResultEncodingAndDecoding(.reminders)
+ assertResultEncodingAndDecoding(.reminder("test_message"))
}
}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ReminderPayloads_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ReminderPayloads_Tests.swift
new file mode 100644
index 00000000000..11372e50e62
--- /dev/null
+++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ReminderPayloads_Tests.swift
@@ -0,0 +1,112 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class ReminderPayload_Tests: XCTestCase {
+ let reminderJSON = XCTestCase.mockData(fromJSONFile: "ReminderPayload")
+
+ func test_reminderPayload_isSerialized() throws {
+ let payload = try JSONDecoder.default.decode(ReminderPayload.self, from: reminderJSON)
+
+ // Test basic properties
+ XCTAssertEqual(payload.channelCid.rawValue, "messaging:26D82FB1-5")
+ XCTAssertEqual(payload.messageId, "lando_calrissian-8tnV2qn0umMogef2WjR4k")
+ XCTAssertNil(payload.remindAt) // Updated to nil as per new JSON
+ XCTAssertEqual(payload.createdAt, "2025-03-19T00:38:38.697482729Z".toDate())
+ XCTAssertEqual(payload.updatedAt, "2025-03-19T00:38:38.697482729Z".toDate())
+
+ // Test embedded message
+ XCTAssertNotNil(payload.message)
+ XCTAssertEqual(payload.message?.id, "lando_calrissian-8tnV2qn0umMogef2WjR4k")
+ XCTAssertEqual(payload.message?.text, "4")
+ XCTAssertEqual(payload.message?.type.rawValue, "regular")
+ XCTAssertEqual(payload.message?.user.id, "lando_calrissian")
+ XCTAssertEqual(payload.message?.createdAt, "2025-03-04T14:33:10.628163Z".toDate())
+ XCTAssertEqual(payload.message?.updatedAt, "2025-03-04T14:33:10.628163Z".toDate())
+
+ // Test channel properties (new in updated JSON)
+ XCTAssertNotNil(payload.channel)
+ XCTAssertEqual(payload.channel?.cid.rawValue, "messaging:26D82FB1-5")
+ XCTAssertEqual(payload.channel?.name, "Yo")
+ }
+}
+
+final class ReminderResponsePayload_Tests: XCTestCase {
+ func test_isSerialized() throws {
+ // Create a JSON representation of a ReminderResponsePayload
+ // with the updated structure including duration
+ let reminderResponseJSON = """
+ {
+ "duration": "30.74ms",
+ "reminder": {
+ "channel_cid": "messaging:26D82FB1-5",
+ "message_id": "lando_calrissian-8tnV2qn0umMogef2WjR4k",
+ "remind_at": null,
+ "created_at": "2025-03-19T00:38:38.697482729Z",
+ "updated_at": "2025-03-19T00:38:38.697482729Z",
+ "user_id": "han_solo"
+ }
+ }
+ """.data(using: .utf8)!
+
+ let payload = try JSONDecoder.default.decode(ReminderResponsePayload.self, from: reminderResponseJSON)
+
+ XCTAssertEqual(payload.reminder.channelCid.rawValue, "messaging:26D82FB1-5")
+ XCTAssertEqual(payload.reminder.messageId, "lando_calrissian-8tnV2qn0umMogef2WjR4k")
+ XCTAssertNil(payload.reminder.remindAt)
+ XCTAssertEqual(payload.reminder.createdAt, "2025-03-19T00:38:38.697482729Z".toDate())
+ XCTAssertEqual(payload.reminder.updatedAt, "2025-03-19T00:38:38.697482729Z".toDate())
+ }
+}
+
+final class RemindersQueryPayload_Tests: XCTestCase {
+ func test_isSerialized() throws {
+ // Create a JSON representation of a RemindersQueryPayload with updated structure
+ let remindersQueryJSON = """
+ {
+ "duration": "30.74ms",
+ "reminders": [
+ {
+ "channel_cid": "messaging:26D82FB1-5",
+ "message_id": "lando_calrissian-8tnV2qn0umMogef2WjR4k",
+ "remind_at": null,
+ "created_at": "2025-03-19T00:38:38.697482729Z",
+ "updated_at": "2025-03-19T00:38:38.697482729Z",
+ "user_id": "han_solo"
+ },
+ {
+ "channel_cid": "messaging:456",
+ "message_id": "message-456",
+ "remind_at": "2023-02-01T12:00:00.000Z",
+ "created_at": "2022-02-03T00:00:00.000Z",
+ "updated_at": "2022-02-03T00:00:00.000Z",
+ "user_id": "luke_skywalker"
+ }
+ ],
+ "next": "next-page-token"
+ }
+ """.data(using: .utf8)!
+
+ let payload = try JSONDecoder.default.decode(RemindersQueryPayload.self, from: remindersQueryJSON)
+
+ // Verify the count of reminders
+ XCTAssertEqual(payload.reminders.count, 2)
+
+ // Verify pagination tokens
+ XCTAssertEqual(payload.next, "next-page-token")
+
+ // Verify first reminder details
+ XCTAssertEqual(payload.reminders[0].channelCid.rawValue, "messaging:26D82FB1-5")
+ XCTAssertEqual(payload.reminders[0].messageId, "lando_calrissian-8tnV2qn0umMogef2WjR4k")
+ XCTAssertNil(payload.reminders[0].remindAt)
+
+ // Verify second reminder details
+ XCTAssertEqual(payload.reminders[1].channelCid.rawValue, "messaging:456")
+ XCTAssertEqual(payload.reminders[1].messageId, "message-456")
+ XCTAssertEqual(payload.reminders[1].remindAt, "2023-02-01T12:00:00.000Z".toDate())
+ }
+}
diff --git a/Tests/StreamChatTests/APIClient/Endpoints/ReminderEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/ReminderEndpoints_Tests.swift
new file mode 100644
index 00000000000..9a8f0dbca76
--- /dev/null
+++ b/Tests/StreamChatTests/APIClient/Endpoints/ReminderEndpoints_Tests.swift
@@ -0,0 +1,89 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class ReminderEndpoints_Tests: XCTestCase {
+ func test_createReminder_buildsCorrectly() {
+ let messageId: MessageId = .unique
+ let remindAt = Date()
+ let request = ReminderRequestBody(remindAt: remindAt)
+
+ let expectedEndpoint = Endpoint(
+ path: .reminder(messageId),
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: request
+ )
+
+ let endpoint: Endpoint = .createReminder(messageId: messageId, request: request)
+
+ // Assert endpoint is built correctly
+ XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
+ XCTAssertEqual("messages/\(messageId)/reminders", endpoint.path.value)
+ }
+
+ func test_updateReminder_buildsCorrectly() {
+ let messageId: MessageId = .unique
+ let remindAt = Date()
+ let request = ReminderRequestBody(remindAt: remindAt)
+
+ let expectedEndpoint = Endpoint(
+ path: .reminder(messageId),
+ method: .patch,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: request
+ )
+
+ let endpoint: Endpoint = .updateReminder(messageId: messageId, request: request)
+
+ // Assert endpoint is built correctly
+ XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
+ XCTAssertEqual("messages/\(messageId)/reminders", endpoint.path.value)
+ }
+
+ func test_deleteReminder_buildsCorrectly() {
+ let messageId: MessageId = .unique
+
+ let expectedEndpoint = Endpoint(
+ path: .reminder(messageId),
+ method: .delete,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: nil
+ )
+
+ let endpoint: Endpoint = .deleteReminder(messageId: messageId)
+
+ // Assert endpoint is built correctly
+ XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
+ XCTAssertEqual("messages/\(messageId)/reminders", endpoint.path.value)
+ }
+
+ func test_queryReminders_buildsCorrectly() {
+ let query = MessageReminderListQuery(
+ filter: .equal(.cid, to: ChannelId.unique),
+ sort: [.init(key: .remindAt, isAscending: true)],
+ pageSize: 25
+ )
+
+ let expectedEndpoint = Endpoint(
+ path: .reminders,
+ method: .post,
+ queryItems: nil,
+ requiresConnectionId: false,
+ body: query
+ )
+
+ let endpoint: Endpoint = .queryReminders(query: query)
+
+ // Assert endpoint is built correctly
+ XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
+ XCTAssertEqual("reminders/query", endpoint.path.value)
+ }
+}
diff --git a/Tests/StreamChatTests/Controllers/MessageController/MessageController+Reminders_Tests.swift b/Tests/StreamChatTests/Controllers/MessageController/MessageController+Reminders_Tests.swift
new file mode 100644
index 00000000000..610f0b79427
--- /dev/null
+++ b/Tests/StreamChatTests/Controllers/MessageController/MessageController+Reminders_Tests.swift
@@ -0,0 +1,207 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class MessageController_Reminders_Tests: XCTestCase {
+ var client: ChatClient_Mock!
+ var controller: ChatMessageController!
+ var remindersRepository: RemindersRepository_Mock!
+
+ override func setUp() {
+ super.setUp()
+
+ client = ChatClient.mock
+ remindersRepository = client.remindersRepository as? RemindersRepository_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()
+ remindersRepository = nil
+ controller = nil
+ client = nil
+
+ super.tearDown()
+ }
+
+ // MARK: - Create Reminder Tests
+
+ func test_createReminder_whenSuccessful() {
+ // Prepare data for mocking
+ let remindAt = Date()
+ let reminderResponse = MessageReminder(
+ id: controller.messageId,
+ remindAt: remindAt,
+ message: .mock(),
+ channel: .mockDMChannel(),
+ createdAt: .init(),
+ updatedAt: .init()
+ )
+
+ // Setup mock response
+ remindersRepository.createReminder_completion_result = .success(reminderResponse)
+
+ // Setup callback verification
+ let expectation = expectation(description: "createReminder completion called")
+ var receivedResult: Result?
+
+ // Call method being tested
+ controller.createReminder(remindAt: remindAt) { result in
+ receivedResult = result
+ expectation.fulfill()
+ }
+
+ // Wait for callback
+ waitForExpectations(timeout: defaultTimeout)
+
+ // Assert remindersRepository is called with correct params
+ XCTAssertEqual(remindersRepository.createReminder_messageId, controller.messageId)
+ XCTAssertEqual(remindersRepository.createReminder_cid, controller.cid)
+ XCTAssertEqual(remindersRepository.createReminder_remindAt, remindAt)
+ XCTAssertEqual(receivedResult?.value?.id, reminderResponse.id)
+ }
+
+ func test_createReminder_whenFailure() {
+ // Setup mock error response
+ let testError = TestError()
+ remindersRepository.createReminder_completion_result = .failure(testError)
+
+ // Setup callback verification
+ let expectation = expectation(description: "createReminder completion called")
+ var receivedError: Error?
+
+ // Call method being tested
+ controller.createReminder(remindAt: nil) { result in
+ if case let .failure(error) = result {
+ receivedError = error
+ }
+ expectation.fulfill()
+ }
+
+ // Wait for callback
+ waitForExpectations(timeout: defaultTimeout)
+
+ // Assert callback is called with correct error
+ XCTAssertEqual(receivedError as? TestError, testError)
+ }
+
+ // MARK: - Update Reminder Tests
+
+ func test_updateReminder_whenSuccessful() {
+ // Prepare data for mocking
+ let remindAt = Date()
+ let reminderResponse = MessageReminder(
+ id: controller.messageId,
+ remindAt: remindAt,
+ message: .mock(),
+ channel: .mockDMChannel(),
+ createdAt: .init(),
+ updatedAt: .init()
+ )
+
+ // Setup mock response
+ remindersRepository.updateReminder_completion_result = .success(reminderResponse)
+
+ // Setup callback verification
+ let expectation = expectation(description: "updateReminder completion called")
+ var receivedResult: Result?
+
+ // Call method being tested
+ controller.updateReminder(remindAt: remindAt) { result in
+ receivedResult = result
+ expectation.fulfill()
+ }
+
+ // Wait for callback
+ waitForExpectations(timeout: defaultTimeout)
+
+ // Assert remindersRepository is called with correct params
+ XCTAssertEqual(remindersRepository.updateReminder_messageId, controller.messageId)
+ XCTAssertEqual(remindersRepository.updateReminder_cid, controller.cid)
+ XCTAssertEqual(remindersRepository.updateReminder_remindAt, remindAt)
+ XCTAssertEqual(receivedResult?.value?.id, reminderResponse.id)
+ }
+
+ func test_updateReminder_whenFailure() {
+ // Setup mock error response
+ let testError = TestError()
+ remindersRepository.updateReminder_completion_result = .failure(testError)
+
+ // Setup callback verification
+ let expectation = expectation(description: "updateReminder completion called")
+ var receivedError: Error?
+
+ // Call method being tested
+ controller.updateReminder(remindAt: nil) { result in
+ if case let .failure(error) = result {
+ receivedError = error
+ }
+ expectation.fulfill()
+ }
+
+ // Wait for callback
+ waitForExpectations(timeout: defaultTimeout)
+
+ // Assert callback is called with correct error
+ XCTAssertEqual(receivedError as? TestError, testError)
+ }
+
+ // MARK: - Delete Reminder Tests
+
+ func test_deleteReminder_whenSuccessful() {
+ // Setup mock response
+ remindersRepository.deleteReminder_error = nil
+
+ // Setup callback verification
+ let expectation = expectation(description: "deleteReminder completion called")
+ var receivedError: Error?
+
+ // Call method being tested
+ controller.deleteReminder { error in
+ receivedError = error
+ expectation.fulfill()
+ }
+
+ // Wait for callback
+ waitForExpectations(timeout: defaultTimeout)
+
+ // Assert remindersRepository is called with correct params
+ XCTAssertEqual(remindersRepository.deleteReminder_messageId, controller.messageId)
+ XCTAssertEqual(remindersRepository.deleteReminder_cid, controller.cid)
+ XCTAssertNil(receivedError)
+ }
+
+ func test_deleteReminder_whenFailure() {
+ // Setup mock error response
+ let testError = TestError()
+ remindersRepository.deleteReminder_error = testError
+
+ // Setup callback verification
+ let expectation = expectation(description: "deleteReminder completion called")
+ var receivedError: Error?
+
+ // Call method being tested
+ controller.deleteReminder { error in
+ receivedError = error
+ expectation.fulfill()
+ }
+
+ // Wait for callback
+ waitForExpectations(timeout: defaultTimeout)
+
+ // Assert callback is called with correct error
+ XCTAssertEqual(receivedError as? TestError, testError)
+ }
+}
diff --git a/Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController+Combine_Tests.swift b/Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController+Combine_Tests.swift
new file mode 100644
index 00000000000..456f7eff78f
--- /dev/null
+++ b/Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController+Combine_Tests.swift
@@ -0,0 +1,82 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import Combine
+import CoreData
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class MessageReminderListController_Combine_Tests: iOS13TestCase {
+ var reminderListController: MessageReminderListController!
+ var cancellables: Set!
+ var client: ChatClient_Mock!
+
+ override func setUp() {
+ super.setUp()
+ client = ChatClient_Mock.mock
+ reminderListController = MessageReminderListController(
+ query: .init(),
+ client: client
+ )
+ cancellables = []
+ }
+
+ override func tearDown() {
+ // Release existing subscriptions and make sure the controller gets released, too
+ cancellables = nil
+ AssertAsync.canBeReleased(&reminderListController)
+ reminderListController = nil
+ super.tearDown()
+ }
+
+ func test_statePublisher() {
+ // Setup Recording publishers
+ var recording = Record.Recording()
+
+ // Setup the chain
+ reminderListController
+ .statePublisher
+ .sink(receiveValue: { recording.receive($0) })
+ .store(in: &cancellables)
+
+ // Keep only the weak reference to the controller. The existing publisher should keep it alive.
+ weak var controller: MessageReminderListController? = reminderListController
+ reminderListController = nil
+
+ controller?.delegateCallback { $0.controller(controller!, didChangeState: .remoteDataFetched) }
+
+ AssertAsync.willBeEqual(recording.output, [.localDataFetched, .remoteDataFetched])
+ }
+
+ func test_remindersChangesPublisher() {
+ // Setup Recording publishers
+ var recording = Record<[ListChange], Never>.Recording()
+
+ // Setup the chain
+ reminderListController
+ .remindersChangesPublisher
+ .sink(receiveValue: { recording.receive($0) })
+ .store(in: &cancellables)
+
+ // Keep only the weak reference to the controller. The existing publisher should keep it alive.
+ weak var controller: MessageReminderListController? = reminderListController
+ reminderListController = nil
+
+ let reminder = MessageReminder(
+ id: .unique,
+ remindAt: nil,
+ message: .unique,
+ channel: .mock(cid: .unique),
+ createdAt: .unique,
+ updatedAt: .unique
+ )
+ let changes: [ListChange] = .init([.insert(reminder, index: .init())])
+ controller?.delegateCallback {
+ $0.controller(controller!, didChangeReminders: changes)
+ }
+
+ XCTAssertEqual(recording.output, .init(arrayLiteral: [.insert(reminder, index: .init())]))
+ }
+}
diff --git a/Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController_Tests.swift b/Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController_Tests.swift
new file mode 100644
index 00000000000..1bad4cbf070
--- /dev/null
+++ b/Tests/StreamChatTests/Controllers/MessageReminderListController/MessageReminderListController_Tests.swift
@@ -0,0 +1,278 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+import CoreData
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class MessageReminderListController_Tests: XCTestCase {
+ var client: ChatClient_Mock!
+ var controller: MessageReminderListController!
+ var repositoryMock: RemindersRepository_Mock!
+
+ override func setUp() {
+ super.setUp()
+ client = ChatClient.mock
+ repositoryMock = client.remindersRepository as? RemindersRepository_Mock
+ controller = makeController()
+ }
+
+ override func tearDown() {
+ client.cleanUp()
+ repositoryMock = nil
+ controller = nil
+ super.tearDown()
+ }
+
+ func test_synchronize_whenSuccess() {
+ let exp = expectation(description: "synchronize completion")
+ controller.synchronize { error in
+ XCTAssertNil(error)
+ exp.fulfill()
+ }
+ XCTAssertEqual(repositoryMock.loadReminders_callCount, 1)
+
+ repositoryMock.loadReminders_completion?(.success(.init(reminders: [
+ .mock(),
+ .mock()
+ ], next: nil)))
+
+ wait(for: [exp], timeout: defaultTimeout)
+ XCTAssertEqual(controller.state, .remoteDataFetched)
+ XCTAssertTrue(controller.hasLoadedAllReminders)
+ }
+
+ func test_synchronize_whenSuccess_whenMoreReminders() {
+ let exp = expectation(description: "synchronize completion")
+ var query = MessageReminderListQuery(pageSize: 2)
+ controller = makeController(query: query)
+ controller.synchronize { error in
+ XCTAssertNil(error)
+ exp.fulfill()
+ }
+
+ repositoryMock.loadReminders_completion?(.success(.init(reminders: [
+ .mock(),
+ .mock(),
+ .mock(),
+ .mock()
+ ], next: .unique)))
+
+ wait(for: [exp], timeout: defaultTimeout)
+ XCTAssertFalse(controller.hasLoadedAllReminders)
+ }
+
+ func test_synchronize_whenFailure() {
+ let exp = expectation(description: "synchronize completion")
+ controller.synchronize { error in
+ XCTAssertNotNil(error)
+ exp.fulfill()
+ }
+ repositoryMock.loadReminders_completion?(.failure(ClientError()))
+
+ wait(for: [exp], timeout: defaultTimeout)
+ XCTAssertFalse(controller.hasLoadedAllReminders)
+ switch controller.state {
+ case .remoteDataFetchFailed:
+ break
+ default:
+ XCTFail()
+ }
+ }
+
+ func test_loadMoreReminders_whenSuccess() {
+ let exp = expectation(description: "loadMoreReminders completion")
+ controller.loadMoreReminders() { result in
+ let reminders = try? result.get()
+ XCTAssertNotNil(reminders)
+ exp.fulfill()
+ }
+ XCTAssertEqual(repositoryMock.loadReminders_query?.pagination.pageSize, controller.query.pagination.pageSize)
+
+ repositoryMock.loadReminders_completion?(.success(.init(reminders: [
+ .mock(),
+ .mock()
+ ])))
+
+ wait(for: [exp], timeout: defaultTimeout)
+ XCTAssertTrue(controller.hasLoadedAllReminders)
+ }
+
+ func test_loadMoreReminders_whenSuccess_whenMoreReminders() {
+ let exp = expectation(description: "loadMoreReminders completion")
+ controller.loadMoreReminders(limit: 2) { result in
+ let reminders = try? result.get()
+ XCTAssertNotNil(reminders)
+ exp.fulfill()
+ }
+ XCTAssertEqual(repositoryMock.loadReminders_query?.pagination.pageSize, 2)
+
+ repositoryMock.loadReminders_completion?(.success(.init(reminders: [
+ .mock(),
+ .mock(),
+ .mock()
+ ], next: .unique)))
+
+ wait(for: [exp], timeout: defaultTimeout)
+ XCTAssertFalse(controller.hasLoadedAllReminders)
+ }
+
+ func test_loadMoreReminders_whenFailure() {
+ let exp = expectation(description: "loadMoreReminders completion")
+ controller.loadMoreReminders() { error in
+ XCTAssertNotNil(error)
+ exp.fulfill()
+ }
+ repositoryMock.loadReminders_completion?(.failure(ClientError()))
+
+ wait(for: [exp], timeout: defaultTimeout)
+ }
+
+ func test_loadMoreReminders_shouldUseNextCursorWhenMorePagesAvailable() {
+ let exp = expectation(description: "synchronize completion")
+ controller.synchronize { error in
+ XCTAssertNil(error)
+ exp.fulfill()
+ }
+ let nextCursor1 = "cursor1"
+ repositoryMock.loadReminders_completion?(.success(
+ .init(reminders: [.mock(), .mock()], next: nextCursor1))
+ )
+ wait(for: [exp], timeout: defaultTimeout)
+
+ let expMoreReminders = expectation(description: "loadMoreReminders1 completion")
+ controller.loadMoreReminders() { result in
+ let reminders = try? result.get()
+ XCTAssertNotNil(reminders)
+ expMoreReminders.fulfill()
+ }
+ XCTAssertEqual(repositoryMock.loadReminders_query?.pagination.cursor, nextCursor1)
+
+ let nextCursor2 = "cursor2"
+ repositoryMock.loadReminders_completion?(.success(.init(
+ reminders: [.mock(), .mock()], next: nextCursor2
+ ))
+ )
+ wait(for: [expMoreReminders], timeout: defaultTimeout)
+
+ controller.loadMoreReminders()
+ XCTAssertEqual(repositoryMock.loadReminders_query?.pagination.cursor, nextCursor2)
+ }
+
+ func test_observer_triggerDidChangeReminders_remindersHaveCorrectOrder() throws {
+ class DelegateMock: MessageReminderListControllerDelegate {
+ var reminders: [MessageReminder] = []
+ let expectation = XCTestExpectation(description: "Did Change Reminders")
+ let expectedRemindersCount: Int
+
+ init(expectedRemindersCount: Int) {
+ self.expectedRemindersCount = expectedRemindersCount
+ }
+
+ func controller(
+ _ controller: MessageReminderListController,
+ didChangeReminders changes: [ListChange]
+ ) {
+ reminders = Array(controller.reminders)
+ guard expectedRemindersCount == reminders.count else { return }
+ expectation.fulfill()
+ }
+ }
+
+ let delegate = DelegateMock(expectedRemindersCount: 3)
+ controller.synchronize()
+ controller.delegate = delegate
+
+ try client.databaseContainer.writeSynchronously { session in
+ let date = Date.unique
+ let cid = ChannelId.unique
+ let messageId1 = MessageId.unique
+ let messageId2 = MessageId.unique
+ let messageId3 = 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: messageId1), for: cid, syncOwnReactions: false, cache: nil)
+ try session.saveMessage(payload: .dummy(messageId: messageId2), for: cid, syncOwnReactions: false, cache: nil)
+ try session.saveMessage(payload: .dummy(messageId: messageId3), for: cid, syncOwnReactions: false, cache: nil)
+
+ let reminders = [
+ ReminderPayload(
+ channelCid: cid,
+ messageId: messageId1,
+ remindAt: date.addingTimeInterval(3),
+ createdAt: date,
+ updatedAt: date
+ ),
+ ReminderPayload(
+ channelCid: cid,
+ messageId: messageId2,
+ remindAt: date.addingTimeInterval(2),
+ createdAt: date,
+ updatedAt: date
+ ),
+ ReminderPayload(
+ channelCid: cid,
+ messageId: messageId3,
+ remindAt: date.addingTimeInterval(1),
+ createdAt: date,
+ updatedAt: date
+ )
+ ]
+
+ try reminders.forEach {
+ try session.saveReminder(payload: $0, cache: nil)
+ }
+ }
+ wait(for: [delegate.expectation], timeout: defaultTimeout)
+ XCTAssertEqual(controller.reminders.count, 3)
+ XCTAssertEqual(delegate.reminders.count, 3)
+ }
+}
+
+// MARK: - Helpers
+
+extension MessageReminderListController_Tests {
+ func makeController(
+ query: MessageReminderListQuery = .init(),
+ repository: RemindersRepository? = nil,
+ observer: BackgroundListDatabaseObserver? = nil
+ ) -> MessageReminderListController {
+ MessageReminderListController(
+ query: query,
+ client: client,
+ environment: .init(
+ createMessageReminderListDatabaseObserver: { database, fetchRequest, itemCreator in
+ observer ?? BackgroundListDatabaseObserver(
+ database: database,
+ fetchRequest: fetchRequest,
+ itemCreator: itemCreator,
+ itemReuseKeyPaths: nil
+ )
+ }
+ )
+ )
+ }
+}
+
+private extension MessageReminder {
+ static func mock(
+ id: String = .unique,
+ remindAt: Date? = nil,
+ message: ChatMessage = .mock(),
+ channel: ChatChannel = .mockDMChannel(),
+ createdAt: Date = .unique,
+ updatedAt: Date = .unique
+ ) -> MessageReminder {
+ .init(
+ id: id,
+ remindAt: remindAt,
+ message: message,
+ channel: channel,
+ createdAt: createdAt,
+ updatedAt: updatedAt
+ )
+ }
+}
diff --git a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
index 9ffb8009d38..3fbadf897fa 100644
--- a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
+++ b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
@@ -396,6 +396,7 @@ final class ChannelDTO_Tests: XCTestCase {
Assert.willBeEqual(payload.channel.config.commands, loadedChannel.config.commands)
Assert.willBeEqual(payload.channel.config.createdAt, loadedChannel.config.createdAt)
Assert.willBeEqual(payload.channel.config.updatedAt, loadedChannel.config.updatedAt)
+ Assert.willBeEqual(payload.channel.config.messageRemindersEnabled, loadedChannel.config.messageRemindersEnabled)
// Own Capabilities
Assert.willBeEqual(payload.channel.ownCapabilities, ["join-channel", "delete-channel"])
diff --git a/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift b/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift
index a714f89d6fe..66d33823109 100644
--- a/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift
+++ b/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift
@@ -435,6 +435,16 @@ final class DatabaseContainer_Tests: XCTestCase {
query: .init(messageId: message.id, filter: .equal(.authorId, to: currentUserId)),
cache: nil
)
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: message.id,
+ remindAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
}
try session.saveMessage(
payload: .dummy(channel: .dummy(cid: cid)),
diff --git a/Tests/StreamChatTests/Query/MessageReminderListQuery_Tests.swift b/Tests/StreamChatTests/Query/MessageReminderListQuery_Tests.swift
new file mode 100644
index 00000000000..1985c25d30e
--- /dev/null
+++ b/Tests/StreamChatTests/Query/MessageReminderListQuery_Tests.swift
@@ -0,0 +1,112 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class MessageReminderListQuery_Tests: XCTestCase {
+ func test_defaultInitialization() {
+ let query = MessageReminderListQuery()
+
+ XCTAssertNil(query.filter)
+ XCTAssertEqual(query.pagination.pageSize, 25)
+ XCTAssertEqual(query.pagination.offset, 0)
+ XCTAssertEqual(query.sort.count, 1)
+ XCTAssertEqual(query.sort[0].key, .remindAt)
+ XCTAssertTrue(query.sort[0].isAscending)
+ }
+
+ func test_customInitialization() {
+ let filter = Filter.equal(.cid, to: ChannelId.unique)
+ let sort = [Sorting(key: .createdAt, isAscending: false)]
+
+ let query = MessageReminderListQuery(
+ filter: filter,
+ sort: sort,
+ pageSize: 10
+ )
+
+ XCTAssertEqual(query.filter?.filterHash, filter.filterHash)
+ XCTAssertEqual(query.pagination.pageSize, 10)
+ XCTAssertEqual(query.sort.count, 1)
+ XCTAssertEqual(query.sort[0].key, .createdAt)
+ XCTAssertFalse(query.sort[0].isAscending)
+ }
+
+ func test_encode_withAllFields() throws {
+ let cid: ChannelId = .init(type: .messaging, id: "123")
+ let filter = Filter.equal(.cid, to: cid)
+ let sort = [Sorting(key: .createdAt, isAscending: false)]
+
+ let query = MessageReminderListQuery(
+ filter: filter,
+ sort: sort,
+ pageSize: 10
+ )
+
+ let expectedData: [String: Any] = [
+ "filter": ["channel_cid": ["$eq": cid.rawValue]],
+ "sort": [["field": "created_at", "direction": -1]],
+ "limit": 10
+ ]
+
+ let expectedJSON = try JSONSerialization.data(withJSONObject: expectedData, options: [])
+ let encodedJSON = try JSONEncoder.default.encode(query)
+ AssertJSONEqual(expectedJSON, encodedJSON)
+ }
+
+ func test_encode_withoutFilter() throws {
+ let sort = [Sorting(key: .createdAt, isAscending: false)]
+
+ let query = MessageReminderListQuery(
+ filter: nil,
+ sort: sort,
+ pageSize: 10
+ )
+
+ let expectedData: [String: Any] = [
+ "sort": [["field": "created_at", "direction": -1]],
+ "limit": 10
+ ]
+
+ let expectedJSON = try JSONSerialization.data(withJSONObject: expectedData, options: [])
+ let encodedJSON = try JSONEncoder.default.encode(query)
+ AssertJSONEqual(expectedJSON, encodedJSON)
+ }
+
+ func test_encode_withoutSort() throws {
+ let cid: ChannelId = .init(type: .messaging, id: "123")
+ let filter = Filter.equal(.cid, to: cid)
+
+ let query = MessageReminderListQuery(
+ filter: filter,
+ sort: [],
+ pageSize: 10
+ )
+
+ let expectedData: [String: Any] = [
+ "filter": ["channel_cid": ["$eq": cid.rawValue]],
+ "limit": 10
+ ]
+
+ let expectedJSON = try JSONSerialization.data(withJSONObject: expectedData, options: [])
+ let encodedJSON = try JSONEncoder.default.encode(query)
+ AssertJSONEqual(expectedJSON, encodedJSON)
+ }
+
+ func test_filterKeys() {
+ // Test the filter keys for proper values
+ XCTAssertEqual(FilterKey.cid.rawValue, "channel_cid")
+ XCTAssertEqual(FilterKey.remindAt.rawValue, "remind_at")
+ XCTAssertEqual(FilterKey.createdAt.rawValue, "created_at")
+ }
+
+ func test_sortingKeys() {
+ // Test the sorting keys for proper values
+ XCTAssertEqual(MessageReminderListSortingKey.remindAt.rawValue, "remind_at")
+ XCTAssertEqual(MessageReminderListSortingKey.createdAt.rawValue, "created_at")
+ XCTAssertEqual(MessageReminderListSortingKey.updatedAt.rawValue, "updated_at")
+ }
+}
diff --git a/Tests/StreamChatTests/Repositories/RemindersRepository_Tests.swift b/Tests/StreamChatTests/Repositories/RemindersRepository_Tests.swift
new file mode 100644
index 00000000000..545214eb04e
--- /dev/null
+++ b/Tests/StreamChatTests/Repositories/RemindersRepository_Tests.swift
@@ -0,0 +1,562 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class RemindersRepository_Tests: XCTestCase {
+ var database: DatabaseContainer_Spy!
+ var apiClient: APIClient_Spy!
+ var repository: RemindersRepository!
+
+ override func setUp() {
+ super.setUp()
+
+ let client = ChatClient.mock
+ database = client.mockDatabaseContainer
+ apiClient = client.mockAPIClient
+ repository = RemindersRepository(database: database, apiClient: apiClient)
+ }
+
+ override func tearDown() {
+ super.tearDown()
+
+ apiClient.cleanUp()
+ apiClient = nil
+ database = nil
+ repository = nil
+ }
+
+ // MARK: - Load Reminders Tests
+
+ func test_loadReminders_makesCorrectAPICall() {
+ // Prepare data for the test
+ let query = MessageReminderListQuery(
+ filter: .equal(.remindAt, to: Date()),
+ sort: [.init(key: .remindAt, isAscending: true)]
+ )
+
+ // Simulate `loadReminders` call
+ let exp = expectation(description: "completion is called")
+ repository.loadReminders(query: query) { _ in
+ exp.fulfill()
+ }
+
+ // Mock response
+ let response = RemindersQueryPayload(
+ reminders: [],
+ next: nil
+ )
+
+ apiClient.test_simulateResponse(.success(response))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert endpoint is correct
+ let expectedEndpoint: Endpoint = .queryReminders(query: query)
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(expectedEndpoint))
+ }
+
+ func test_loadReminders_savesRemindersToDatabase() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let remindAt = Date().addingTimeInterval(3600) // 1 hour from now
+ let createdAt = Date().addingTimeInterval(-3600) // 1 hour ago
+ let updatedAt = Date()
+
+ let query = MessageReminderListQuery(
+ filter: .equal(.remindAt, to: Date()),
+ sort: [.init(key: .remindAt, isAscending: true)]
+ )
+
+ // Create a reminder payload
+ let reminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: remindAt,
+ createdAt: createdAt,
+ updatedAt: updatedAt
+ )
+
+ let response = RemindersQueryPayload(
+ reminders: [reminderPayload],
+ next: nil
+ )
+
+ // Create a message to add reminder to
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ // Simulate `loadReminders` call
+ var result: Result?
+ let exp = expectation(description: "completion is called")
+ repository.loadReminders(query: query) { receivedResult in
+ result = receivedResult
+ exp.fulfill()
+ }
+
+ apiClient.test_simulateResponse(.success(response))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert response is parsed correctly
+ guard case .success(let reminderResponse) = result else {
+ XCTFail("Expected successful result")
+ return
+ }
+
+ XCTAssertEqual(reminderResponse.reminders.count, 1)
+ XCTAssertEqual(reminderResponse.reminders.first?.id, messageId)
+ XCTAssertNearlySameDate(reminderResponse.reminders.first?.remindAt, remindAt)
+
+ // Assert reminder is saved to database
+ var savedReminder: MessageReminder?
+ try database.writeSynchronously { session in
+ savedReminder = try session.message(id: messageId)?.reminder?.asModel()
+ }
+
+ XCTAssertNotNil(savedReminder)
+ XCTAssertNearlySameDate(savedReminder?.remindAt, remindAt)
+ }
+
+ func test_loadReminders_propagatesAPIError() {
+ // Prepare data for the test
+ let query = MessageReminderListQuery(
+ filter: .equal(.remindAt, to: Date()),
+ sort: [.init(key: .remindAt, isAscending: true)]
+ )
+
+ // Simulate `loadReminders` call
+ var result: Result?
+ let exp = expectation(description: "completion is called")
+ repository.loadReminders(query: query) { receivedResult in
+ result = receivedResult
+ exp.fulfill()
+ }
+
+ let testError = TestError()
+ apiClient.test_simulateResponse(Result.failure(testError))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert error is propagated correctly
+ guard case .failure = result else {
+ XCTFail("Expected failure result")
+ return
+ }
+ }
+
+ // MARK: - Create Reminder Tests
+
+ func test_createReminder_makesCorrectAPICall() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let remindAt = Date()
+
+ // Create a message to add reminder to
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ // Simulate `createReminder` call
+ let exp = expectation(description: "completion is called")
+ repository.createReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: remindAt
+ ) { _ in
+ exp.fulfill()
+ }
+
+ apiClient.test_mockResponseResult(.success(ReminderResponsePayload(
+ reminder: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: remindAt,
+ createdAt: .unique,
+ updatedAt: .unique
+ )
+ )))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert endpoint is correct
+ let expectedEndpoint: Endpoint = .createReminder(
+ messageId: messageId,
+ request: ReminderRequestBody(remindAt: remindAt)
+ )
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(expectedEndpoint))
+ }
+
+ func test_createReminder_updatesLocalMessageOptimistically() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let remindAt = Date()
+
+ // Create a message to add reminder to
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ // Simulate `createReminder` call
+ repository.createReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: remindAt
+ ) { _ in }
+
+ // Assert reminder was created locally
+ var expectedRemindAt: Date?
+ try database.writeSynchronously { session in
+ let message = session.message(id: messageId)
+ expectedRemindAt = message?.reminder?.remindAt?.bridgeDate
+ }
+ XCTAssertNearlySameDate(expectedRemindAt, remindAt)
+ }
+
+ func test_createReminder_rollsBackOnFailure() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let remindAt = Date()
+
+ // Create a message to add reminder to
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ // Simulate `createReminder` call
+ let exp = expectation(description: "completion is called")
+ repository.createReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: remindAt
+ ) { _ in
+ exp.fulfill()
+ }
+
+ // Assert reminder was created locally
+ var expectedRemindAt: Date?
+ try database.writeSynchronously { session in
+ let message = session.message(id: messageId)
+ expectedRemindAt = message?.reminder?.remindAt?.bridgeDate
+ }
+ XCTAssertNearlySameDate(expectedRemindAt, remindAt)
+
+ apiClient.test_simulateResponse(Result.failure(TestError()))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert reminder was rolled back
+ var actualRemindAt: Date?
+ try database.writeSynchronously { session in
+ let message = session.message(id: messageId)
+ actualRemindAt = message?.reminder?.remindAt?.bridgeDate
+ }
+ XCTAssertNil(actualRemindAt)
+ }
+
+ // MARK: - Update Reminder Tests
+
+ func test_updateReminder_makesCorrectAPICall() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let newRemindAt = Date().addingTimeInterval(3600) // 1 hour from now
+
+ // Create a message with an existing reminder
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+ let messageDTO = try XCTUnwrap(database.viewContext.message(id: messageId))
+
+ try database.writeSynchronously { session in
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
+ }
+
+ // Simulate `updateReminder` call
+ let exp = expectation(description: "completion is called")
+ repository.updateReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: newRemindAt
+ ) { _ in
+ exp.fulfill()
+ }
+
+ apiClient.test_mockResponseResult(.success(ReminderResponsePayload(
+ reminder: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: newRemindAt,
+ createdAt: .unique,
+ updatedAt: .unique
+ )
+ )))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert endpoint is correct
+ let expectedEndpoint: Endpoint = .updateReminder(
+ messageId: messageId,
+ request: ReminderRequestBody(remindAt: newRemindAt)
+ )
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(expectedEndpoint))
+ }
+
+ func test_updateReminder_updatesLocalMessageOptimistically() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let newRemindAt = Date().addingTimeInterval(3600) // 1 hour from now
+
+ // Create a message with an existing reminder
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ try database.writeSynchronously { session in
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
+ }
+
+ // Simulate `updateReminder` call
+ repository.updateReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: newRemindAt
+ ) { _ in }
+
+ // Assert reminder was updated locally (optimistically)
+ var updatedRemindAt: Date?
+ try database.writeSynchronously { session in
+ let message = session.message(id: messageId)
+ updatedRemindAt = message?.reminder?.remindAt?.bridgeDate
+ }
+ XCTAssertNearlySameDate(updatedRemindAt, newRemindAt)
+ }
+
+ func test_updateReminder_rollsBackOnFailure() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+ let originalRemindAt = Date().addingTimeInterval(-3600) // 1 hour ago
+ let newRemindAt = Date().addingTimeInterval(3600) // 1 hour from now
+
+ // Create a message with an existing reminder
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ try database.writeSynchronously { session in
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: originalRemindAt,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
+ }
+
+ // Simulate `updateReminder` call
+ let exp = expectation(description: "completion is called")
+ repository.updateReminder(
+ messageId: messageId,
+ cid: cid,
+ remindAt: newRemindAt
+ ) { _ in
+ exp.fulfill()
+ }
+
+ // Assert reminder was updated locally (optimistically)
+ var updatedRemindAt: Date?
+ try database.writeSynchronously { session in
+ let message = session.message(id: messageId)
+ updatedRemindAt = message?.reminder?.remindAt?.bridgeDate
+ }
+ XCTAssertNearlySameDate(updatedRemindAt, newRemindAt)
+
+ // Simulate API failure
+ apiClient.test_simulateResponse(Result.failure(TestError()))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert reminder was rolled back to original state
+ var rolledBackRemindAt: Date?
+ try database.writeSynchronously { session in
+ let message = session.message(id: messageId)
+ rolledBackRemindAt = message?.reminder?.remindAt?.bridgeDate
+ }
+ XCTAssertNearlySameDate(rolledBackRemindAt, originalRemindAt)
+ }
+
+ // MARK: - Delete Reminder Tests
+
+ func test_deleteReminder_makesCorrectAPICall() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+
+ // Create a message with reminder
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ try database.writeSynchronously { session in
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
+ }
+
+ // Simulate `deleteReminder` call
+ let exp = expectation(description: "completion is called")
+ repository.deleteReminder(
+ messageId: messageId,
+ cid: cid
+ ) { _ in
+ exp.fulfill()
+ }
+
+ apiClient.test_mockResponseResult(.success(EmptyResponse()))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert endpoint is correct
+ let expectedEndpoint: Endpoint = .deleteReminder(messageId: messageId)
+ XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(expectedEndpoint))
+ }
+
+ func test_deleteReminder_deletesLocalReminderOptimistically() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+
+ // Create a message with reminder
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ try database.writeSynchronously { session in
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
+ }
+
+ // Verify reminder exists before deletion
+ var hasReminderBefore = false
+ try database.writeSynchronously { session in
+ hasReminderBefore = session.message(id: messageId)?.reminder != nil
+ }
+ XCTAssertTrue(hasReminderBefore, "Message should have a reminder before deletion")
+
+ // Simulate `deleteReminder` call
+ repository.deleteReminder(
+ messageId: messageId,
+ cid: cid
+ ) { _ in }
+
+ // Assert reminder was deleted locally (optimistically)
+ var hasReminderAfter = true
+ try database.writeSynchronously { session in
+ hasReminderAfter = session.message(id: messageId)?.reminder != nil
+ }
+ XCTAssertFalse(hasReminderAfter, "Reminder should be optimistically deleted locally")
+ }
+
+ func test_deleteReminder_rollsBackOnFailure() throws {
+ // Prepare data for the test
+ let messageId: MessageId = .unique
+ let cid: ChannelId = .unique
+
+ // Create a message with reminder
+ try database.createMessage(id: messageId, cid: cid, text: "Test message")
+
+ try database.writeSynchronously { session in
+ try session.saveReminder(
+ payload: .init(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: .unique,
+ createdAt: .unique,
+ updatedAt: .unique
+ ),
+ cache: nil
+ )
+ }
+
+ // Store original reminder values for later comparison
+ var originalRemindAt: Date?
+ var originalCreatedAt: Date?
+ var originalUpdatedAt: Date?
+
+ try database.writeSynchronously { session in
+ guard let reminder = session.message(id: messageId)?.reminder else { return }
+ originalRemindAt = reminder.remindAt?.bridgeDate
+ originalCreatedAt = reminder.createdAt.bridgeDate
+ originalUpdatedAt = reminder.updatedAt.bridgeDate
+ }
+
+ // Simulate `deleteReminder` call
+ let exp = expectation(description: "completion is called")
+ repository.deleteReminder(
+ messageId: messageId,
+ cid: cid
+ ) { _ in
+ exp.fulfill()
+ }
+
+ // Verify reminder was optimistically deleted
+ var hasReminderAfterDelete = true
+ try database.writeSynchronously { session in
+ hasReminderAfterDelete = session.message(id: messageId)?.reminder != nil
+ }
+ XCTAssertFalse(hasReminderAfterDelete, "Reminder should be optimistically deleted")
+
+ // Simulate API failure
+ apiClient.test_simulateResponse(Result.failure(TestError()))
+
+ wait(for: [exp], timeout: defaultTimeout)
+
+ // Assert reminder was restored with original values
+ var restoredRemindAt: Date?
+ var restoredCreatedAt: Date?
+ var restoredUpdatedAt: Date?
+
+ try database.writeSynchronously { session in
+ guard let reminder = session.message(id: messageId)?.reminder else {
+ XCTFail("Reminder should be restored after API failure")
+ return
+ }
+
+ restoredRemindAt = reminder.remindAt?.bridgeDate
+ restoredCreatedAt = reminder.createdAt.bridgeDate
+ restoredUpdatedAt = reminder.updatedAt.bridgeDate
+ }
+
+ XCTAssertNearlySameDate(restoredRemindAt, originalRemindAt)
+ XCTAssertNearlySameDate(restoredCreatedAt, originalCreatedAt)
+ XCTAssertNearlySameDate(restoredUpdatedAt, originalUpdatedAt)
+ }
+}
diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware_Tests.swift
new file mode 100644
index 00000000000..52042d05fda
--- /dev/null
+++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ReminderUpdaterMiddleware_Tests.swift
@@ -0,0 +1,212 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class ReminderUpdaterMiddleware_Tests: XCTestCase {
+ var database: DatabaseContainer_Spy!
+ var middleware: ReminderUpdaterMiddleware!
+
+ override func setUp() {
+ super.setUp()
+ database = DatabaseContainer_Spy(kind: .inMemory)
+ middleware = ReminderUpdaterMiddleware()
+ }
+
+ override func tearDown() {
+ middleware = nil
+ database = nil
+ super.tearDown()
+ }
+
+ func test_reminderCreatedEvent_savesReminder() throws {
+ // Setup
+ let messageId = "test-message-id"
+ let cid = ChannelId(type: .messaging, id: "test-channel")
+ let reminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: nil,
+ createdAt: Date(),
+ updatedAt: Date()
+ )
+
+ let eventPayload = EventPayload(
+ eventType: .messageReminderCreated,
+ createdAt: Date(),
+ messageId: messageId,
+ reminder: reminderPayload
+ )
+
+ let event = try ReminderCreatedEventDTO(from: eventPayload)
+
+ // Save required data for reminder to reference
+ try database.writeSynchronously { session in
+ try session.saveChannel(payload: .dummy(cid: cid), query: nil, cache: nil)
+ try session.saveMessage(
+ payload: .dummy(messageId: messageId, authorUserId: "test-user"),
+ for: cid,
+ syncOwnReactions: false,
+ skipDraftUpdate: true,
+ cache: nil
+ )
+ }
+
+ // Execute
+ _ = middleware.handle(event: event, session: database.viewContext)
+
+ // Assert
+ let reminder = database.viewContext.message(id: messageId)?.reminder
+ XCTAssertNotNil(reminder, "Reminder should be saved")
+ XCTAssertEqual(reminder?.id, messageId, "Reminder ID should match message ID")
+ }
+
+ func test_reminderUpdatedEvent_updatesReminder() throws {
+ // Setup
+ let messageId = "test-message-id"
+ let cid = ChannelId(type: .messaging, id: "test-channel")
+ let initialDate = Date().addingTimeInterval(-3600) // 1 hour ago
+ let updatedDate = Date() // now
+
+ // First create the reminder
+ let initialReminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: initialDate,
+ createdAt: initialDate,
+ updatedAt: initialDate
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveChannel(payload: .dummy(cid: cid), query: nil, cache: nil)
+ try session.saveMessage(
+ payload: .dummy(messageId: messageId, authorUserId: "test-user"),
+ for: cid,
+ syncOwnReactions: false,
+ skipDraftUpdate: true,
+ cache: nil
+ )
+ try session.saveReminder(payload: initialReminderPayload, cache: nil)
+ }
+
+ // Create update payload
+ let updatedReminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: updatedDate,
+ createdAt: initialDate,
+ updatedAt: updatedDate
+ )
+
+ let eventPayload = EventPayload(
+ eventType: .messageReminderUpdated,
+ createdAt: Date(),
+ messageId: messageId,
+ reminder: updatedReminderPayload
+ )
+
+ let event = try ReminderUpdatedEventDTO(from: eventPayload)
+
+ // Execute
+ _ = middleware.handle(event: event, session: database.viewContext)
+
+ // Assert
+ let reminder = database.viewContext.message(id: messageId)?.reminder
+ XCTAssertNotNil(reminder, "Reminder should exist")
+ XCTAssertNearlySameDate(reminder?.remindAt?.bridgeDate, updatedDate)
+ }
+
+ func test_reminderDueNotificationEvent_updatesReminder() throws {
+ // Setup
+ let messageId = "test-message-id"
+ let cid = ChannelId(type: .messaging, id: "test-channel")
+ let initialDate = Date().addingTimeInterval(-3600) // 1 hour ago
+
+ // First create the reminder
+ let initialReminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: initialDate,
+ createdAt: initialDate,
+ updatedAt: initialDate
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveChannel(payload: .dummy(cid: cid), query: nil, cache: nil)
+ try session.saveMessage(
+ payload: .dummy(messageId: messageId, authorUserId: "test-user"),
+ for: cid,
+ syncOwnReactions: false,
+ skipDraftUpdate: true,
+ cache: nil
+ )
+ try session.saveReminder(payload: initialReminderPayload, cache: nil)
+ }
+
+ // Create due notification payload (same as the original in this case)
+ let eventPayload = EventPayload(
+ eventType: .messageReminderDue,
+ createdAt: Date(),
+ messageId: messageId,
+ reminder: initialReminderPayload
+ )
+
+ let event = try ReminderDueNotificationEventDTO(from: eventPayload)
+
+ // Execute
+ _ = middleware.handle(event: event, session: database.viewContext)
+
+ // Assert
+ let reminder = database.viewContext.message(id: messageId)?.reminder
+ XCTAssertNotNil(reminder, "Reminder should still exist after due notification")
+ }
+
+ func test_reminderDeletedEvent_deletesReminder() throws {
+ // Setup
+ let messageId = "test-message-id"
+ let cid = ChannelId(type: .messaging, id: "test-channel")
+
+ // First create the reminder
+ let reminderPayload = ReminderPayload(
+ channelCid: cid,
+ messageId: messageId,
+ remindAt: Date(),
+ createdAt: Date(),
+ updatedAt: Date()
+ )
+
+ try database.writeSynchronously { session in
+ try session.saveChannel(payload: .dummy(cid: cid), query: nil, cache: nil)
+ try session.saveMessage(
+ payload: .dummy(messageId: messageId, authorUserId: "test-user"),
+ for: cid,
+ syncOwnReactions: false,
+ skipDraftUpdate: true,
+ cache: nil
+ )
+ try session.saveReminder(payload: reminderPayload, cache: nil)
+ }
+
+ // Verify reminder exists
+ XCTAssertNotNil(database.viewContext.message(id: messageId)?.reminder, "Reminder should exist before deletion")
+
+ // Create delete event payload
+ let eventPayload = EventPayload(
+ eventType: .messageReminderDeleted,
+ createdAt: Date(),
+ messageId: messageId,
+ reminder: reminderPayload
+ )
+
+ let event = try ReminderDeletedEventDTO(from: eventPayload)
+
+ // Execute
+ _ = middleware.handle(event: event, session: database.viewContext)
+
+ // Assert
+ XCTAssertNil(database.viewContext.message(id: messageId)?.reminder, "Reminder should be deleted")
+ }
+}
diff --git a/Tests/StreamChatTests/WebSocketClient/Events/ReminderEvents_Tests.swift b/Tests/StreamChatTests/WebSocketClient/Events/ReminderEvents_Tests.swift
new file mode 100644
index 00000000000..bfcfb36ccc0
--- /dev/null
+++ b/Tests/StreamChatTests/WebSocketClient/Events/ReminderEvents_Tests.swift
@@ -0,0 +1,179 @@
+//
+// Copyright Ā© 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatTestTools
+import XCTest
+
+final class ReminderEvents_Tests: XCTestCase {
+ private let messageId = "477172a9-a59b-48dc-94a3-9aec4dc181bb"
+ private let cid = ChannelId(type: .messaging, id: "!members-vhPyEGDAjFA4JyC7fxDg3LsMFLGqKhXOKqZM-Y681_E")
+
+ var eventDecoder: EventDecoder!
+
+ override func setUp() {
+ super.setUp()
+ eventDecoder = EventDecoder()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ eventDecoder = nil
+ }
+
+ // MARK: - ReminderCreatedEvent Tests
+
+ func test_reminderCreatedEvent_decoding() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderCreated")
+ let event = try eventDecoder.decode(from: json) as? ReminderCreatedEventDTO
+
+ XCTAssertNotNil(event)
+ XCTAssertEqual(event?.messageId, "f7af18f2-0a46-431d-8901-19c105de7f0a")
+ XCTAssertEqual(event?.reminder.channelCid, cid)
+ XCTAssertNil(event?.reminder.remindAt)
+ XCTAssertEqual(event?.createdAt.description, "2025-03-20 15:50:09 +0000")
+ }
+
+ func test_reminderCreatedEvent_toDomainEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderCreated")
+ let event = try eventDecoder.decode(from: json) as? ReminderCreatedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ // Save required data
+ let channelId = event?.reminder.channelCid ?? cid
+ let messageId = event?.messageId ?? "test-message-id"
+ _ = try session.saveChannel(payload: .dummy(cid: channelId), query: nil, cache: nil)
+ _ = try session.saveMessage(payload: .dummy(messageId: messageId, authorUserId: "test-user"), for: channelId, cache: nil)
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? MessageReminderCreatedEvent)
+ XCTAssertEqual(domainEvent.messageId, "f7af18f2-0a46-431d-8901-19c105de7f0a")
+ XCTAssertEqual(domainEvent.reminder.id, "f7af18f2-0a46-431d-8901-19c105de7f0a")
+ XCTAssertEqual(domainEvent.reminder.channel.cid, channelId)
+ }
+
+ func test_reminderCreatedEvent_toDomainEvent_whenRemoveChannelOnly_shouldSaveChannelFromEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderCreated")
+ let event = try eventDecoder.decode(from: json) as? ReminderCreatedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? MessageReminderCreatedEvent)
+ XCTAssertEqual(domainEvent.messageId, event?.messageId)
+ XCTAssertEqual(domainEvent.reminder.id, event?.messageId)
+ XCTAssertEqual(domainEvent.reminder.channel.name, event?.reminder.channel?.name)
+ }
+
+ // MARK: - ReminderUpdatedEvent Tests
+
+ func test_reminderUpdatedEvent_decoding() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderUpdated")
+ let event = try eventDecoder.decode(from: json) as? ReminderUpdatedEventDTO
+
+ XCTAssertNotNil(event)
+ XCTAssertEqual(event?.messageId, messageId)
+ XCTAssertEqual(event?.reminder.channelCid, cid)
+ XCTAssertEqual(event?.reminder.remindAt?.description, "2025-03-20 15:50:58 +0000")
+ XCTAssertEqual(event?.createdAt.description, "2025-03-20 15:48:58 +0000")
+ }
+
+ func test_reminderUpdatedEvent_toDomainEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderUpdated")
+ let event = try eventDecoder.decode(from: json) as? ReminderUpdatedEventDTO
+ 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: messageId, authorUserId: "test-user"), for: cid, cache: nil)
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? MessageReminderUpdatedEvent)
+ XCTAssertEqual(domainEvent.messageId, messageId)
+ XCTAssertEqual(domainEvent.reminder.id, messageId)
+ XCTAssertEqual(domainEvent.reminder.channel.cid, cid)
+ XCTAssertEqual(domainEvent.reminder.remindAt?.description, "2025-03-20 15:50:58 +0000")
+ }
+
+ func test_reminderUpdatedEvent_toDomainEvent_returnsNilWhenMissingData() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderUpdated")
+ let event = try eventDecoder.decode(from: json) as? ReminderUpdatedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ // Don't save any data to test nil case
+ XCTAssertNil(event?.toDomainEvent(session: session))
+ }
+
+ // MARK: - ReminderDeletedEvent Tests
+
+ func test_reminderDeletedEvent_decoding() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderDeleted")
+ let event = try eventDecoder.decode(from: json) as? ReminderDeletedEventDTO
+
+ XCTAssertNotNil(event)
+ XCTAssertEqual(event?.messageId, messageId)
+ XCTAssertEqual(event?.reminder.channelCid, cid)
+ XCTAssertEqual(event?.reminder.remindAt?.description, "2025-03-20 15:50:58 +0000")
+ XCTAssertEqual(event?.createdAt.description, "2025-03-20 15:49:25 +0000")
+ }
+
+ func test_reminderDeletedEvent_toDomainEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderDeleted")
+ let event = try eventDecoder.decode(from: json) as? ReminderDeletedEventDTO
+ 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: messageId, authorUserId: "test-user"), for: cid, cache: nil)
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? MessageReminderDeletedEvent)
+ XCTAssertEqual(domainEvent.messageId, messageId)
+ XCTAssertEqual(domainEvent.reminder.id, messageId)
+ XCTAssertEqual(domainEvent.reminder.channel.cid, cid)
+ XCTAssertEqual(domainEvent.reminder.remindAt?.description, "2025-03-20 15:50:58 +0000")
+ }
+
+ func test_reminderDeletedEvent_toDomainEvent_returnsNilWhenMissingData() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderDeleted")
+ let event = try eventDecoder.decode(from: json) as? ReminderDeletedEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ // Don't save any data to test nil case
+ XCTAssertNil(event?.toDomainEvent(session: session))
+ }
+
+ // MARK: - ReminderDueEvent Tests
+
+ func test_reminderDueEvent_decoding() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderDue")
+ let event = try eventDecoder.decode(from: json) as? ReminderDueNotificationEventDTO
+
+ XCTAssertNotNil(event)
+ XCTAssertEqual(event?.messageId, messageId)
+ XCTAssertEqual(event?.reminder.channelCid, cid)
+ XCTAssertEqual(event?.reminder.remindAt?.description, "2025-03-20 15:50:58 +0000")
+ XCTAssertEqual(event?.createdAt.description, "2025-03-20 15:48:58 +0000")
+ }
+
+ func test_reminderDueEvent_toDomainEvent() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderDue")
+ let event = try eventDecoder.decode(from: json) as? ReminderDueNotificationEventDTO
+ 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: messageId, authorUserId: "test-user"), for: cid, cache: nil)
+
+ let domainEvent = try XCTUnwrap(event?.toDomainEvent(session: session) as? MessageReminderDueEvent)
+ XCTAssertEqual(domainEvent.messageId, messageId)
+ XCTAssertEqual(domainEvent.reminder.id, messageId)
+ XCTAssertEqual(domainEvent.reminder.channel.cid, cid)
+ XCTAssertEqual(domainEvent.reminder.remindAt?.description, "2025-03-20 15:50:58 +0000")
+ }
+
+ func test_reminderDueEvent_toDomainEvent_returnsNilWhenMissingData() throws {
+ let json = XCTestCase.mockData(fromJSONFile: "ReminderDue")
+ let event = try eventDecoder.decode(from: json) as? ReminderDueNotificationEventDTO
+ let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
+
+ // Don't save any data to test nil case
+ XCTAssertNil(event?.toDomainEvent(session: session))
+ }
+}
From 902dabca4d045d3357e8913faa090067505d472f Mon Sep 17 00:00:00 2001
From: Alexey Alter-Pesotskiy
Date: Fri, 6 Jun 2025 11:58:14 +0100
Subject: [PATCH 05/11] [CI] Add changelog to TestFlight build's test
instructions (#3691)
---
Gemfile.lock | 54 ++++++++++++++++++++++-----------------------
fastlane/Fastfile | 4 +++-
fastlane/Pluginfile | 2 +-
3 files changed, 31 insertions(+), 29 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index b9688475b4d..b1b705f4b61 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -25,28 +25,28 @@ GEM
artifactory (3.0.17)
ast (2.4.3)
atomos (0.1.3)
- aws-eventstream (1.3.2)
- aws-partitions (1.1092.0)
- aws-sdk-core (3.222.2)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1112.0)
+ aws-sdk-core (3.225.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
- aws-sdk-kms (1.99.0)
- aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sdk-kms (1.103.0)
+ aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.183.0)
- aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sdk-s3 (1.189.0)
+ aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
- aws-sigv4 (1.11.0)
+ aws-sigv4 (1.12.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
- base64 (0.2.0)
- benchmark (0.4.0)
- bigdecimal (3.1.9)
+ base64 (0.3.0)
+ benchmark (0.4.1)
+ bigdecimal (3.2.2)
claide (1.1.0)
claide-plugins (0.9.2)
cork
@@ -96,7 +96,7 @@ GEM
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.5)
- connection_pool (2.5.2)
+ connection_pool (2.5.3)
cork (0.3.0)
colored2 (~> 3.1)
danger (9.5.1)
@@ -122,7 +122,7 @@ GEM
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
- drb (2.2.1)
+ drb (2.2.3)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
@@ -160,7 +160,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
- fastlane (2.227.1)
+ fastlane (2.227.2)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -208,14 +208,14 @@ GEM
fastlane
pry
fastlane-plugin-sonarcloud_metric_kit (0.2.1)
- fastlane-plugin-stream_actions (0.3.79)
+ fastlane-plugin-stream_actions (0.3.82)
xctest_list (= 1.2.1)
fastlane-plugin-versioning (0.7.1)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
- faye-websocket (0.11.3)
+ faye-websocket (0.12.0)
eventmachine (>= 0.12.0)
- websocket-driver (>= 0.5.1)
+ websocket-driver (>= 0.8.0)
ffi (1.17.2)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
@@ -267,7 +267,7 @@ GEM
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
- json (2.11.3)
+ json (2.12.2)
jwt (2.10.1)
base64
kramdown (2.5.1)
@@ -278,7 +278,7 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- mini_portile2 (2.8.8)
+ mini_portile2 (2.8.9)
minitest (5.25.5)
molinillo (0.8.0)
multi_json (1.15.0)
@@ -289,8 +289,8 @@ GEM
nanaimo (0.4.0)
nap (1.1.0)
naturally (2.2.1)
- net-http-persistent (4.0.5)
- connection_pool (~> 2.2)
+ net-http-persistent (4.0.6)
+ connection_pool (~> 2.2, >= 2.2.4)
netrc (0.11.0)
nio4r (2.7.4)
nkf (0.2.0)
@@ -328,7 +328,7 @@ GEM
rackup (2.2.1)
rack (>= 3)
rainbow (3.1.1)
- rake (13.2.1)
+ rake (13.3.0)
rchardet (1.9.0)
regexp_parser (2.10.0)
representable (3.2.0)
@@ -348,7 +348,7 @@ GEM
rubocop-ast (>= 1.23.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.44.1)
+ rubocop-ast (1.45.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.19.1)
@@ -365,7 +365,7 @@ GEM
faraday (>= 0.17.3, < 3)
securerandom (0.4.1)
security (0.1.5)
- signet (0.19.0)
+ signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -386,7 +386,7 @@ GEM
clamp (~> 1.3)
nokogiri (>= 1.14.3)
xcodeproj (~> 1.27)
- sorbet-runtime (0.5.12043)
+ sorbet-runtime (0.5.12157)
stream-chat-ruby (3.0.0)
faraday
faraday-multipart
@@ -410,7 +410,7 @@ GEM
concurrent-ruby (~> 1.0)
uber (0.1.0)
unicode-display_width (2.6.0)
- websocket-driver (0.7.7)
+ websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@@ -440,7 +440,7 @@ DEPENDENCIES
fastlane-plugin-create_xcframework
fastlane-plugin-lizard
fastlane-plugin-sonarcloud_metric_kit
- fastlane-plugin-stream_actions (= 0.3.79)
+ fastlane-plugin-stream_actions (= 0.3.82)
fastlane-plugin-versioning
faye-websocket
json
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index c108679650d..a00b91fccdc 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -298,7 +298,9 @@ lane :uikit_testflight_build do |options|
app_version: app_version,
app_identifier: 'io.getstream.iOS.ChatDemoApp',
configuration: configuration,
- extensions: ['DemoShare']
+ extensions: ['DemoShare'],
+ use_changelog: true,
+ is_manual_upload: is_manual_upload
)
end
diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile
index dcc7813c3b0..1fcaf10fac8 100644
--- a/fastlane/Pluginfile
+++ b/fastlane/Pluginfile
@@ -5,4 +5,4 @@
gem 'fastlane-plugin-versioning'
gem 'fastlane-plugin-create_xcframework'
gem 'fastlane-plugin-sonarcloud_metric_kit'
-gem 'fastlane-plugin-stream_actions', '0.3.79'
+gem 'fastlane-plugin-stream_actions', '0.3.82'
From 9cd1bcf46d7e0d071a8a809afcce76244c89a325 Mon Sep 17 00:00:00 2001
From: Martin Mitrevski
Date: Fri, 6 Jun 2025 14:57:52 +0200
Subject: [PATCH 06/11] Updated CHANGELOG (#3692)
---
CHANGELOG.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cde2de29c2d..e1a4243ff0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,11 +3,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
# Upcoming
-### š Changed
-
-# [4.79.1](https://github.com/GetStream/stream-chat-swift/releases/tag/4.79.1)
-_June 03, 2025_
-
## StreamChat
### ā
Added
- Add new `Filter.isNil` to make it easier to query by nil values [#3623](https://github.com/GetStream/stream-chat-swift/pull/3623)
@@ -16,6 +11,11 @@ _June 03, 2025_
- Add `ChatMessageController.updateReminder()`
- Add `ChatMessageController.deleteReminder()`
- Add `MessageReminderListController` and `MessageReminderListQuery`
+
+# [4.79.1](https://github.com/GetStream/stream-chat-swift/releases/tag/4.79.1)
+_June 03, 2025_
+
+## StreamChat
### š Fixed
- Fix an issue where completion handler was called twice after waiting for token refresh [#3683](https://github.com/GetStream/stream-chat-swift/pull/3683)
- Fix message not marked as published if it was previously intercepted [#3687](https://github.com/GetStream/stream-chat-swift/pull/3687)
From 5b4451c126c0123d7d0fe81958e45a044d418b7a Mon Sep 17 00:00:00 2001
From: Alexey Alter-Pesotskiy
Date: Tue, 10 Jun 2025 12:01:54 +0100
Subject: [PATCH 07/11] [CI] Bump snapshot version after every release (#3693)
---
Gemfile.lock | 16 ++++++++--------
fastlane/Fastfile | 10 +++++-----
fastlane/Pluginfile | 2 +-
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index b1b705f4b61..eaf9ac3c028 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -26,7 +26,7 @@ GEM
ast (2.4.3)
atomos (0.1.3)
aws-eventstream (1.4.0)
- aws-partitions (1.1112.0)
+ aws-partitions (1.1114.0)
aws-sdk-core (3.225.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -34,7 +34,7 @@ GEM
base64
jmespath (~> 1, >= 1.6.1)
logger
- aws-sdk-kms (1.103.0)
+ aws-sdk-kms (1.104.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.189.0)
@@ -160,7 +160,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
- fastlane (2.227.2)
+ fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -208,7 +208,7 @@ GEM
fastlane
pry
fastlane-plugin-sonarcloud_metric_kit (0.2.1)
- fastlane-plugin-stream_actions (0.3.82)
+ fastlane-plugin-stream_actions (0.3.83)
xctest_list (= 1.2.1)
fastlane-plugin-versioning (0.7.1)
fastlane-sirp (1.0.0)
@@ -288,7 +288,7 @@ GEM
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
- naturally (2.2.1)
+ naturally (2.2.2)
net-http-persistent (4.0.6)
connection_pool (~> 2.2, >= 2.2.4)
netrc (0.11.0)
@@ -348,7 +348,7 @@ GEM
rubocop-ast (>= 1.23.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.45.0)
+ rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.19.1)
@@ -386,7 +386,7 @@ GEM
clamp (~> 1.3)
nokogiri (>= 1.14.3)
xcodeproj (~> 1.27)
- sorbet-runtime (0.5.12157)
+ sorbet-runtime (0.5.12164)
stream-chat-ruby (3.0.0)
faraday
faraday-multipart
@@ -440,7 +440,7 @@ DEPENDENCIES
fastlane-plugin-create_xcframework
fastlane-plugin-lizard
fastlane-plugin-sonarcloud_metric_kit
- fastlane-plugin-stream_actions (= 0.3.82)
+ fastlane-plugin-stream_actions (= 0.3.83)
fastlane-plugin-versioning
faye-websocket
json
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index a00b91fccdc..478629e9a8f 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -67,7 +67,6 @@ end
desc 'Start a new release'
lane :release do |options|
- previous_version_number = last_git_tag
artifacts_path = File.absolute_path('../StreamChatArtifacts.json')
extra_changes = lambda do |release_version|
# Set the framework version on the artifacts
@@ -76,7 +75,9 @@ lane :release do |options|
File.write(artifacts_path, JSON.dump(artifacts))
# Set the framework version in SystemEnvironment+Version.swift
- new_content = File.read(swift_environment_path).gsub!(previous_version_number, release_version).gsub('-SNAPSHOT', '')
+ old_content = File.read(swift_environment_path)
+ current_version = old_content[/version: String = "([^"]+)"/, 1]
+ new_content = old_content.gsub(current_version, release_version)
File.open(swift_environment_path, 'w') { |f| f.puts(new_content) }
# Update sdk sizes
@@ -103,11 +104,10 @@ end
lane :merge_main do
merge_main_to_develop
- current_version = get_sdk_version_from_environment
- add_snapshot_to_current_version(file_path: swift_environment_path)
+ update_release_version_to_snapshot(file_path: swift_environment_path)
ensure_git_branch(branch: 'develop')
sh("git add #{swift_environment_path}")
- sh("git commit -m 'Add snapshot postfix to v#{current_version}'")
+ sh("git commit -m 'Update release version to snapshot'")
sh('git push')
end
diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile
index 1fcaf10fac8..9f04f172643 100644
--- a/fastlane/Pluginfile
+++ b/fastlane/Pluginfile
@@ -5,4 +5,4 @@
gem 'fastlane-plugin-versioning'
gem 'fastlane-plugin-create_xcframework'
gem 'fastlane-plugin-sonarcloud_metric_kit'
-gem 'fastlane-plugin-stream_actions', '0.3.82'
+gem 'fastlane-plugin-stream_actions', '0.3.83'
From 9e230f95f194805c124d3a54477c762d74a7960c Mon Sep 17 00:00:00 2001
From: Stream SDK Bot <60655709+Stream-SDK-Bot@users.noreply.github.com>
Date: Mon, 16 Jun 2025 09:41:29 +0100
Subject: [PATCH 08/11] [CI] Sync Mock Server (#3696)
---
.../Fixtures/JSONs/http_add_member.json | 40 +--
.../Fixtures/JSONs/http_attachment.json | 4 +-
.../Fixtures/JSONs/http_channel_creation.json | 58 ++--
.../Fixtures/JSONs/http_channel_removal.json | 46 +--
.../Fixtures/JSONs/http_channels.json | 70 ++---
.../Fixtures/JSONs/http_events.json | 12 +-
.../Fixtures/JSONs/http_giphy_link.json | 28 +-
.../Fixtures/JSONs/http_message.json | 18 +-
.../JSONs/http_message_ephemeral.json | 72 ++---
.../Fixtures/JSONs/http_reaction.json | 56 ++--
.../Fixtures/JSONs/http_truncate.json | 70 ++---
.../Fixtures/JSONs/http_unsplash_link.json | 22 +-
.../Fixtures/JSONs/http_youtube_link.json | 18 +-
.../Fixtures/JSONs/ws_events.json | 86 +++---
.../Fixtures/JSONs/ws_events_channel.json | 284 ++++++++----------
.../Fixtures/JSONs/ws_events_member.json | 20 +-
.../Fixtures/JSONs/ws_health_check.json | 12 +-
.../Fixtures/JSONs/ws_message.json | 110 +++----
.../Fixtures/JSONs/ws_reaction.json | 74 ++---
19 files changed, 515 insertions(+), 585 deletions(-)
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_add_member.json
index 82db8497b34..de599c619b8 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": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "id": "ec2807ff-5c60-41bc-a816-49578260471a",
"type": "messaging",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "last_message_at": "2025-06-01T00:22:32.65955Z",
- "created_at": "2025-06-01T00:22:30.030603Z",
- "updated_at": "2025-06-01T00:22:30.030603Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "last_message_at": "2025-06-15T00:25:36.86243Z",
+ "created_at": "2025-06-15T00:25:33.285826Z",
+ "updated_at": "2025-06-15T00:25:33.285826Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -14,10 +14,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"team": "test",
"type": "team",
@@ -133,8 +133,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -154,13 +154,13 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -177,10 +177,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"team": "test",
"type": "team",
@@ -188,8 +188,8 @@
"birthland": "Tatooine"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -209,7 +209,7 @@
"updated_at": "2025-03-28T15:21:20.061525Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T14:14:34.87384Z",
+ "last_active": "2025-06-14T17:34:03.224367Z",
"blocked_user_ids": [],
"birthland": "Polis Massa",
"private_settings": {
@@ -222,8 +222,8 @@
}
},
"status": "member",
- "created_at": "2025-06-01T00:22:34.135368Z",
- "updated_at": "2025-06-01T00:22:34.135368Z",
+ "created_at": "2025-06-15T00:25:38.137522Z",
+ "updated_at": "2025-06-15T00:25:38.137522Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -231,5 +231,5 @@
"notifications_muted": false
}
],
- "duration": "34.61ms"
+ "duration": "38.76ms"
}
\ 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 d8e04dad5e8..0cf07795844 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/2d6917b0-178e-4e24-a5f0-142c60de7fe2.yoda.jpg?Key-Pair-Id=APKAIHG36VEWPDULE23Q&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9mcmFua2Z1cnQuc3RyZWFtLWlvLWNkbi5jb20vMTAyMzk5L2ltYWdlcy8yZDY5MTdiMC0xNzhlLTRlMjQtYTVmMC0xNDJjNjBkZTdmZTIueW9kYS5qcGc~Km9oPTAqb3c9MCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3NDk5NDY5NTR9fX1dfQ__&Signature=RhbWiNYYCQJeqk3j--ykjan6FsnJDQZAxGVMcZQa7o49NI5QbREeDlljytMsPVDHFVnBSpMM~KczxlXw52sPtV7RZ1q0G0IGNabU1SPv7oSxJDK7I1XB2bpW1DtMBh1ntP9TIawE79QBl2kQ3XBaX0woEcz2cSIPdZS5kKcnhim~eRpXNKdg38RsZro624z3hZYJbP217aJ9PAP6zTtOAWdnNrTtFmvBY3SlQ1OORREld9~GPqIAfvBBgIk0xIJ73BcH59Oq37jbldB9yOlSo8kPG1gkZPDbpu6PUOrcOa8n6URTt6KNIt55q6O6BkRI2ORtWaBD~SEKmH3bQssHFw__&oh=0&ow=0",
- "duration": "142.15ms"
+ "file": "https://frankfurt.stream-io-cdn.com/102399/images/84500679-c986-471d-be2a-461542a4dbaf.yoda.jpg?Key-Pair-Id=APKAIHG36VEWPDULE23Q&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9mcmFua2Z1cnQuc3RyZWFtLWlvLWNkbi5jb20vMTAyMzk5L2ltYWdlcy84NDUwMDY3OS1jOTg2LTQ3MWQtYmUyYS00NjE1NDJhNGRiYWYueW9kYS5qcGc~Km9oPTAqb3c9MCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3NTExNTY3Mzh9fX1dfQ__&Signature=N0MgP9HIGp5hCyxzq-70m0lO2Km3P73oZC-jepZlIxtv4go-DnWvgPHGKEw6bo60Ud2vV8whVkG92U1c5OuU~dpwwZj8L8q0VqMDA7T~VOVEEN4VlwlD2VPElyA2IUCISqkuTd61xO9CqR0nsmHum48zdWPZn001uNZwN5rWb1GJfJijeVPhu1rkytdygNeE1ZeZiDG0gUKt7~9H30~HS75zivkryTkKRD9yhmmeInwV45FocoDzUGsPc7Ux8xW6DaxXhUaNC6FdQp9GMLCmBNrgZjMLZsS-mm~qsl78ZR24eEzuT8mwfVh9rmAmW9xUdyXEZCUod6KpMrLiRGahIA__&oh=0&ow=0",
+ "duration": "124.23ms"
}
\ 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 d003fa1d57e..13fb1c099af 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": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "id": "ec2807ff-5c60-41bc-a816-49578260471a",
"type": "messaging",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:30.030603Z",
- "updated_at": "2025-06-01T00:22:30.030603Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:33.285826Z",
+ "updated_at": "2025-06-15T00:25:33.285826Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -13,10 +13,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"team": "test",
"type": "team",
@@ -134,7 +134,7 @@
"blocked_user_ids": [],
"birthland": "Serenno"
},
- "last_read": "2025-06-01T00:22:30.076395768Z",
+ "last_read": "2025-06-15T00:25:33.34121176Z",
"unread_messages": 0
},
{
@@ -149,11 +149,11 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
- "last_read": "2025-06-01T00:22:30.076395768Z",
+ "last_read": "2025-06-15T00:25:33.34121176Z",
"unread_messages": 0
},
{
@@ -165,17 +165,17 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"birthland": "Tatooine",
"team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}"
},
- "last_read": "2025-06-01T00:22:30.076395768Z",
+ "last_read": "2025-06-15T00:25:33.34121176Z",
"unread_messages": 0
}
],
@@ -198,8 +198,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -219,13 +219,13 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -242,10 +242,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
@@ -253,8 +253,8 @@
"type": "team"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -271,19 +271,19 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
"team": "test",
- "type": "team"
+ "type": "team",
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -291,5 +291,5 @@
"notifications_muted": false
},
"threads": [],
- "duration": "88.91ms"
+ "duration": "145.45ms"
}
\ 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 ac2b55c5260..f6593d1e860 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channel_removal.json
@@ -1,12 +1,12 @@
{
- "duration": "25.08ms",
+ "duration": "32.46ms",
"channel": {
- "id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "id": "ec2807ff-5c60-41bc-a816-49578260471a",
"type": "messaging",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:30.030603Z",
- "updated_at": "2025-06-01T00:22:38.557677Z",
- "deleted_at": "2025-06-01T00:22:39.321634Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:33.285826Z",
+ "updated_at": "2025-06-15T00:25:47.043063Z",
+ "deleted_at": "2025-06-15T00:25:47.390053Z",
"created_by": null,
"frozen": false,
"disabled": false,
@@ -29,8 +29,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -50,13 +50,13 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -73,10 +73,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:47.038320949Z",
"blocked_user_ids": [],
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
@@ -84,8 +84,8 @@
"type": "team"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -105,7 +105,7 @@
"updated_at": "2025-03-28T15:21:20.061525Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T14:14:34.87384Z",
+ "last_active": "2025-06-14T17:34:03.224367Z",
"blocked_user_ids": [],
"birthland": "Polis Massa",
"private_settings": {
@@ -118,8 +118,8 @@
}
},
"status": "member",
- "created_at": "2025-06-01T00:22:34.135368Z",
- "updated_at": "2025-06-01T00:22:34.135368Z",
+ "created_at": "2025-06-15T00:25:38.137522Z",
+ "updated_at": "2025-06-15T00:25:38.137522Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -176,7 +176,7 @@
}
]
},
- "truncated_at": "2025-06-01T00:22:39.321634Z",
+ "truncated_at": "2025-06-15T00:25:47.390053Z",
"truncated_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -185,15 +185,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
- "team": "test"
+ "birthland": "Tatooine"
}
}
}
\ 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 ab8ee69f07b..ee50a4becb2 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_channels.json
@@ -2,11 +2,11 @@
"channels": [
{
"channel": {
- "id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "id": "ec2807ff-5c60-41bc-a816-49578260471a",
"type": "messaging",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:30.030603Z",
- "updated_at": "2025-06-01T00:22:30.030603Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:33.285826Z",
+ "updated_at": "2025-06-15T00:25:33.285826Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -15,15 +15,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"frozen": false,
"disabled": false,
@@ -137,7 +137,7 @@
"birthland": "Serenno"
},
"unread_messages": 0,
- "last_read": "2025-06-01T00:22:31Z"
+ "last_read": "2025-06-15T00:25:34Z"
},
{
"user": {
@@ -151,12 +151,12 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
"unread_messages": 0,
- "last_read": "2025-06-01T00:22:31Z"
+ "last_read": "2025-06-15T00:25:34Z"
},
{
"user": {
@@ -167,18 +167,18 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"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-06-01T00:22:31Z"
+ "last_read": "2025-06-15T00:25:34Z"
}
],
"members": [
@@ -200,8 +200,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -221,13 +221,13 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -244,19 +244,19 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -273,19 +273,19 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
"team": "test",
- "type": "team"
+ "type": "team",
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"channel_role": "channel_member",
@@ -294,5 +294,5 @@
"threads": []
}
],
- "duration": "74.07ms"
+ "duration": "92.12ms"
}
\ 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 fba9436dc43..5e550a393b8 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:42dcc850-6b61-48bd-9316-70665fb826df",
- "channel_id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "channel_id": "ec2807ff-5c60-41bc-a816-49578260471a",
"channel_type": "messaging",
"user": {
"id": "luke_skywalker",
@@ -12,17 +12,17 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine"
},
- "created_at": "2025-06-01T00:22:31.554747791Z"
+ "created_at": "2025-06-15T00:25:34.999482144Z"
},
- "duration": "6.76ms"
+ "duration": "6.47ms"
}
\ 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 952bed18790..3d6eef9c4ba 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": "73e80ef2-c814-4b12-bb2f-85cd2c4f6539",
+ "id": "d82c481c-541b-4da4-a704-6ec285420492",
"text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
"html": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi
\n",
"type": "regular",
@@ -12,15 +12,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
- "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}"
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine"
},
"attachments": [
{
@@ -28,9 +28,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.Y2lkPTc5MGI3NjExdHU0bmN1MThnNXRqZWNjYjdrN21xeHN6Z21zZnVwaHYwN2IxZDJ0byZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
- "thumb_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHU0bmN1MThnNXRqZWNjYjdrN21xeHN6Z21zZnVwaHYwN2IxZDJ0byZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
- "asset_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExdHU0bmN1MThnNXRqZWNjYjdrN21xeHN6Z21zZnVwaHYwN2IxZDJ0byZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.mp4",
+ "image_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZmdiZ3NydzJ5NDZxbWVhajAyYzNmZ3BwYWJndWM4aXJyOXQwZXBydyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
+ "thumb_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZmdiZ3NydzJ5NDZxbWVhajAyYzNmZ3BwYWJndWM4aXJyOXQwZXBydyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.webp",
+ "asset_url": "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZmdiZ3NydzJ5NDZxbWVhajAyYzNmZ3BwYWJndWM4aXJyOXQwZXBydyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/gw3IWyGkC0rsazTi/giphy.mp4",
"og_scrape_url": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi"
}
],
@@ -40,15 +40,15 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:38.142136Z",
- "updated_at": "2025-06-01T00:22:38.142136Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:46.443211Z",
+ "updated_at": "2025-06-15T00:25:46.443211Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
+ "language": "en",
"en_text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
- "fr_text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi",
- "language": "en"
+ "fr_text": "https://giphy.com/gifs/test-gw3IWyGkC0rsazTi"
},
"silent": false,
"pinned": false,
@@ -57,5 +57,5 @@
"pin_expires": null,
"restricted_visibility": []
},
- "duration": "217.87ms"
+ "duration": "687.06ms"
}
\ 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 dd5decba945..92020781f10 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_message.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -12,10 +12,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"birthland": "Tatooine",
"team": "test",
@@ -29,15 +29,15 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:32.65955Z",
- "updated_at": "2025-06-01T00:22:32.65955Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:36.86243Z",
+ "updated_at": "2025-06-15T00:25:36.86243Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
+ "language": "en",
"en_text": "Test",
- "fr_text": "Testez",
- "language": "en"
+ "fr_text": "Testez"
},
"silent": false,
"pinned": false,
@@ -46,5 +46,5 @@
"pin_expires": null,
"restricted_visibility": []
},
- "duration": "474.26ms"
+ "duration": "1352.04ms"
}
\ 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 7250ad5fc97..c4e6087afca 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": "7445d79f-47ae-4be2-a99d-24b93c866e41",
+ "id": "82168f7f-d7e2-48a1-9273-417d03365da0",
"text": "/giphy Test",
"command": "giphy",
"html": "/giphy Test
\n",
@@ -13,22 +13,22 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
- "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}"
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine"
},
"attachments": [
{
"type": "giphy",
"title": "Test",
- "title_link": "https://giphy.com/gifs/sourthensweet2-test-test-qa-gw3EQMe9rugnpcd2",
- "thumb_url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/giphy.gif",
+ "title_link": "https://giphy.com/gifs/rhuber-test-EtpYMjyYJowVy",
+ "thumb_url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/giphy.gif",
"actions": [
{
"name": "image_action",
@@ -54,52 +54,52 @@
],
"giphy": {
"original": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/giphy.gif",
- "width": "316",
- "height": "177",
- "size": "1920215",
- "frames": "111"
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/giphy.gif",
+ "width": "400",
+ "height": "167",
+ "size": "1569544",
+ "frames": "49"
},
"fixed_height": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/200.gif",
- "width": "357",
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/200.gif",
+ "width": "479",
"height": "200",
- "size": "2684384",
+ "size": "1576548",
"frames": ""
},
"fixed_height_still": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/200_s.gif",
- "width": "357",
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/200_s.gif",
+ "width": "479",
"height": "200",
- "size": "29454",
+ "size": "32259",
"frames": ""
},
"fixed_height_downsampled": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/200_d.gif",
- "width": "357",
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/200_d.gif",
+ "width": "479",
"height": "200",
- "size": "159066",
+ "size": "212805",
"frames": ""
},
"fixed_width": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/200w.gif",
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/200w.gif",
"width": "200",
- "height": "112",
- "size": "1236738",
+ "height": "84",
+ "size": "331240",
"frames": ""
},
"fixed_width_still": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/200w_s.gif",
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/200w_s.gif",
"width": "200",
- "height": "112",
- "size": "13302",
+ "height": "84",
+ "size": "8017",
"frames": ""
},
"fixed_width_downsampled": {
- "url": "https://media2.giphy.com/media/v1.Y2lkPWM0YjAzNjc1ZXhndHBnc3EzejdpaWt0cTBiejQyc3J6ZzlxejViNXpoZ242OTE4bSZlcD12MV9naWZzX3NlYXJjaCZjdD1n/gw3EQMe9rugnpcd2/200w_d.gif",
+ "url": "https://media3.giphy.com/media/v1.Y2lkPWM0YjAzNjc1amdqbTV3eXl6d2dqZ3hnOXJmdGt4ZHJsZmNicDFpYjNoejJ6cWNneiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/EtpYMjyYJowVy/200w_d.gif",
"width": "200",
- "height": "112",
- "size": "68805",
+ "height": "84",
+ "size": "43614",
"frames": ""
}
}
@@ -111,15 +111,15 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:35.700273Z",
- "updated_at": "2025-06-01T00:22:35.700273Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:39.407203Z",
+ "updated_at": "2025-06-15T00:25:39.407203Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
- "en_text": "/giphy Test",
"fr_text": "/Test Giphy",
- "language": "en"
+ "language": "en",
+ "en_text": "/giphy Test"
},
"silent": false,
"pinned": false,
@@ -132,5 +132,5 @@
"name": "Giphy"
}
},
- "duration": "47.41ms"
+ "duration": "272.79ms"
}
\ 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 e42119d7b6c..bc1bad366a4 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_reaction.json
@@ -1,6 +1,6 @@
{
"message": {
- "id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -12,10 +12,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"birthland": "Tatooine",
"team": "test",
@@ -25,7 +25,7 @@
"attachments": [],
"latest_reactions": [
{
- "message_id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "message_id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
@@ -35,25 +35,25 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
"team": "test",
- "type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}"
+ "type": "team"
},
"type": "like",
"score": 1,
- "created_at": "2025-06-01T00:22:33.420026Z",
- "updated_at": "2025-06-01T00:22:33.420026Z"
+ "created_at": "2025-06-15T00:25:37.516538Z",
+ "updated_at": "2025-06-15T00:25:37.516538Z"
}
],
"own_reactions": [
{
- "message_id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "message_id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
@@ -63,20 +63,20 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"type": "like",
"score": 1,
- "created_at": "2025-06-01T00:22:33.420026Z",
- "updated_at": "2025-06-01T00:22:33.420026Z"
+ "created_at": "2025-06-15T00:25:37.516538Z",
+ "updated_at": "2025-06-15T00:25:37.516538Z"
}
],
"reaction_counts": {
@@ -89,15 +89,15 @@
"like": {
"count": 1,
"sum_scores": 1,
- "first_reaction_at": "2025-06-01T00:22:33.420026Z",
- "last_reaction_at": "2025-06-01T00:22:33.420026Z"
+ "first_reaction_at": "2025-06-15T00:25:37.516538Z",
+ "last_reaction_at": "2025-06-15T00:25:37.516538Z"
}
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:32.65955Z",
- "updated_at": "2025-06-01T00:22:33.428305Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:36.86243Z",
+ "updated_at": "2025-06-15T00:25:37.529233Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
@@ -113,7 +113,7 @@
"restricted_visibility": []
},
"reaction": {
- "message_id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "message_id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
@@ -123,10 +123,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"team": "test",
"type": "team",
@@ -135,8 +135,8 @@
},
"type": "like",
"score": 1,
- "created_at": "2025-06-01T00:22:33.420026Z",
- "updated_at": "2025-06-01T00:22:33.420026Z"
+ "created_at": "2025-06-15T00:25:37.516538Z",
+ "updated_at": "2025-06-15T00:25:37.516538Z"
},
- "duration": "32.87ms"
+ "duration": "39.84ms"
}
\ 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 a8f9d290a44..953f6dcc36c 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_truncate.json
@@ -1,12 +1,12 @@
{
- "duration": "69.26ms",
+ "duration": "70.67ms",
"channel": {
- "id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "id": "ec2807ff-5c60-41bc-a816-49578260471a",
"type": "messaging",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
"last_message_at": "0001-01-01T00:00:00Z",
- "created_at": "2025-06-01T00:22:30.030603Z",
- "updated_at": "2025-06-01T00:22:38.557677Z",
+ "created_at": "2025-06-15T00:25:33.285826Z",
+ "updated_at": "2025-06-15T00:25:47.043063Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -15,15 +15,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
- "team": "test"
+ "birthland": "Tatooine"
},
"frozen": false,
"disabled": false,
@@ -46,8 +46,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -67,13 +67,13 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -90,10 +90,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine",
@@ -101,8 +101,8 @@
"type": "team"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -122,8 +122,9 @@
"updated_at": "2025-03-28T15:21:20.061525Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T14:14:34.87384Z",
+ "last_active": "2025-06-14T17:34:03.224367Z",
"blocked_user_ids": [],
+ "birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
"enabled": false
@@ -131,12 +132,11 @@
"typingIndicators": {
"enabled": false
}
- },
- "birthland": "Polis Massa"
+ }
},
"status": "member",
- "created_at": "2025-06-01T00:22:34.135368Z",
- "updated_at": "2025-06-01T00:22:34.135368Z",
+ "created_at": "2025-06-15T00:25:38.137522Z",
+ "updated_at": "2025-06-15T00:25:38.137522Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -194,7 +194,7 @@
}
]
},
- "truncated_at": "2025-06-01T00:22:38.550767Z",
+ "truncated_at": "2025-06-15T00:25:47.035983Z",
"truncated_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -203,20 +203,20 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
"name": "Sync Mock Server"
},
"message": {
- "id": "c13cf987-25a8-4ba9-8d3a-42b378183911",
+ "id": "d636862b-394f-48af-a9f2-6b4fa8e45d98",
"text": "Channel truncated",
"html": "Channel truncated
\n",
"type": "system",
@@ -228,10 +228,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"team": "test",
"type": "team",
@@ -245,9 +245,9 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:38.550768Z",
- "updated_at": "2025-06-01T00:22:38.550768Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:47.035984Z",
+ "updated_at": "2025-06-15T00:25:47.035984Z",
"shadowed": false,
"mentioned_users": [],
"silent": false,
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/http_unsplash_link.json
index 417baf30c9b..eb925d7ccd6 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": "043cfe8d-ee10-4922-8c12-367f45d32fef",
+ "id": "b3021d20-53c7-4a2d-a572-8aba1c69ab72",
"text": "https://unsplash.com/photos/1_2d3MRbI9c",
"html": "https://unsplash.com/photos/1_2d3MRbI9c
\n",
"type": "regular",
@@ -12,15 +12,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
- "team": "test"
+ "birthland": "Tatooine"
},
"attachments": [
{
@@ -29,8 +29,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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQ4NzM2NDY4fA&ixlib=rb-4.1.0",
- "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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQ4NzM2NDY4fA&ixlib=rb-4.1.0",
+ "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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQ5OTQ3MTQ0fA&ixlib=rb-4.1.0",
+ "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=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQ5OTQ3MTQ0fA&ixlib=rb-4.1.0",
"og_scrape_url": "https://unsplash.com/photos/1_2d3MRbI9c"
}
],
@@ -40,9 +40,9 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:37.527336Z",
- "updated_at": "2025-06-01T00:22:37.527336Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:45.47899Z",
+ "updated_at": "2025-06-15T00:25:45.47899Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
@@ -57,5 +57,5 @@
"pin_expires": null,
"restricted_visibility": []
},
- "duration": "268.95ms"
+ "duration": "4355.84ms"
}
\ 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 201fd256b81..9427cbee840 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": "e954ff62-86dc-40af-a294-a83b849a69c9",
+ "id": "cc20202e-3ec1-4407-b35f-70981d74d2c4",
"text": "https://youtube.com/watch?v=xOX7MsrbaPY",
"html": "https://youtube.com/watch?v=xOX7MsrbaPY
\n",
"type": "regular",
@@ -12,15 +12,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "birthland": "Tatooine",
"team": "test",
"type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"attachments": [
{
@@ -41,9 +41,9 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:36.878053Z",
- "updated_at": "2025-06-01T00:22:36.878053Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:40.546889Z",
+ "updated_at": "2025-06-15T00:25:40.546889Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
@@ -58,5 +58,5 @@
"pin_expires": null,
"restricted_visibility": []
},
- "duration": "814.45ms"
+ "duration": "560.98ms"
}
\ 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 47f05f3d9f9..8b4a4764ef4 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:42dcc850-6b61-48bd-9316-70665fb826df",
- "channel_id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "channel_id": "ec2807ff-5c60-41bc-a816-49578260471a",
"channel_type": "messaging",
"user": {
"id": "luke_skywalker",
@@ -11,10 +11,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"shadow_banned": false,
"privacy_settings": {
@@ -29,36 +29,57 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
- "created_at": "2025-05-30T21:14:29.53116Z",
+ "id": "804506291419e705e68fdc61b5f71297f30881364077b8c3f374f6938bef6cb4f9801da58770c8070108cf30ac06c060630037019523dc91d750de2293f8df7be86c8c8be6be95666859bdc62fbed488",
+ "created_at": "2025-06-14T16:19:23.502954Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-05-30T18:32:15.279068Z",
+ "id": "6397e24030aa17262a850157abf97612eff0243e873b7d1f79c996662ecb1682",
+ "created_at": "2025-06-12T13:18:40.260038Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
- "created_at": "2025-05-30T09:18:11.558444Z",
+ "id": "80215dc8442845c41f30d28c05db0a8f697f727303be0fb49ca7743af6a211d3",
+ "created_at": "2025-06-04T02:43:01.595306Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-06-03T14:54:34.358128Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8009e0a929c11c0297edc1a8c994a68fb7064cebede77d29bbd663c93390b1d22394adbdeb06a68f9b07720daa0e300b1dd951f90bb49c81275a0eaed20f24c954ceba42b8c76010b9ceaa70f19c5a24",
+ "created_at": "2025-06-03T12:06:41.61606Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9a6f27ad051302224dfed7419a81610495663634d40bb7233188fdd2f81163a2",
- "created_at": "2025-05-30T07:05:24.952084Z",
+ "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
+ "created_at": "2025-05-30T21:14:29.53116Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "43714000e6175c535c1b0846791b43ec4930cffde3bb741e67c5d2d8e4f97954",
- "created_at": "2025-05-30T06:41:50.204282Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-05-30T18:32:15.279068Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
+ "created_at": "2025-05-30T09:18:11.558444Z",
"user_id": "luke_skywalker"
},
{
@@ -96,13 +117,6 @@
"created_at": "2025-05-27T12:28:46.620215Z",
"user_id": "luke_skywalker"
},
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "805b59caefec362b118f5634c530af810fd1196c978d3c71bc8749bd227d5222a5f192e44661f164d2f4b7c7a6c8b819ebd19dfcbc80e97cff754f877d4e0f38f406367716039f4e09c177dd9872d6d1",
- "created_at": "2025-05-23T10:54:00.886132Z",
- "user_id": "luke_skywalker"
- },
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
@@ -172,34 +186,6 @@
"id": "8083e22c635fefc3b8ca928aee3179830f2d3553fecd6a11ccd6dc6cb91672f035c4e76f4cb3f0332ce1e18fe4ecfa091ab247f353a35d3f903f7151bd00cff341852e862719edbd150f6a1778256e3d",
"created_at": "2025-05-07T08:38:58.92846Z",
"user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80e8764b1eeb995ed22831a0f1c22b76a0faabd433516129fcfd3b783a4ceeacd07b233a491124f3f82e7e2e3c95b1992a0c6ceb131778e39ecea0f5606f99a10f29a10c5febead6f2a7774d460f672c",
- "created_at": "2025-05-03T07:54:57.657433Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "806601fb89a59a17cda263c987c0ec225790b7cfc38e54f750dd44d1c9fcb24dda89bfb49cbfbdab25091e3df96fb5335bf94e51a53f42fc59cd29eeeb466db906be5d5910e36cdbcf80bb0a1c81704f",
- "created_at": "2025-05-02T18:26:44.057936Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "466ec8f052e4e43f6f429a55907217c289618204ebaf4f7ca8fe52c303b01d05",
- "created_at": "2025-05-02T11:18:09.863541Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "82093d2a6d4b1a5ce7516398b350e46816da77c05f22d88c27c0d1e0cf3dd22d",
- "created_at": "2025-05-02T10:58:41.787223Z",
- "user_id": "luke_skywalker"
}
],
"invisible": false,
@@ -208,5 +194,5 @@
"pando": "{\"speciality\":\"ios engineer\"}",
"birthland": "Tatooine"
},
- "created_at": "2025-06-01T00:22:31.554747791Z"
+ "created_at": "2025-06-15T00:25:34.999482144Z"
}
\ 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 2c4afceefe1..a738783f135 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_events_channel.json
@@ -1,17 +1,17 @@
{
"type": "channel.updated",
- "created_at": "2025-06-01T00:22:34.156669443Z",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
+ "created_at": "2025-06-15T00:25:38.161298226Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
"channel_member_count": 4,
"channel_type": "messaging",
- "channel_id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "channel_id": "ec2807ff-5c60-41bc-a816-49578260471a",
"channel": {
- "id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "id": "ec2807ff-5c60-41bc-a816-49578260471a",
"type": "messaging",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "last_message_at": "2025-06-01T00:22:32.65955Z",
- "created_at": "2025-06-01T00:22:30.030603Z",
- "updated_at": "2025-06-01T00:22:30.030603Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "last_message_at": "2025-06-15T00:25:36.86243Z",
+ "created_at": "2025-06-15T00:25:33.285826Z",
+ "updated_at": "2025-06-15T00:25:33.285826Z",
"created_by": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -20,10 +20,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"shadow_banned": false,
"privacy_settings": {
@@ -38,36 +38,57 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
- "created_at": "2025-05-30T21:14:29.53116Z",
+ "id": "804506291419e705e68fdc61b5f71297f30881364077b8c3f374f6938bef6cb4f9801da58770c8070108cf30ac06c060630037019523dc91d750de2293f8df7be86c8c8be6be95666859bdc62fbed488",
+ "created_at": "2025-06-14T16:19:23.502954Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-05-30T18:32:15.279068Z",
+ "id": "6397e24030aa17262a850157abf97612eff0243e873b7d1f79c996662ecb1682",
+ "created_at": "2025-06-12T13:18:40.260038Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
- "created_at": "2025-05-30T09:18:11.558444Z",
+ "id": "80215dc8442845c41f30d28c05db0a8f697f727303be0fb49ca7743af6a211d3",
+ "created_at": "2025-06-04T02:43:01.595306Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-06-03T14:54:34.358128Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8009e0a929c11c0297edc1a8c994a68fb7064cebede77d29bbd663c93390b1d22394adbdeb06a68f9b07720daa0e300b1dd951f90bb49c81275a0eaed20f24c954ceba42b8c76010b9ceaa70f19c5a24",
+ "created_at": "2025-06-03T12:06:41.61606Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9a6f27ad051302224dfed7419a81610495663634d40bb7233188fdd2f81163a2",
- "created_at": "2025-05-30T07:05:24.952084Z",
+ "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
+ "created_at": "2025-05-30T21:14:29.53116Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "43714000e6175c535c1b0846791b43ec4930cffde3bb741e67c5d2d8e4f97954",
- "created_at": "2025-05-30T06:41:50.204282Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-05-30T18:32:15.279068Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
+ "created_at": "2025-05-30T09:18:11.558444Z",
"user_id": "luke_skywalker"
},
{
@@ -105,13 +126,6 @@
"created_at": "2025-05-27T12:28:46.620215Z",
"user_id": "luke_skywalker"
},
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "805b59caefec362b118f5634c530af810fd1196c978d3c71bc8749bd227d5222a5f192e44661f164d2f4b7c7a6c8b819ebd19dfcbc80e97cff754f877d4e0f38f406367716039f4e09c177dd9872d6d1",
- "created_at": "2025-05-23T10:54:00.886132Z",
- "user_id": "luke_skywalker"
- },
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
@@ -181,34 +195,6 @@
"id": "8083e22c635fefc3b8ca928aee3179830f2d3553fecd6a11ccd6dc6cb91672f035c4e76f4cb3f0332ce1e18fe4ecfa091ab247f353a35d3f903f7151bd00cff341852e862719edbd150f6a1778256e3d",
"created_at": "2025-05-07T08:38:58.92846Z",
"user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80e8764b1eeb995ed22831a0f1c22b76a0faabd433516129fcfd3b783a4ceeacd07b233a491124f3f82e7e2e3c95b1992a0c6ceb131778e39ecea0f5606f99a10f29a10c5febead6f2a7774d460f672c",
- "created_at": "2025-05-03T07:54:57.657433Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "806601fb89a59a17cda263c987c0ec225790b7cfc38e54f750dd44d1c9fcb24dda89bfb49cbfbdab25091e3df96fb5335bf94e51a53f42fc59cd29eeeb466db906be5d5910e36cdbcf80bb0a1c81704f",
- "created_at": "2025-05-02T18:26:44.057936Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "466ec8f052e4e43f6f429a55907217c289618204ebaf4f7ca8fe52c303b01d05",
- "created_at": "2025-05-02T11:18:09.863541Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "82093d2a6d4b1a5ce7516398b350e46816da77c05f22d88c27c0d1e0cf3dd22d",
- "created_at": "2025-05-02T10:58:41.787223Z",
- "user_id": "luke_skywalker"
}
],
"invisible": false,
@@ -334,8 +320,8 @@
"birthland": "Serenno"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -355,7 +341,7 @@
"updated_at": "2025-04-24T15:07:52.050477Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T19:56:11.382352Z",
+ "last_active": "2025-06-10T06:55:59.491807Z",
"blocked_user_ids": [],
"shadow_banned": false,
"privacy_settings": {
@@ -370,15 +356,15 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "5c58d8fb9271dd91c704682681a429bb9d67d00dedf236bc46ca42e8aaff704c",
- "created_at": "2025-05-28T12:13:43.149735Z",
+ "id": "801f108a7521af9233f66e763339a5faff36e534ef24bce87154544ae1b4f7add0b077ce763b0449ba774b8e59e0362fee7fb1e5416081c835060aa013ea22bd04e7fd775cfbbe7b4fbff5be9dfde484",
+ "created_at": "2025-06-07T01:46:40.213206Z",
"user_id": "han_solo"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "ba86b2990f31f4ff55f2d3fbe6f7b5cf8a8afd141225a45b2cd7a33990535881",
- "created_at": "2025-05-28T12:11:41.132274Z",
+ "id": "80ec0dff63e1b885f1607e2989e0ca6c83885a0b8eafa968e0ff1f4458887c961f0ac756c2ee4b8c557f363d337d6029888bd0d3fe9cc4912146be6e2d1d6848d8a0f135de7de73f3ce1fa4ee9704795",
+ "created_at": "2025-06-03T11:44:12.612508Z",
"user_id": "han_solo"
},
{
@@ -479,13 +465,6 @@
"created_at": "2025-02-04T13:16:37.534389Z",
"user_id": "han_solo"
},
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80562365fd4558a3f35d7242fe7f75f55d343e3c08793327b95c2a66bed62dd580e03ea87ee2670e045188cc002ce0c474cc2be00aac2e41417566303513d01c2f9b08fbe20a875423706773c938a276",
- "created_at": "2025-01-30T10:34:15.063447Z",
- "user_id": "han_solo"
- },
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
@@ -520,35 +499,14 @@
"id": "c0fd9f37a0a660d02f19b26df4c29c9519115010b6fcc6e3c8c98bd9c40f0782",
"created_at": "2025-01-16T02:08:16.720431Z",
"user_id": "han_solo"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "66af1dcb8df0727454c1a7544baa2cd41f5af0ef4c0400527c6c20cac5c8794d",
- "created_at": "2025-01-12T12:06:15.167679Z",
- "user_id": "han_solo"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "19caa4c1fd038ebfd2ade42c858bca2e69b7fbbb34514ed23d93f21f3ef134fe",
- "created_at": "2025-01-11T11:27:14.812828Z",
- "user_id": "han_solo"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "bc36105667f3570260bcbd736f73dbd3fb3342c35deb95dd9eaa249d96fe8d8a",
- "created_at": "2025-01-08T15:54:32.363611Z",
- "user_id": "han_solo"
}
],
"invisible": false,
"birthland": "Corellia"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "member",
@@ -565,10 +523,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"shadow_banned": false,
"privacy_settings": {
@@ -583,36 +541,57 @@
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
- "created_at": "2025-05-30T21:14:29.53116Z",
+ "id": "804506291419e705e68fdc61b5f71297f30881364077b8c3f374f6938bef6cb4f9801da58770c8070108cf30ac06c060630037019523dc91d750de2293f8df7be86c8c8be6be95666859bdc62fbed488",
+ "created_at": "2025-06-14T16:19:23.502954Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-05-30T18:32:15.279068Z",
+ "id": "6397e24030aa17262a850157abf97612eff0243e873b7d1f79c996662ecb1682",
+ "created_at": "2025-06-12T13:18:40.260038Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
- "created_at": "2025-05-30T09:18:11.558444Z",
+ "id": "80215dc8442845c41f30d28c05db0a8f697f727303be0fb49ca7743af6a211d3",
+ "created_at": "2025-06-04T02:43:01.595306Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-06-03T14:54:34.358128Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8009e0a929c11c0297edc1a8c994a68fb7064cebede77d29bbd663c93390b1d22394adbdeb06a68f9b07720daa0e300b1dd951f90bb49c81275a0eaed20f24c954ceba42b8c76010b9ceaa70f19c5a24",
+ "created_at": "2025-06-03T12:06:41.61606Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9a6f27ad051302224dfed7419a81610495663634d40bb7233188fdd2f81163a2",
- "created_at": "2025-05-30T07:05:24.952084Z",
+ "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
+ "created_at": "2025-05-30T21:14:29.53116Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "43714000e6175c535c1b0846791b43ec4930cffde3bb741e67c5d2d8e4f97954",
- "created_at": "2025-05-30T06:41:50.204282Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-05-30T18:32:15.279068Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
+ "created_at": "2025-05-30T09:18:11.558444Z",
"user_id": "luke_skywalker"
},
{
@@ -650,13 +629,6 @@
"created_at": "2025-05-27T12:28:46.620215Z",
"user_id": "luke_skywalker"
},
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "805b59caefec362b118f5634c530af810fd1196c978d3c71bc8749bd227d5222a5f192e44661f164d2f4b7c7a6c8b819ebd19dfcbc80e97cff754f877d4e0f38f406367716039f4e09c177dd9872d6d1",
- "created_at": "2025-05-23T10:54:00.886132Z",
- "user_id": "luke_skywalker"
- },
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
@@ -726,45 +698,17 @@
"id": "8083e22c635fefc3b8ca928aee3179830f2d3553fecd6a11ccd6dc6cb91672f035c4e76f4cb3f0332ce1e18fe4ecfa091ab247f353a35d3f903f7151bd00cff341852e862719edbd150f6a1778256e3d",
"created_at": "2025-05-07T08:38:58.92846Z",
"user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80e8764b1eeb995ed22831a0f1c22b76a0faabd433516129fcfd3b783a4ceeacd07b233a491124f3f82e7e2e3c95b1992a0c6ceb131778e39ecea0f5606f99a10f29a10c5febead6f2a7774d460f672c",
- "created_at": "2025-05-03T07:54:57.657433Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "806601fb89a59a17cda263c987c0ec225790b7cfc38e54f750dd44d1c9fcb24dda89bfb49cbfbdab25091e3df96fb5335bf94e51a53f42fc59cd29eeeb466db906be5d5910e36cdbcf80bb0a1c81704f",
- "created_at": "2025-05-02T18:26:44.057936Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "466ec8f052e4e43f6f429a55907217c289618204ebaf4f7ca8fe52c303b01d05",
- "created_at": "2025-05-02T11:18:09.863541Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "82093d2a6d4b1a5ce7516398b350e46816da77c05f22d88c27c0d1e0cf3dd22d",
- "created_at": "2025-05-02T10:58:41.787223Z",
- "user_id": "luke_skywalker"
}
],
"invisible": false,
- "team": "test",
- "type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test",
+ "type": "team"
},
"status": "member",
- "created_at": "2025-06-01T00:22:30.049441Z",
- "updated_at": "2025-06-01T00:22:30.049441Z",
+ "created_at": "2025-06-15T00:25:33.304034Z",
+ "updated_at": "2025-06-15T00:25:33.304034Z",
"banned": false,
"shadow_banned": false,
"role": "owner",
@@ -784,7 +728,7 @@
"updated_at": "2025-03-28T15:21:20.061525Z",
"banned": false,
"online": false,
- "last_active": "2025-05-30T14:14:34.87384Z",
+ "last_active": "2025-06-14T17:34:03.224367Z",
"blocked_user_ids": [],
"shadow_banned": false,
"privacy_settings": {
@@ -796,6 +740,27 @@
}
},
"devices": [
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "80562365fd4558a3f35d7242fe7f75f55d343e3c08793327b95c2a66bed62dd580e03ea87ee2670e045188cc002ce0c474cc2be00aac2e41417566303513d01c2f9b08fbe20a875423706773c938a276",
+ "created_at": "2025-06-11T13:32:58.048525Z",
+ "user_id": "leia_organa"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "15266a2cdf77614a3d9c88c714022dd0e946e537a8a61c33e6813e26480ed518",
+ "created_at": "2025-06-11T13:31:05.651333Z",
+ "user_id": "leia_organa"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "14216b28af8556468ae8dbf08a5ea602f9deb9c2eb115d9b6aacf7f7bde74ade",
+ "created_at": "2025-06-04T10:55:53.753151Z",
+ "user_id": "leia_organa"
+ },
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
@@ -949,16 +914,10 @@
"id": "7806d403ee3740e98fc519a123a9450dbd9d90b13229bff5dbe7a1d5825d2fdf",
"created_at": "2024-12-27T17:32:18.338986Z",
"user_id": "leia_organa"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80bfc09ef7858948e403a0db71cb1a3f3e6f284fece7a4c6c751a02f25c66bf95d89362ca55c988102aeee876e91c150779c4dcdc206e11124a2df77d3c843ed75dbb42d7bb0b0724007b0d1d6011a09",
- "created_at": "2024-12-26T14:26:34.216526Z",
- "user_id": "leia_organa"
}
],
"invisible": false,
+ "birthland": "Polis Massa",
"private_settings": {
"readReceipts": {
"enabled": false
@@ -966,12 +925,11 @@
"typingIndicators": {
"enabled": false
}
- },
- "birthland": "Polis Massa"
+ }
},
"status": "member",
- "created_at": "2025-06-01T00:22:34.135368Z",
- "updated_at": "2025-06-01T00:22:34.135368Z",
+ "created_at": "2025-06-15T00:25:38.137522Z",
+ "updated_at": "2025-06-15T00:25:38.137522Z",
"banned": false,
"shadow_banned": false,
"role": "admin",
@@ -1039,15 +997,11 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
- "team": "test",
- "type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine",
"privacy_settings": {
"read_receipts": {
"enabled": false
@@ -1055,6 +1009,10 @@
"typing_indicators": {
"enabled": false
}
- }
+ },
+ "team": "test",
+ "type": "team",
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine"
}
}
\ 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 f953902fc7a..097b0aee5b8 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:42dcc850-6b61-48bd-9316-70665fb826df",
- "channel_id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "channel_id": "ec2807ff-5c60-41bc-a816-49578260471a",
"channel_type": "messaging",
"member": {
"user_id": "leia_organa",
@@ -11,8 +11,8 @@
"teams_role": null,
"created_at": "2024-04-04T09:42:00.68335Z",
"updated_at": "2025-03-28T15:21:20.061525Z",
- "last_active": "2025-05-30T14:14:34.87384Z",
- "last_engaged_at": "2025-05-30T12:11:28.461421Z",
+ "last_active": "2025-06-14T17:34:03.224367Z",
+ "last_engaged_at": "2025-06-14T17:34:05.446982Z",
"banned": false,
"online": false,
"language": "zh",
@@ -29,8 +29,8 @@
"image": "https://vignette.wikia.nocookie.net/starwars/images/f/fc/Leia_Organa_TLJ.png"
},
"status": "member",
- "created_at": "2025-06-01T00:22:34.135368Z",
- "updated_at": "2025-06-01T00:22:34.135368Z",
+ "created_at": "2025-06-15T00:25:38.137522Z",
+ "updated_at": "2025-06-15T00:25:38.137522Z",
"banned": false,
"shadow_banned": false,
"is_global_banned": false,
@@ -46,8 +46,8 @@
"teams_role": null,
"created_at": "2024-04-04T09:42:00.68335Z",
"updated_at": "2025-03-28T15:21:20.061525Z",
- "last_active": "2025-05-30T14:14:34.87384Z",
- "last_engaged_at": "2025-05-30T12:11:28.461421Z",
+ "last_active": "2025-06-14T17:34:03.224367Z",
+ "last_engaged_at": "2025-06-14T17:34:05.446982Z",
"banned": false,
"online": false,
"language": "zh",
@@ -63,6 +63,6 @@
},
"name": "Leia Organa"
},
- "channel_last_message_at": "2025-06-01T00:22:32.65955Z",
- "created_at": "2025-06-01T00:22:34.145463292Z"
+ "channel_last_message_at": "2025-06-15T00:25:36.86243Z",
+ "created_at": "2025-06-15T00:25:38.150649795Z"
}
\ 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 0ad412807b7..9d5093a6f0a 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": "6835991c-0a15-3975-0200-00000000b370",
+ "connection_id": "684a957c-0a15-3975-0200-000000000f8a",
"me": {
"id": "luke_skywalker",
"name": "Luke Skywalker",
@@ -8,10 +8,10 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"privacy_settings": {
"typing_indicators": {
"enabled": true
@@ -28,12 +28,12 @@
"total_unread_count": 0,
"unread_channels": 0,
"unread_threads": 0,
- "team": "test",
"type": "team",
"pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ "birthland": "Tatooine",
+ "team": "test"
},
"cid": "*",
"type": "health.check",
- "created_at": "2025-06-01T00:22:29.014580924Z"
+ "created_at": "2025-06-15T00:25:32.398620669Z"
}
\ 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 b777208442e..d97de06a1b6 100644
--- a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json
+++ b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_message.json
@@ -1,16 +1,16 @@
{
"type": "message.new",
- "created_at": "2025-06-01T00:22:32.706437281Z",
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
+ "created_at": "2025-06-15T00:25:36.923178296Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
"channel_member_count": 3,
"channel_custom": {
"name": "Sync Mock Server"
},
"channel_type": "messaging",
- "channel_id": "42dcc850-6b61-48bd-9316-70665fb826df",
- "message_id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "channel_id": "ec2807ff-5c60-41bc-a816-49578260471a",
+ "message_id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"message": {
- "id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -22,46 +22,67 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
"shadow_banned": false,
"devices": [
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
- "created_at": "2025-05-30T21:14:29.53116Z",
+ "id": "804506291419e705e68fdc61b5f71297f30881364077b8c3f374f6938bef6cb4f9801da58770c8070108cf30ac06c060630037019523dc91d750de2293f8df7be86c8c8be6be95666859bdc62fbed488",
+ "created_at": "2025-06-14T16:19:23.502954Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
- "created_at": "2025-05-30T18:32:15.279068Z",
+ "id": "6397e24030aa17262a850157abf97612eff0243e873b7d1f79c996662ecb1682",
+ "created_at": "2025-06-12T13:18:40.260038Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
- "created_at": "2025-05-30T09:18:11.558444Z",
+ "id": "80215dc8442845c41f30d28c05db0a8f697f727303be0fb49ca7743af6a211d3",
+ "created_at": "2025-06-04T02:43:01.595306Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8078596852ae3ecc7bb8d20eb1bde24d596115270e266be1e7164009c28c841c89120222e5594b8929bafb7b55ec47e5b1db2e1274b6e0c9a8f24ba1087832d4dc8acefc857be26848f2b074fdaa42fe",
+ "created_at": "2025-06-03T14:54:34.358128Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "8009e0a929c11c0297edc1a8c994a68fb7064cebede77d29bbd663c93390b1d22394adbdeb06a68f9b07720daa0e300b1dd951f90bb49c81275a0eaed20f24c954ceba42b8c76010b9ceaa70f19c5a24",
+ "created_at": "2025-06-03T12:06:41.61606Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "9a6f27ad051302224dfed7419a81610495663634d40bb7233188fdd2f81163a2",
- "created_at": "2025-05-30T07:05:24.952084Z",
+ "id": "80b9b312b4b635f1f0e4005ba0acc30d8f022a59e5006edb24f4eb7546930dbe0de2ce85800eabc1ec1a004d0a87fc7520b4f9039718f4f8437c466eb33ab9b392a77607ce982e31e443685e2b6b90b1",
+ "created_at": "2025-05-30T21:14:29.53116Z",
"user_id": "luke_skywalker"
},
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
- "id": "43714000e6175c535c1b0846791b43ec4930cffde3bb741e67c5d2d8e4f97954",
- "created_at": "2025-05-30T06:41:50.204282Z",
+ "id": "809065933d06e42d27179dfe7f24ef7c34e74c0898c184cc1de0abd791f0f9d2a1342ad140129c3a26ba7a4f86b3534358475d9964024ff373bc2c705bc2d999cbbe0402af60077fb7af6c1f73295bd7",
+ "created_at": "2025-05-30T18:32:15.279068Z",
+ "user_id": "luke_skywalker"
+ },
+ {
+ "push_provider": "apn",
+ "push_provider_name": "APN-Configuration",
+ "id": "804d29de95a5a1fee45ffa38a6700d75807f844dbdcdffb6256377697ca2095c1f4a43a1823b7858af4a3dd47d96fda739110aa11072b1c0bec52446806ce65b9408b68b81343a8594bf21d3c18af6c0",
+ "created_at": "2025-05-30T09:18:11.558444Z",
"user_id": "luke_skywalker"
},
{
@@ -99,13 +120,6 @@
"created_at": "2025-05-27T12:28:46.620215Z",
"user_id": "luke_skywalker"
},
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "805b59caefec362b118f5634c530af810fd1196c978d3c71bc8749bd227d5222a5f192e44661f164d2f4b7c7a6c8b819ebd19dfcbc80e97cff754f877d4e0f38f406367716039f4e09c177dd9872d6d1",
- "created_at": "2025-05-23T10:54:00.886132Z",
- "user_id": "luke_skywalker"
- },
{
"push_provider": "apn",
"push_provider_name": "APN-Configuration",
@@ -175,34 +189,6 @@
"id": "8083e22c635fefc3b8ca928aee3179830f2d3553fecd6a11ccd6dc6cb91672f035c4e76f4cb3f0332ce1e18fe4ecfa091ab247f353a35d3f903f7151bd00cff341852e862719edbd150f6a1778256e3d",
"created_at": "2025-05-07T08:38:58.92846Z",
"user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "80e8764b1eeb995ed22831a0f1c22b76a0faabd433516129fcfd3b783a4ceeacd07b233a491124f3f82e7e2e3c95b1992a0c6ceb131778e39ecea0f5606f99a10f29a10c5febead6f2a7774d460f672c",
- "created_at": "2025-05-03T07:54:57.657433Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "806601fb89a59a17cda263c987c0ec225790b7cfc38e54f750dd44d1c9fcb24dda89bfb49cbfbdab25091e3df96fb5335bf94e51a53f42fc59cd29eeeb466db906be5d5910e36cdbcf80bb0a1c81704f",
- "created_at": "2025-05-02T18:26:44.057936Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "466ec8f052e4e43f6f429a55907217c289618204ebaf4f7ca8fe52c303b01d05",
- "created_at": "2025-05-02T11:18:09.863541Z",
- "user_id": "luke_skywalker"
- },
- {
- "push_provider": "apn",
- "push_provider_name": "APN-Configuration",
- "id": "82093d2a6d4b1a5ce7516398b350e46816da77c05f22d88c27c0d1e0cf3dd22d",
- "created_at": "2025-05-02T10:58:41.787223Z",
- "user_id": "luke_skywalker"
}
],
"invisible": false,
@@ -218,9 +204,9 @@
"reaction_scores": {},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:32.65955Z",
- "updated_at": "2025-06-01T00:22:32.65955Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:36.86243Z",
+ "updated_at": "2025-06-15T00:25:36.86243Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
@@ -243,11 +229,15 @@
"role": "admin",
"teams": [],
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
"banned": false,
"online": true,
- "last_active": "2025-06-01T00:22:28.9988294Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
"blocked_user_ids": [],
+ "team": "test",
+ "type": "team",
+ "pando": "{\"speciality\":\"ios engineer\"}",
+ "birthland": "Tatooine",
"privacy_settings": {
"read_receipts": {
"enabled": false
@@ -255,11 +245,7 @@
"typing_indicators": {
"enabled": false
}
- },
- "team": "test",
- "type": "team",
- "pando": "{\"speciality\":\"ios engineer\"}",
- "birthland": "Tatooine"
+ }
},
"watcher_count": 1,
"unread_count": 0,
diff --git a/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json b/TestTools/StreamChatTestMockServer/Fixtures/JSONs/ws_reaction.json
index 9ca7d6869f7..92d03e644c3 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:42dcc850-6b61-48bd-9316-70665fb826df",
- "channel_id": "42dcc850-6b61-48bd-9316-70665fb826df",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "channel_id": "ec2807ff-5c60-41bc-a816-49578260471a",
"channel_type": "messaging",
"message": {
- "id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"text": "Test",
"html": "Test
\n",
"type": "regular",
@@ -13,9 +13,9 @@
"role": "admin",
"teams_role": null,
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
- "last_active": "2025-06-01T00:22:28.9988294Z",
- "last_engaged_at": "2025-05-30T00:04:51.894863Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
+ "last_engaged_at": "2025-06-14T02:15:22.646364Z",
"banned": false,
"online": true,
"language": "en",
@@ -30,30 +30,30 @@
"attachments": [],
"latest_reactions": [
{
- "message_id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "message_id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
"role": "admin",
"teams_role": null,
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
- "last_active": "2025-06-01T00:22:28.9988294Z",
- "last_engaged_at": "2025-05-30T00:04:51.894863Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
+ "last_engaged_at": "2025-06-14T02:15:22.646364Z",
"banned": false,
"online": true,
"language": "en",
- "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"
+ "type": "team",
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg",
+ "pando": "{\"speciality\":\"ios engineer\"}"
},
"type": "like",
"score": 1,
- "created_at": "2025-06-01T00:22:33.420026Z",
- "updated_at": "2025-06-01T00:22:33.420026Z"
+ "created_at": "2025-06-15T00:25:37.516538Z",
+ "updated_at": "2025-06-15T00:25:37.516538Z"
}
],
"own_reactions": [],
@@ -67,15 +67,15 @@
"like": {
"count": 1,
"sum_scores": 1,
- "first_reaction_at": "2025-06-01T00:22:33.420026Z",
- "last_reaction_at": "2025-06-01T00:22:33.420026Z"
+ "first_reaction_at": "2025-06-15T00:25:37.516538Z",
+ "last_reaction_at": "2025-06-15T00:25:37.516538Z"
}
},
"reply_count": 0,
"deleted_reply_count": 0,
- "cid": "messaging:42dcc850-6b61-48bd-9316-70665fb826df",
- "created_at": "2025-06-01T00:22:32.65955Z",
- "updated_at": "2025-06-01T00:22:33.428305Z",
+ "cid": "messaging:ec2807ff-5c60-41bc-a816-49578260471a",
+ "created_at": "2025-06-15T00:25:36.86243Z",
+ "updated_at": "2025-06-15T00:25:37.529233Z",
"shadowed": false,
"mentioned_users": [],
"i18n": {
@@ -90,49 +90,49 @@
"pin_expires": null
},
"reaction": {
- "message_id": "33b1b88e-ecdd-4d3d-aa89-69ce9413a093",
+ "message_id": "b9841bf2-9a49-4adf-ae93-5e07d37d7c22",
"user_id": "luke_skywalker",
"user": {
"id": "luke_skywalker",
"role": "admin",
"teams_role": null,
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
- "last_active": "2025-06-01T00:22:28.9988294Z",
- "last_engaged_at": "2025-05-30T00:04:51.894863Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
+ "last_engaged_at": "2025-06-14T02:15:22.646364Z",
"banned": false,
"online": true,
"language": "en",
+ "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\"}",
- "birthland": "Tatooine"
+ "image": "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg"
},
"type": "like",
"score": 1,
- "created_at": "2025-06-01T00:22:33.420026Z",
- "updated_at": "2025-06-01T00:22:33.420026Z"
+ "created_at": "2025-06-15T00:25:37.516538Z",
+ "updated_at": "2025-06-15T00:25:37.516538Z"
},
"user": {
"id": "luke_skywalker",
"role": "admin",
"teams_role": null,
"created_at": "2024-04-04T09:26:11.805899Z",
- "updated_at": "2025-05-22T11:47:57.82369Z",
- "last_active": "2025-06-01T00:22:28.9988294Z",
- "last_engaged_at": "2025-05-30T00:04:51.894863Z",
+ "updated_at": "2025-06-06T08:22:36.844279Z",
+ "last_active": "2025-06-15T00:25:32.362084978Z",
+ "last_engaged_at": "2025-06-14T02:15:22.646364Z",
"banned": false,
"online": true,
"language": "en",
+ "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",
- "team": "test"
+ "birthland": "Tatooine"
},
- "channel_last_message_at": "2025-06-01T00:22:32.65955Z",
- "created_at": "2025-06-01T00:22:33.437268437Z"
+ "channel_last_message_at": "2025-06-15T00:25:36.86243Z",
+ "created_at": "2025-06-15T00:25:37.540065556Z"
}
\ No newline at end of file
From 47c5024c851bb18e088b081f8211260bc94c4d9d Mon Sep 17 00:00:00 2001
From: Alexey Alter-Pesotskiy
Date: Mon, 16 Jun 2025 15:34:05 +0100
Subject: [PATCH 09/11] [CI] Automate the release step of merging the main
branch to develop (#3694)
---
.github/workflows/merge-main-to-develop.yml | 32 +++++++++++++++++++++
.github/workflows/release-publish.yml | 8 ------
Gemfile.lock | 4 +--
fastlane/Fastfile | 4 ++-
fastlane/Pluginfile | 2 +-
5 files changed, 38 insertions(+), 12 deletions(-)
create mode 100644 .github/workflows/merge-main-to-develop.yml
diff --git a/.github/workflows/merge-main-to-develop.yml b/.github/workflows/merge-main-to-develop.yml
new file mode 100644
index 00000000000..43b7da2c50b
--- /dev/null
+++ b/.github/workflows/merge-main-to-develop.yml
@@ -0,0 +1,32 @@
+name: "Merge main to develop"
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ merge:
+ name: Merge
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4.1.1
+ with:
+ token: ${{ secrets.ADMIN_API_TOKEN }}
+ fetch-depth: 0
+
+ - uses: ./.github/actions/ruby-cache
+
+ - run: bundle exec fastlane merge_main
+ env:
+ GITHUB_TOKEN: ${{ secrets.ADMIN_API_TOKEN }}
+
+ - uses: 8398a7/action-slack@v3
+ if: failure()
+ with:
+ status: ${{ job.status }}
+ text: "ā ļø , the merge of `main` to `develop` failed on CI. Consider using this command locally: `bundle exec fastlane merge_main`"
+ fields: repo,commit,author,workflow
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml
index e664c5a7e16..4b8db0bf5e0 100644
--- a/.github/workflows/release-publish.yml
+++ b/.github/workflows/release-publish.yml
@@ -26,11 +26,3 @@ jobs:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
run: bundle exec fastlane publish_release --verbose
-
- - uses: 8398a7/action-slack@v3
- with:
- status: ${{ job.status }}
- text: "š The new release has been shipped! š¢\n\nā ļø , don't forget to merge `main` back to `develop` from `localhost` using the command: `bundle exec fastlane merge_main`"
- fields: repo
- env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/Gemfile.lock b/Gemfile.lock
index eaf9ac3c028..bfe227be55a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -208,7 +208,7 @@ GEM
fastlane
pry
fastlane-plugin-sonarcloud_metric_kit (0.2.1)
- fastlane-plugin-stream_actions (0.3.83)
+ fastlane-plugin-stream_actions (0.3.84)
xctest_list (= 1.2.1)
fastlane-plugin-versioning (0.7.1)
fastlane-sirp (1.0.0)
@@ -440,7 +440,7 @@ DEPENDENCIES
fastlane-plugin-create_xcframework
fastlane-plugin-lizard
fastlane-plugin-sonarcloud_metric_kit
- fastlane-plugin-stream_actions (= 0.3.83)
+ fastlane-plugin-stream_actions (= 0.3.84)
fastlane-plugin-versioning
faye-websocket
json
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 478629e9a8f..f6dcb013b06 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -25,7 +25,7 @@ before_all do |lane|
if is_ci
setup_ci
setup_git_config
- select_xcode(version: xcode_version) unless [:sonar_upload, :allure_launch, :allure_upload, :pod_lint, :sync_mock_server, :copyright].include?(lane)
+ select_xcode(version: xcode_version) unless [:sonar_upload, :allure_launch, :allure_upload, :pod_lint, :sync_mock_server, :copyright, :merge_main].include?(lane)
end
end
@@ -133,6 +133,8 @@ lane :publish_release do |options|
)
update_spm(version: release_version)
+
+ sh('gh workflow run merge-main-to-develop.yml --ref main')
end
lane :get_sdk_version_from_environment do
diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile
index 9f04f172643..f6b1d8414f6 100644
--- a/fastlane/Pluginfile
+++ b/fastlane/Pluginfile
@@ -5,4 +5,4 @@
gem 'fastlane-plugin-versioning'
gem 'fastlane-plugin-create_xcframework'
gem 'fastlane-plugin-sonarcloud_metric_kit'
-gem 'fastlane-plugin-stream_actions', '0.3.83'
+gem 'fastlane-plugin-stream_actions', '0.3.84'
From b4ab66658c22785a0f6c28d0d736825bb170a273 Mon Sep 17 00:00:00 2001
From: Alexey Alter-Pesotskiy
Date: Mon, 16 Jun 2025 17:35:08 +0100
Subject: [PATCH 10/11] [CI] Update danger usage (#3697)
---
.github/workflows/smoke-checks.yml | 2 +-
fastlane/Fastfile | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index fb04a5d08b9..b07c40e14a9 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -56,7 +56,7 @@ jobs:
fetch-depth: 100
- uses: ./.github/actions/bootstrap
- name: Run Danger
- run: bundle exec danger
+ run: bundle exec fastlane lint_pr
- name: Run Fastlane Linting
run: bundle exec fastlane rubocop
- name: Run SwiftFormat Linting
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index f6dcb013b06..9538f39be02 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -793,6 +793,11 @@ lane :rubocop do
sh('bundle exec rubocop')
end
+desc 'Run PR linting'
+lane :lint_pr do
+ danger(dangerfile: 'Dangerfile') if is_ci
+end
+
lane :install_runtime do |options|
install_ios_runtime(version: options[:ios], custom_script: 'Scripts/install_ios_runtime.sh')
end
From 6a3d5c5ec6b55504aa68d4d999f3d6fb0f42e32d Mon Sep 17 00:00:00 2001
From: Stream Bot
Date: Tue, 17 Jun 2025 08:32:26 +0000
Subject: [PATCH 11/11] Bump 4.80.0
---
CHANGELOG.md | 5 +++++
README.md | 4 ++--
Sources/StreamChat/Generated/SystemEnvironment+Version.swift | 2 +-
Sources/StreamChat/Info.plist | 2 +-
Sources/StreamChatUI/Info.plist | 2 +-
StreamChat-XCFramework.podspec | 2 +-
StreamChat.podspec | 2 +-
StreamChatArtifacts.json | 2 +-
StreamChatUI-XCFramework.podspec | 2 +-
StreamChatUI.podspec | 2 +-
10 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1a4243ff0e..c3c8abebf74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
# Upcoming
+### š Changed
+
+# [4.80.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.80.0)
+_June 17, 2025_
+
## StreamChat
### ā
Added
- Add new `Filter.isNil` to make it easier to query by nil values [#3623](https://github.com/GetStream/stream-chat-swift/pull/3623)
diff --git a/README.md b/README.md
index 8d53431db3a..6bdf9706d68 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,8 @@
-
-
+
+
This is the official iOS SDK for [Stream Chat](https://getstream.io/chat/sdk/ios/), a service for building chat and messaging applications. This library includes both a low-level SDK and a set of reusable UI components.
diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
index cbf32e0ad2b..f81118ac163 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.79.1-SNAPSHOT"
+ public static let version: String = "4.80.0"
}
diff --git a/Sources/StreamChat/Info.plist b/Sources/StreamChat/Info.plist
index 1a947ff3784..5681ded7134 100644
--- a/Sources/StreamChat/Info.plist
+++ b/Sources/StreamChat/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.79.1
+ 4.80.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/Sources/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist
index 1a947ff3784..5681ded7134 100644
--- a/Sources/StreamChatUI/Info.plist
+++ b/Sources/StreamChatUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.79.1
+ 4.80.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec
index 1e3d9049cc5..86521e3b526 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.79.1"
+ spec.version = "4.80.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 7d1b6748a83..b496781b456 100644
--- a/StreamChat.podspec
+++ b/StreamChat.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChat"
- spec.version = "4.79.1"
+ spec.version = "4.80.0"
spec.summary = "StreamChat iOS Chat Client"
spec.description = "stream-chat-swift is the official Swift client for Stream Chat, a service for building chat applications."
diff --git a/StreamChatArtifacts.json b/StreamChatArtifacts.json
index e8ef778851a..de534696919 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","4.73.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.73.0/StreamChat-All.zip","4.74.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.74.0/StreamChat-All.zip","4.75.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.75.0/StreamChat-All.zip","4.76.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.76.0/StreamChat-All.zip","4.77.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.77.0/StreamChat-All.zip","4.78.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.78.0/StreamChat-All.zip","4.79.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.0/StreamChat-All.zip","4.79.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.1/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","4.74.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.74.0/StreamChat-All.zip","4.75.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.75.0/StreamChat-All.zip","4.76.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.76.0/StreamChat-All.zip","4.77.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.77.0/StreamChat-All.zip","4.78.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.78.0/StreamChat-All.zip","4.79.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.0/StreamChat-All.zip","4.79.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.79.1/StreamChat-All.zip","4.80.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.80.0/StreamChat-All.zip"}
\ No newline at end of file
diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec
index b67b7ef8a89..bc7a2201df9 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.79.1"
+ spec.version = "4.80.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 d95e8453b35..1aeb45bc939 100644
--- a/StreamChatUI.podspec
+++ b/StreamChatUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChatUI"
- spec.version = "4.79.1"
+ spec.version = "4.80.0"
spec.summary = "StreamChat UI Components"
spec.description = "StreamChatUI SDK offers flexible UI components able to display data provided by StreamChat SDK."