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 @@

- StreamChat - StreamChatUI + StreamChat + StreamChatUI

This is the official iOS SDK for [Stream Chat](https://getstream.io/chat/sdk/ios/), a service for building chat and messaging applications. This library includes both a low-level SDK and a set of reusable UI components. diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift index 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."