diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index cb5b40a5881..97e12da3dbf 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -68,7 +68,8 @@ jobs:
build-old-xcode:
name: Build LLC + UI (Xcode 15)
runs-on: macos-15
- if: ${{ github.event.inputs.record_snapshots != 'true' }}
+ # if: ${{ github.event.inputs.record_snapshots != 'true' }}
+ if: false # disable Xcode 15 builds
env:
XCODE_VERSION: "15.4"
steps:
diff --git a/.swiftformat b/.swiftformat
index 7cd15b4c1cf..808ece6758c 100644
--- a/.swiftformat
+++ b/.swiftformat
@@ -1,6 +1,6 @@
# Stream rules
--header "\nCopyright © {year} Stream.io Inc. All rights reserved.\n"
---swiftversion 5.6
+--swiftversion 6.0
--ifdef no-indent
--disable redundantType
diff --git a/DemoApp/Screens/Create Chat/CreateChatViewController.swift b/DemoApp/Screens/Create Chat/CreateChatViewController.swift
index 023cddd1dae..7c7ef6793a5 100644
--- a/DemoApp/Screens/Create Chat/CreateChatViewController.swift
+++ b/DemoApp/Screens/Create Chat/CreateChatViewController.swift
@@ -25,19 +25,21 @@ class CreateChatViewController: UIViewController {
// Create the Channel on backend
controller.synchronize { [weak self] error in
- if let error = error {
- self?.presentAlert(title: "Error when creating the channel", message: error.localizedDescription)
- return
+ Task { @MainActor in
+ if let error = error {
+ self?.presentAlert(title: "Error when creating the channel", message: error.localizedDescription)
+ return
+ }
+
+ // Send the message
+ createMessage(text)
+
+ // Present the new chat and controller
+ let vc = ChatChannelVC()
+ vc.channelController = controller
+
+ navController.setViewControllers([navController.viewControllers.first!, vc], animated: true)
}
-
- // Send the message
- createMessage(text)
-
- // Present the new chat and controller
- let vc = ChatChannelVC()
- vc.channelController = controller
-
- navController.setViewControllers([navController.viewControllers.first!, vc], animated: true)
}
}
}
@@ -143,9 +145,11 @@ class CreateChatViewController: UIViewController {
])
// Empty initial search to get all users
- searchController.search(term: nil) { error in
+ searchController.search(term: nil) { [weak self] error in
if error != nil {
- self.update(for: .error)
+ Task { @MainActor in
+ self?.update(for: .error)
+ }
}
}
infoLabel.text = "On the platform"
@@ -235,9 +239,11 @@ class CreateChatViewController: UIViewController {
operation?.cancel()
operation = DispatchWorkItem { [weak self] in
- self?.searchController.search(term: sender.text) { error in
+ self?.searchController.search(term: sender.text) { [weak self] error in
if error != nil {
- self?.update(for: .error)
+ Task { @MainActor in
+ self?.update(for: .error)
+ }
}
}
}
diff --git a/DemoApp/Screens/Create Chat/NameGroupViewController.swift b/DemoApp/Screens/Create Chat/NameGroupViewController.swift
index 84f5ac61b7e..798807367a8 100644
--- a/DemoApp/Screens/Create Chat/NameGroupViewController.swift
+++ b/DemoApp/Screens/Create Chat/NameGroupViewController.swift
@@ -131,11 +131,13 @@ class NameGroupViewController: UIViewController {
name: name,
members: Set(selectedUsers.map(\.id))
)
- channelController?.synchronize { error in
- if let error = error {
- self.presentAlert(title: "Error when creating the channel", message: error.localizedDescription)
- } else {
- self.navigationController?.popToRootViewController(animated: true)
+ channelController?.synchronize { [weak self] error in
+ Task { @MainActor in
+ if let error = error {
+ self?.presentAlert(title: "Error when creating the channel", message: error.localizedDescription)
+ } else {
+ self?.navigationController?.popToRootViewController(animated: true)
+ }
}
}
} catch {
diff --git a/DemoApp/Screens/DemoDraftMessageListVC.swift b/DemoApp/Screens/DemoDraftMessageListVC.swift
index 2765be5d8e5..6805dd09c97 100644
--- a/DemoApp/Screens/DemoDraftMessageListVC.swift
+++ b/DemoApp/Screens/DemoDraftMessageListVC.swift
@@ -94,7 +94,9 @@ class DemoDraftMessageListVC: UIViewController, ThemeProvider {
currentUserController.delegate = self
loadingIndicator.startAnimating()
currentUserController.loadDraftMessages { [weak self] _ in
- self?.loadingIndicator.stopAnimating()
+ Task { @MainActor in
+ self?.loadingIndicator.stopAnimating()
+ }
}
}
@@ -105,7 +107,9 @@ class DemoDraftMessageListVC: UIViewController, ThemeProvider {
isPaginatingDrafts = true
currentUserController.loadMoreDraftMessages { [weak self] _ in
- self?.isPaginatingDrafts = false
+ Task { @MainActor in
+ self?.isPaginatingDrafts = false
+ }
}
}
diff --git a/DemoApp/Screens/MembersViewController.swift b/DemoApp/Screens/MembersViewController.swift
index 83c137fabe4..04161f0c44d 100644
--- a/DemoApp/Screens/MembersViewController.swift
+++ b/DemoApp/Screens/MembersViewController.swift
@@ -37,7 +37,9 @@ class MembersViewController: UITableViewController, ChatChannelMemberListControl
private func synchronizeAndUpdateData() {
membersController.synchronize { [weak self] _ in
- self?.updateData()
+ Task { @MainActor in
+ self?.updateData()
+ }
}
}
diff --git a/DemoApp/Shared/StreamChatWrapper.swift b/DemoApp/Shared/StreamChatWrapper.swift
index c113aa538e8..23e6e768fd6 100644
--- a/DemoApp/Shared/StreamChatWrapper.swift
+++ b/DemoApp/Shared/StreamChatWrapper.swift
@@ -8,7 +8,7 @@ import StreamChatUI
import UIKit
import UserNotifications
-final class StreamChatWrapper {
+final class StreamChatWrapper: @unchecked Sendable {
static var shared = StreamChatWrapper(apiKeyString: apiKeyString)
static func replaceSharedInstance(apiKeyString: String) {
@@ -56,7 +56,7 @@ extension StreamChatWrapper {
// MARK: User Authentication
extension StreamChatWrapper {
- func connect(user: DemoUserType, completion: @escaping (Error?) -> Void) {
+ func connect(user: DemoUserType, completion: @escaping @Sendable(Error?) -> Void) {
switch user {
case let .credentials(userCredentials):
connectUser(credentials: userCredentials, completion: completion)
@@ -69,7 +69,7 @@ extension StreamChatWrapper {
}
}
- func connectUser(credentials: UserCredentials?, completion: @escaping (Error?) -> Void) {
+ func connectUser(credentials: UserCredentials?, completion: @escaping @Sendable(Error?) -> Void) {
guard let userCredentials = credentials else {
log.error("User credentials are missing")
return
@@ -187,7 +187,7 @@ extension StreamChatWrapper {
// An object to test the Stream Models transformer.
// By default it is not used. To use it, set it to the `modelsTransformer` property of the `ChatClientConfig`.
-class CustomStreamModelsTransformer: StreamModelsTransformer {
+final class CustomStreamModelsTransformer: StreamModelsTransformer {
func transform(channel: ChatChannel) -> ChatChannel {
channel.replacing(
name: "Custom Name",
diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
index 766207b8674..95bd96aa05c 100644
--- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
+++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
@@ -128,10 +128,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
team: channelController.channel?.team
) { [unowned self] error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't update name of channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't update name of channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -152,10 +154,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
extraData: channelController.channel?.extraData ?? [:]
) { [unowned self] error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't update image url of channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't update image url of channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -203,23 +207,27 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
let client = channelController.client
client.currentUserController().loadBlockedUsers { result in
guard let blockedUsers = try? result.get() else { return }
- self.rootViewController.present(MembersViewController(
- membersController: client.memberListController(
- query: .init(
- cid: cid,
- filter: .in(.id, values: blockedUsers.map(\.userId))
+ Task { @MainActor in
+ self.rootViewController.present(MembersViewController(
+ membersController: client.memberListController(
+ query: .init(
+ cid: cid,
+ filter: .in(.id, values: blockedUsers.map(\.userId))
+ )
)
- )
- ), animated: true)
+ ), animated: true)
+ }
}
}),
.init(title: "Load More Members", handler: { [unowned self] _ in
channelController.loadMoreChannelReads(limit: 100) { error in
guard let error else { return }
- self.rootViewController.presentAlert(
- title: "Couldn't load more members to channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't load more members to channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}),
.init(title: "Add member", isEnabled: canUpdateChannelMembers, handler: { [unowned self] _ in
@@ -233,10 +241,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
message: "Members added to the channel"
) { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't add user \(id) to channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't add user \(id) to channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -253,10 +263,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
message: "Members added to the channel"
) { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't add user \(id) to channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't add user \(id) to channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -268,13 +280,15 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
userIds: [member.id],
message: "Members removed from the channel"
) { [unowned self] error in
- if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't remove user \(member.id) from channel \(cid)",
- message: "\(error)"
- )
- } else {
- self.rootNavigationController?.popViewController(animated: true)
+ Task { @MainActor in
+ if let error = error {
+ self.rootViewController.presentAlert(
+ title: "Couldn't remove user \(member.id) from channel \(cid)",
+ message: "\(error)"
+ )
+ } else {
+ self.rootNavigationController?.popViewController(animated: true)
+ }
}
}
}
@@ -288,10 +302,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.memberController(userId: member.id, in: channelController.cid!)
.ban { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't ban user \(member.id) from channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't ban user \(member.id) from channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -305,10 +321,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.memberController(userId: member.id, in: channelController.cid!)
.shadowBan { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't ban user \(member.id) from channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't ban user \(member.id) from channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -322,10 +340,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.memberController(userId: member.id, in: channelController.cid!)
.unban { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't unban user \(member.id) from channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't unban user \(member.id) from channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -343,10 +363,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
message: "Premium member added to the channel"
) { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't add user \(id) to channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't add user \(id) to channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -356,15 +378,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
UIAlertAction(title: member.id, style: .default) { _ in
channelController.client.memberController(userId: member.id, in: cid)
.partialUpdate(extraData: ["is_premium": true], unsetProperties: nil) { [unowned self] result in
- do {
- let data = try result.get()
- print("Member updated. Premium: ", data.isPremium)
- self.rootNavigationController?.popViewController(animated: true)
- } catch {
- self.rootViewController.presentAlert(
- title: "Couldn't set user \(member.id) as premium.",
- message: "\(error)"
- )
+ Task { @MainActor in
+ do {
+ let data = try result.get()
+ print("Member updated. Premium: ", data.isPremium)
+ self.rootNavigationController?.popViewController(animated: true)
+ } catch {
+ self.rootViewController.presentAlert(
+ title: "Couldn't set user \(member.id) as premium.",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -374,15 +398,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.init(title: "Set current member as premium", isVisible: isPremiumMemberFeatureEnabled, handler: { [unowned self] _ in
channelController.client.currentUserController()
.updateMemberData(["is_premium": true], in: cid) { [unowned self] result in
- do {
- let data = try result.get()
- print("Member updated. Premium: ", data.isPremium)
- self.rootNavigationController?.popViewController(animated: true)
- } catch {
- self.rootViewController.presentAlert(
- title: "Couldn't set current user as premium.",
- message: "\(error)"
- )
+ Task { @MainActor in
+ do {
+ let data = try result.get()
+ print("Member updated. Premium: ", data.isPremium)
+ self.rootNavigationController?.popViewController(animated: true)
+ } catch {
+ self.rootViewController.presentAlert(
+ title: "Couldn't set current user as premium.",
+ message: "\(error)"
+ )
+ }
}
}
}),
@@ -391,15 +417,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
UIAlertAction(title: member.id, style: .default) { _ in
channelController.client.memberController(userId: member.id, in: cid)
.partialUpdate(extraData: nil, unsetProperties: ["is_premium"]) { [unowned self] result in
- do {
- let data = try result.get()
- print("Member updated. Premium: ", data.isPremium)
- self.rootNavigationController?.popViewController(animated: true)
- } catch {
- self.rootViewController.presentAlert(
- title: "Couldn't set user \(member.id) as premium.",
- message: "\(error)"
- )
+ Task { @MainActor in
+ do {
+ let data = try result.get()
+ print("Member updated. Premium: ", data.isPremium)
+ self.rootNavigationController?.popViewController(animated: true)
+ } catch {
+ self.rootViewController.presentAlert(
+ title: "Couldn't set user \(member.id) as premium.",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -409,21 +437,27 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.init(title: "Freeze channel", isEnabled: canFreezeChannel, handler: { [unowned self] _ in
channelController.freezeChannel { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't freeze channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't freeze channel \(cid)", message: "\(error)")
+ }
}
}
}),
.init(title: "Unfreeze channel", isEnabled: canFreezeChannel, handler: { [unowned self] _ in
channelController.unfreezeChannel { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't unfreeze channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't unfreeze channel \(cid)", message: "\(error)")
+ }
}
}
}),
.init(title: "Mute channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in
channelController.muteChannel { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)")
+ }
}
}
}),
@@ -435,7 +469,9 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
}
channelController.muteChannel(expiration: expiration * 1000) { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't mute channel \(cid)", message: "\(error)")
+ }
}
}
}
@@ -443,46 +479,60 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.init(title: "Cool channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in
channelController.partialChannelUpdate(extraData: ["is_cool": true]) { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) cool", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) cool", message: "\(error)")
+ }
}
}
}),
.init(title: "Uncool channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in
channelController.partialChannelUpdate(extraData: ["is_cool": false]) { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) uncool", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't make a channel \(cid) uncool", message: "\(error)")
+ }
}
}
}),
.init(title: "Unmute channel", isEnabled: canMuteChannel, handler: { [unowned self] _ in
channelController.unmuteChannel { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Couldn't unmute channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't unmute channel \(cid)", message: "\(error)")
+ }
}
}
}),
.init(title: "Pin channel", isEnabled: true, handler: { [unowned self] _ in
channelController.pin { error in
guard let error else { return }
- self.rootViewController.presentAlert(title: "Couldn't pin channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't pin channel \(cid)", message: "\(error)")
+ }
}
}),
.init(title: "Unpin channel", isEnabled: true, handler: { [unowned self] _ in
channelController.unpin { error in
guard let error else { return }
- self.rootViewController.presentAlert(title: "Couldn't unpin channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't unpin channel \(cid)", message: "\(error)")
+ }
}
}),
.init(title: "Archive channel", isEnabled: true, handler: { [unowned self] _ in
channelController.archive { error in
guard let error else { return }
- self.rootViewController.presentAlert(title: "Couldn't archive channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't archive channel \(cid)", message: "\(error)")
+ }
}
}),
.init(title: "Unarchive channel", isEnabled: true, handler: { [unowned self] _ in
channelController.unarchive { error in
guard let error else { return }
- self.rootViewController.presentAlert(title: "Couldn't unarchive channel \(cid)", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Couldn't unarchive channel \(cid)", message: "\(error)")
+ }
}
}),
.init(title: "Enable slow mode", isEnabled: canSetChannelCooldown, handler: { [unowned self] _ in
@@ -494,10 +544,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
}
channelController.enableSlowMode(cooldownDuration: duration) { [unowned self] error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't enable slow mode on channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't enable slow mode on channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}
@@ -505,10 +557,12 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.init(title: "Disable slow mode", isEnabled: canSetChannelCooldown, handler: { [unowned self] _ in
channelController.disableSlowMode { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't disable slow mode on channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't disable slow mode on channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}),
@@ -520,20 +574,24 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.init(title: "Clear History", handler: { _ in
channelController.hideChannel(clearHistory: true) { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't hide channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't hide channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}),
.init(title: "Keep History", handler: { _ in
channelController.hideChannel(clearHistory: false) { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't hide channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't hide channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
})
@@ -544,30 +602,36 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
.init(title: "Show channel", isEnabled: channelController.channel?.isHidden == true, handler: { [unowned self] _ in
channelController.showChannel { error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't unhide channel \(cid)",
- message: "\(error)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't unhide channel \(cid)",
+ message: "\(error)"
+ )
+ }
}
}
}),
.init(title: "Truncate channel w/o message", isEnabled: canUpdateChannel, handler: { _ in
channelController.truncateChannel { [unowned self] error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't truncate channel \(cid)",
- message: "\(error.localizedDescription)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't truncate channel \(cid)",
+ message: "\(error.localizedDescription)"
+ )
+ }
}
}
}),
.init(title: "Truncate channel with message", isEnabled: canUpdateChannel, handler: { _ in
channelController.truncateChannel(systemMessage: "Channel truncated") { [unowned self] error in
if let error = error {
- self.rootViewController.presentAlert(
- title: "Couldn't truncate channel \(cid)",
- message: "\(error.localizedDescription)"
- )
+ Task { @MainActor in
+ self.rootViewController.presentAlert(
+ title: "Couldn't truncate channel \(cid)",
+ message: "\(error.localizedDescription)"
+ )
+ }
}
}
}),
@@ -620,19 +684,21 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
let messageController = channelController.client.messageController(cid: cid, messageId: id)
messageController.synchronize { [weak self] error in
- let message = messageController.message
-
- var errorMessage: String? = error?.localizedDescription
- if message?.cid != cid {
- errorMessage = "Message ID does not belong to this channel."
- }
-
- if let errorMessage = errorMessage {
- self?.rootViewController.presentAlert(title: errorMessage)
- return
+ Task { @MainActor in
+ let message = messageController.message
+
+ var errorMessage: String? = error?.localizedDescription
+ if message?.cid != cid {
+ errorMessage = "Message ID does not belong to this channel."
+ }
+
+ if let errorMessage = errorMessage {
+ self?.rootViewController.presentAlert(title: errorMessage)
+ return
+ }
+
+ self?.showChannel(for: cid, at: message?.id)
}
-
- self?.showChannel(for: cid, at: message?.id)
}
}
}),
@@ -654,7 +720,9 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
channelController.client.currentUserController()
.updateUserData(unsetProperties: ["image"]) { [unowned self] error in
if let error {
- self.rootViewController.presentAlert(title: error.localizedDescription)
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: error.localizedDescription)
+ }
}
}
}),
@@ -684,7 +752,9 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
override func didTapDeleteButton(for cid: ChannelId) {
rootViewController.controller.client.channelController(for: cid).deleteChannel { error in
if let error = error {
- self.rootViewController.presentAlert(title: "Channel \(cid) couldn't be deleted", message: "\(error)")
+ Task { @MainActor in
+ self.rootViewController.presentAlert(title: "Channel \(cid) couldn't be deleted", message: "\(error)")
+ }
}
}
}
diff --git a/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift b/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
index e6daf818060..4f24699fc78 100644
--- a/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
+++ b/DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
@@ -37,16 +37,20 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
guard confirmed else { return }
self.messageController.deleteMessage { _ in
- let pollId = self.messageController.message?.poll?.id
- if let pollId, AppConfig.shared.demoAppConfig.shouldDeletePollOnMessageDeletion {
- let channelController = self.messageController.client.channelController(
- for: self.messageController.cid
- )
- channelController.deletePoll(pollId: pollId) { _ in
+ Task { @MainActor in
+ let pollId = self.messageController.message?.poll?.id
+ if let pollId, AppConfig.shared.demoAppConfig.shouldDeletePollOnMessageDeletion {
+ let channelController = self.messageController.client.channelController(
+ for: self.messageController.cid
+ )
+ channelController.deletePoll(pollId: pollId) { _ in
+ Task { @MainActor in
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
+ }
+ } else {
self.delegate?.chatMessageActionsVCDidFinish(self)
}
- } else {
- self.delegate?.chatMessageActionsVCDidFinish(self)
}
}
}
@@ -65,14 +69,18 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
if let error = error {
log.error("Error when pinning message: \(error)")
}
- self.delegate?.chatMessageActionsVCDidFinish(self)
+ Task { @MainActor in
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
}
} else {
self.messageController.unpin { error in
if let error = error {
log.error("Error when unpinning message: \(error)")
}
- self.delegate?.chatMessageActionsVCDidFinish(self)
+ Task { @MainActor in
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
}
}
},
@@ -88,7 +96,9 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
guard confirmed else { return }
self.messageController.deleteMessage(hard: true) { _ in
- self.delegate?.chatMessageActionsVCDidFinish(self)
+ Task { @MainActor in
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
}
}
},
@@ -101,7 +111,9 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
action: { [weak self] _ in
guard let self = self else { return }
self.messageController.translate(to: .turkish) { _ in
- self.delegate?.chatMessageActionsVCDidFinish(self)
+ Task { @MainActor in
+ self.delegate?.chatMessageActionsVCDidFinish(self)
+ }
}
},
diff --git a/DemoApp/StreamChat/Components/DemoChatThreadVC.swift b/DemoApp/StreamChat/Components/DemoChatThreadVC.swift
index 319cf930cc9..0d1e23b0582 100644
--- a/DemoApp/StreamChat/Components/DemoChatThreadVC.swift
+++ b/DemoApp/StreamChat/Components/DemoChatThreadVC.swift
@@ -54,7 +54,9 @@ class DemoChatThreadVC: ChatThreadVC, CurrentChatUserControllerDelegate {
return
}
self.messageController.updateThread(title: title) { [weak self] result in
- self?.thread = try? result.get()
+ Task { @MainActor in
+ self?.thread = try? result.get()
+ }
}
}
}),
@@ -63,8 +65,10 @@ class DemoChatThreadVC: ChatThreadVC, CurrentChatUserControllerDelegate {
}),
.init(title: "Load newest thread info", style: .default, handler: { [unowned self] _ in
self.messageController.loadThread { [weak self] result in
- self?.thread = try? result.get()
- self?.present(DebugObjectViewController(object: self?.thread), animated: true)
+ Task { @MainActor in
+ self?.thread = try? result.get()
+ self?.present(DebugObjectViewController(object: self?.thread), animated: true)
+ }
}
})
])
diff --git a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
index e63244d3ff0..3b741bfad7b 100644
--- a/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
+++ b/DemoApp/StreamChat/DemoAppCoordinator+DemoApp.swift
@@ -218,7 +218,7 @@ private extension DemoAppCoordinator {
func disconnect() {
chat.client?.disconnect { [weak self] in
- DispatchQueue.main.async {
+ Task { @MainActor in
self?.showLogin(animated: true)
}
}
diff --git a/DemoShare/DemoShareViewModel.swift b/DemoShare/DemoShareViewModel.swift
index 78c14b04ef2..0b4658df68b 100644
--- a/DemoShare/DemoShareViewModel.swift
+++ b/DemoShare/DemoShareViewModel.swift
@@ -178,8 +178,10 @@ class DemoShareViewModel: ObservableObject, ChatChannelControllerDelegate {
)
self.channelListController = chatClient.channelListController(query: channelListQuery)
channelListController?.synchronize { [weak self] error in
- guard let self, error == nil else { return }
- channels = channelListController?.channels ?? []
+ Task { @MainActor [weak self] in
+ guard let self, error == nil else { return }
+ channels = channelListController?.channels ?? []
+ }
}
}
}
diff --git a/Package.swift b/Package.swift
index 17c6eec1c8a..605f6188188 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.7
+// swift-tools-version:6.0
import Foundation
import PackageDescription
@@ -51,7 +51,10 @@ let package = Package(
dependencies: ["StreamChat"],
path: "TestTools/StreamChatTestMockServer",
exclude: ["Info.plist"],
- resources: [.process("Fixtures")]
+ resources: [.process("Fixtures")],
+ swiftSettings: [
+ .swiftLanguageMode(.v5)
+ ]
),
]
)
diff --git a/README.md b/README.md
index 04f85c8f7e2..035ed83ce85 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
diff --git a/Sources/StreamChat/APIClient/APIClient.swift b/Sources/StreamChat/APIClient/APIClient.swift
index c63078301b8..b7806679e52 100644
--- a/Sources/StreamChat/APIClient/APIClient.swift
+++ b/Sources/StreamChat/APIClient/APIClient.swift
@@ -5,7 +5,7 @@
import Foundation
/// An object allowing making request to Stream Chat servers.
-class APIClient {
+class APIClient: @unchecked Sendable {
/// The URL session used for all requests.
let session: URLSession
@@ -16,10 +16,10 @@ class APIClient {
let decoder: RequestDecoder
/// Used for reobtaining tokens when they expire and API client receives token expiration error
- var tokenRefresher: ((@escaping () -> Void) -> Void)?
+ @Atomic var tokenRefresher: ((@escaping @Sendable() -> Void) -> Void)?
/// Used to queue requests that happen while we are offline
- var queueOfflineRequest: QueueOfflineRequestBlock?
+ @Atomic var queueOfflineRequest: QueueOfflineRequestBlock?
/// The attachment downloader.
let attachmentDownloader: AttachmentDownloader
@@ -79,7 +79,7 @@ class APIClient {
/// - completion: Called when the networking request is finished.
func request(
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
let requestOperation = operation(endpoint: endpoint, isRecoveryOperation: false, completion: completion)
operationQueue.addOperation(requestOperation)
@@ -92,7 +92,7 @@ class APIClient {
/// - completion: Called when the networking request is finished.
func recoveryRequest(
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
if !isInRecoveryMode {
log.assertionFailure("We should not call this method if not in recovery mode")
@@ -110,7 +110,7 @@ class APIClient {
/// - completion: Called when the networking request is finished.
func unmanagedRequest(
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
OperationQueue.main.addOperation(
unmanagedOperation(endpoint: endpoint, completion: completion)
@@ -120,7 +120,7 @@ class APIClient {
private func operation(
endpoint: Endpoint,
isRecoveryOperation: Bool,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) -> AsyncOperation {
AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in
guard let self = self else {
@@ -189,7 +189,7 @@ class APIClient {
private func unmanagedOperation(
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) -> AsyncOperation {
AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in
self?.executeRequest(endpoint: endpoint) { [weak self] result in
@@ -219,7 +219,7 @@ class APIClient {
/// - completion: Called when the networking request is finished.
private func executeRequest(
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
encoder.encodeRequest(for: endpoint) { [weak self] (requestResult) in
let urlRequest: URLRequest
@@ -268,7 +268,7 @@ class APIClient {
}
}
- private func refreshToken(completion: @escaping (ClientError) -> Void) {
+ private func refreshToken(completion: @escaping @Sendable(ClientError) -> Void) {
guard !isRefreshingToken else {
completion(ClientError.RefreshingToken())
return
@@ -290,11 +290,11 @@ class APIClient {
func downloadFile(
from remoteURL: URL,
to localURL: URL,
- progress: ((Double) -> Void)?,
- completion: @escaping (Error?) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Error?) -> Void
) {
let downloadOperation = AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in
- self?.attachmentDownloader.download(from: remoteURL, to: localURL, progress: progress) { error in
+ self?.attachmentDownloader.download(from: remoteURL, to: localURL, progress: progress) { [weak self] error in
if let error, self?.isConnectionError(error) == true {
// Do not retry unless its a connection problem and we still have retries left
if operation.canRetry {
@@ -314,11 +314,11 @@ class APIClient {
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)?,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Result) -> Void
) {
let uploadOperation = AsyncOperation(maxRetries: maximumRequestRetries) { [weak self] operation, done in
- self?.attachmentUploader.upload(attachment, progress: progress) { result in
+ self?.attachmentUploader.upload(attachment, progress: progress) { [weak self] result in
switch result {
case let .failure(error) where self?.isConnectionError(error) == true:
// Do not retry unless its a connection problem and we still have retries left
diff --git a/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift b/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift
index 95f1c604ce8..3f3a363afd2 100644
--- a/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift
+++ b/Sources/StreamChat/APIClient/AttachmentDownloader/AttachmentDownloader.swift
@@ -16,14 +16,15 @@ protocol AttachmentDownloader {
func download(
from remoteURL: URL,
to localURL: URL,
- progress: ((Double) -> Void)?,
- completion: @escaping (Error?) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Error?) -> Void
)
}
-final class StreamAttachmentDownloader: AttachmentDownloader {
+final class StreamAttachmentDownloader: AttachmentDownloader, Sendable {
private let session: URLSession
- @Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:]
+ nonisolated(unsafe) private var _taskProgressObservers: [Int: NSKeyValueObservation] = [:]
+ private let queue = DispatchQueue(label: "io.getstream.stream-attachment-downloader", target: .global())
init(sessionConfiguration: URLSessionConfiguration) {
session = URLSession(configuration: sessionConfiguration)
@@ -32,8 +33,8 @@ final class StreamAttachmentDownloader: AttachmentDownloader {
func download(
from remoteURL: URL,
to localURL: URL,
- progress: ((Double) -> Void)?,
- completion: @escaping (Error?) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Error?) -> Void
) {
let request = URLRequest(url: remoteURL)
let task = session.downloadTask(with: request) { temporaryURL, _, downloadError in
@@ -54,13 +55,13 @@ final class StreamAttachmentDownloader: AttachmentDownloader {
}
if let progressHandler = progress {
let taskID = task.taskIdentifier
- _taskProgressObservers.mutate { observers in
- observers[taskID] = task.progress.observe(\.fractionCompleted, options: [.initial]) { [weak self] progress, _ in
+ queue.async { [weak self] in
+ self?._taskProgressObservers[taskID] = task.progress.observe(\.fractionCompleted, options: [.initial]) { [weak self] progress, _ in
progressHandler(progress.fractionCompleted)
if progress.isFinished || progress.isCancelled {
- self?._taskProgressObservers.mutate { observers in
- observers[taskID]?.invalidate()
- observers[taskID] = nil
+ self?.queue.async { [weak self] in
+ self?._taskProgressObservers[taskID]?.invalidate()
+ self?._taskProgressObservers[taskID] = nil
}
}
}
diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift b/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
index 33ae7ef0832..073b85120d1 100644
--- a/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
+++ b/Sources/StreamChat/APIClient/AttachmentUploader/AttachmentUploader.swift
@@ -5,7 +5,7 @@
import Foundation
/// The component responsible to upload files.
-public protocol AttachmentUploader {
+public protocol AttachmentUploader: Sendable {
/// Uploads a type-erased attachment, and returns the attachment with the remote information.
/// - Parameters:
/// - attachment: A type-erased attachment.
@@ -13,12 +13,12 @@ public protocol AttachmentUploader {
/// - completion: The callback with the uploaded attachment.
func upload(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)?,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Result) -> Void
)
}
-public class StreamAttachmentUploader: AttachmentUploader {
+public class StreamAttachmentUploader: AttachmentUploader, @unchecked Sendable {
let cdnClient: CDNClient
init(cdnClient: CDNClient) {
@@ -27,8 +27,8 @@ public class StreamAttachmentUploader: AttachmentUploader {
public func upload(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)?,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Result) -> Void
) {
cdnClient.uploadAttachment(attachment, progress: progress) { (result: Result) in
completion(result.map { file in
diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift
index 2dee646b0e3..3ed99a53927 100644
--- a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift
+++ b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachment.swift
@@ -5,7 +5,7 @@
import Foundation
/// The attachment which was successfully uploaded.
-public struct UploadedAttachment {
+public struct UploadedAttachment: Sendable {
/// The attachment which contains the payload details of the attachment.
public var attachment: AnyChatMessageAttachment
diff --git a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift
index 047cbcf8102..10f53e21f89 100644
--- a/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift
+++ b/Sources/StreamChat/APIClient/AttachmentUploader/UploadedAttachmentPostProcessor.swift
@@ -5,6 +5,6 @@
import Foundation
/// A component that can be used to change an attachment which was successfully uploaded.
-public protocol UploadedAttachmentPostProcessor {
+public protocol UploadedAttachmentPostProcessor: Sendable {
func process(uploadedAttachment: UploadedAttachment) -> UploadedAttachment
}
diff --git a/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift b/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
index facef3c0f28..bd0ee74ab7b 100644
--- a/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
+++ b/Sources/StreamChat/APIClient/CDNClient/CDNClient.swift
@@ -16,7 +16,7 @@ public struct UploadedFile: Decodable {
}
/// The CDN client is responsible to upload files to a CDN.
-public protocol CDNClient {
+public protocol CDNClient: Sendable {
static var maxAttachmentSize: Int64 { get }
/// Uploads attachment as a multipart/form-data and returns only the uploaded remote file.
@@ -26,8 +26,8 @@ public protocol CDNClient {
/// - completion: Returns the uploaded file's information.
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)?,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Result) -> Void
)
/// Uploads attachment as a multipart/form-data and returns the uploaded remote file and its thumbnail.
@@ -37,16 +37,16 @@ public protocol CDNClient {
/// - completion: Returns the uploaded file's information.
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)?,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Result) -> Void
)
}
public extension CDNClient {
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)?,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)?,
+ completion: @escaping @Sendable(Result) -> Void
) {
uploadAttachment(attachment, progress: progress, completion: { (result: Result) in
switch result {
@@ -60,14 +60,15 @@ public extension CDNClient {
}
/// Default implementation of CDNClient that uses Stream CDN
-class StreamCDNClient: CDNClient {
+final class StreamCDNClient: CDNClient, Sendable {
static var maxAttachmentSize: Int64 { 100 * 1024 * 1024 }
private let decoder: RequestDecoder
private let encoder: RequestEncoder
private let session: URLSession
/// Keeps track of uploading tasks progress
- @Atomic private var taskProgressObservers: [Int: NSKeyValueObservation] = [:]
+ private nonisolated(unsafe) var _taskProgressObservers: [Int: NSKeyValueObservation] = [:]
+ private let queue = DispatchQueue(label: "io.getstream.stream-cdn-client", target: .global())
init(
encoder: RequestEncoder,
@@ -81,8 +82,8 @@ class StreamCDNClient: CDNClient {
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)? = nil,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)? = nil,
+ completion: @escaping @Sendable(Result) -> Void
) {
uploadAttachment(attachment, progress: progress, completion: { (result: Result) in
switch result {
@@ -96,8 +97,8 @@ class StreamCDNClient: CDNClient {
func uploadAttachment(
_ attachment: AnyChatMessageAttachment,
- progress: ((Double) -> Void)? = nil,
- completion: @escaping (Result) -> Void
+ progress: (@Sendable(Double) -> Void)? = nil,
+ completion: @escaping @Sendable(Result) -> Void
) {
guard
let uploadingState = attachment.uploadingState,
@@ -148,13 +149,13 @@ class StreamCDNClient: CDNClient {
if let progressListener = progress {
let taskID = task.taskIdentifier
- self._taskProgressObservers.mutate { observers in
- observers[taskID] = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
+ queue.async {
+ self._taskProgressObservers[taskID] = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
progressListener(progress.fractionCompleted)
if progress.isFinished || progress.isCancelled {
- self?._taskProgressObservers.mutate { observers in
- observers[taskID]?.invalidate()
- observers[taskID] = nil
+ self?.queue.async { [weak self] in
+ self?._taskProgressObservers[taskID]?.invalidate()
+ self?._taskProgressObservers[taskID] = nil
}
}
}
diff --git a/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift b/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift
index 35c9854143c..f85016357ef 100644
--- a/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift
+++ b/Sources/StreamChat/APIClient/ChatRemoteNotificationHandler.swift
@@ -66,9 +66,9 @@ public class ChatPushNotificationInfo {
}
}
-public class ChatRemoteNotificationHandler {
- var client: ChatClient
- var content: UNNotificationContent
+public class ChatRemoteNotificationHandler: @unchecked Sendable {
+ let client: ChatClient
+ let content: UNNotificationContent
let chatCategoryIdentifiers: Set = ["stream.chat", "MESSAGE_NEW"]
let channelRepository: ChannelRepository
let messageRepository: MessageRepository
@@ -80,7 +80,7 @@ public class ChatRemoteNotificationHandler {
messageRepository = client.messageRepository
}
- public func handleNotification(completion: @escaping (ChatPushNotificationContent) -> Void) -> Bool {
+ public func handleNotification(completion: @escaping @Sendable(ChatPushNotificationContent) -> Void) -> Bool {
guard chatCategoryIdentifiers.contains(content.categoryIdentifier) else {
return false
}
@@ -89,7 +89,7 @@ public class ChatRemoteNotificationHandler {
return true
}
- private func getContent(completion: @escaping (ChatPushNotificationContent) -> Void) {
+ private func getContent(completion: @escaping @Sendable(ChatPushNotificationContent) -> Void) {
guard let payload = content.userInfo["stream"], let dict = payload as? [String: String] else {
return completion(.unknown(UnknownNotificationContent(content: content)))
}
@@ -115,7 +115,7 @@ public class ChatRemoteNotificationHandler {
}
}
- private func getContent(cid: ChannelId, messageId: MessageId, completion: @escaping (ChatMessage?, ChatChannel?) -> Void) {
+ private func getContent(cid: ChannelId, messageId: MessageId, completion: @escaping @Sendable(ChatMessage?, ChatChannel?) -> Void) {
var query = ChannelQuery(cid: cid, pageSize: 10, membersLimit: 10)
query.options = .state
channelRepository.getChannel(for: query, store: false) { [messageRepository] channelResult in
diff --git a/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
index ce531036df7..452e967aa45 100644
--- a/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
@@ -292,7 +292,7 @@ extension Endpoint {
static func startTypingEvent(cid: ChannelId, parentMessageId: MessageId?) -> Endpoint {
let eventType = EventType.userStartTyping
- let body: Encodable
+ let body: Encodable & Sendable
if let parentMessageId = parentMessageId {
body = ["event": ["type": eventType.rawValue, "parent_id": parentMessageId]]
} else {
@@ -309,7 +309,7 @@ extension Endpoint {
static func stopTypingEvent(cid: ChannelId, parentMessageId: MessageId?) -> Endpoint {
let eventType = EventType.userStopTyping
- let body: Encodable
+ let body: Encodable & Sendable
if let parentMessageId = parentMessageId {
body = ["event": ["type": eventType.rawValue, "parent_id": parentMessageId]]
} else {
diff --git a/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift b/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift
index 2741620545d..2ea34bcc343 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Endpoint.swift
@@ -4,21 +4,21 @@
import Foundation
-struct Endpoint: Codable {
+struct Endpoint: Codable, Sendable {
let path: EndpointPath
let method: EndpointMethod
- let queryItems: Encodable?
+ let queryItems: (Encodable & Sendable)?
let requiresConnectionId: Bool
let requiresToken: Bool
- let body: Encodable?
+ let body: (Encodable & Sendable)?
init(
path: EndpointPath,
method: EndpointMethod,
- queryItems: Encodable? = nil,
+ queryItems: (Encodable & Sendable)? = nil,
requiresConnectionId: Bool = false,
requiresToken: Bool = true,
- body: Encodable? = nil
+ body: (Encodable & Sendable)? = nil
) {
self.path = path
self.method = method
@@ -64,7 +64,7 @@ struct Endpoint: Codable {
}
}
-private extension Encodable {
+private extension Encodable where Self: Sendable {
func encodedAsData() throws -> Data {
try JSONEncoder.stream.encode(AnyEncodable(self))
}
@@ -79,7 +79,7 @@ enum EndpointMethod: String, Codable, Equatable {
}
/// A type representing empty response of an Endpoint.
-public struct EmptyResponse: Decodable {}
+public struct EmptyResponse: Decodable, Sendable {}
/// A type representing empty body for `.post` Endpoints.
/// Our backend currently expects a body (not `nil`), even if it's empty.
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
index 2d365f39cfb..2b3ce86560e 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
@@ -213,7 +213,7 @@ struct ChannelReadPayload: Decodable {
}
/// A channel config.
-public class ChannelConfig: Codable {
+public final class ChannelConfig: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case reactionsEnabled = "reactions"
case typingEventsEnabled = "typing_events"
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift
index 42f1fff9b22..a5a74ed8baa 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift
@@ -5,7 +5,7 @@
import Foundation
/// An object describing the incoming current user JSON payload.
-class CurrentUserPayload: UserPayload {
+final class CurrentUserPayload: UserPayload, @unchecked Sendable {
/// A list of devices.
let devices: [DevicePayload]
/// Muted users.
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift
index a94b633cfbb..2040aa42af4 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/DraftPayloads.swift
@@ -4,7 +4,7 @@
import Foundation
-class DraftPayloadResponse: Decodable {
+final class DraftPayloadResponse: Decodable, Sendable {
let draft: DraftPayload
init(draft: DraftPayload) {
@@ -12,7 +12,7 @@ class DraftPayloadResponse: Decodable {
}
}
-class DraftListPayloadResponse: Decodable {
+final class DraftListPayloadResponse: Decodable, Sendable {
let drafts: [DraftPayload]
let next: String?
@@ -22,7 +22,7 @@ class DraftListPayloadResponse: Decodable {
}
}
-class DraftPayload: Decodable {
+final class DraftPayload: Decodable, Sendable {
let cid: ChannelId?
let channelPayload: ChannelDetailPayload?
let createdAt: Date
@@ -61,7 +61,7 @@ class DraftPayload: Decodable {
}
}
-class DraftMessagePayload: Decodable {
+final class DraftMessagePayload: Decodable, Sendable {
let id: String
let text: String
let command: String?
@@ -113,7 +113,7 @@ class DraftMessagePayload: Decodable {
}
}
-class DraftMessageRequestBody: Encodable {
+final class DraftMessageRequestBody: Encodable, Sendable {
let id: String
let text: String
let command: String?
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift
index 9078b2c56ef..dca9205a6c4 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessageModerationDetailsPayload.swift
@@ -4,7 +4,7 @@
import Foundation
-struct MessageModerationDetailsPayload: Decodable {
+struct MessageModerationDetailsPayload: Decodable, Sendable {
let originalText: String
let action: String
let textHarms: [String]?
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
index 880be5f98ac..f49da891ef2 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
@@ -70,7 +70,7 @@ struct MessageSearchResultsPayload: Decodable {
}
/// An object describing the incoming message JSON payload.
-class MessagePayload: Decodable {
+final class MessagePayload: Decodable, Sendable {
let id: String
/// Only messages from `translate` endpoint contain `cid`
let cid: ChannelId?
@@ -103,16 +103,16 @@ class MessagePayload: Decodable {
let translations: [TranslationLanguage: String]?
let originalLanguage: String?
let moderationDetails: MessageModerationDetailsPayload? // moderation v1 payload
- var moderation: MessageModerationDetailsPayload? // moderation v2 payload
+ let moderation: MessageModerationDetailsPayload? // moderation v2 payload
- var pinned: Bool
- var pinnedBy: UserPayload?
- var pinnedAt: Date?
- var pinExpires: Date?
+ let pinned: Bool
+ let pinnedBy: UserPayload?
+ let pinnedAt: Date?
+ let pinExpires: Date?
- var poll: PollPayload?
+ let poll: PollPayload?
- var draft: DraftPayload?
+ let draft: DraftPayload?
/// Only message payload from `getMessage` endpoint contains channel data. It's a convenience workaround for having to
/// make an extra call do get channel details.
@@ -263,7 +263,7 @@ class MessagePayload: Decodable {
}
/// An object describing the outgoing message JSON payload.
-struct MessageRequestBody: Encodable {
+struct MessageRequestBody: Encodable, Sendable {
let id: String
let user: UserRequestBody
let text: String
@@ -366,7 +366,7 @@ struct MessageReactionsPayload: Decodable {
}
/// A command in a message, e.g. /giphy.
-public struct Command: Codable, Hashable {
+public struct Command: Codable, Hashable, Sendable {
/// A command name.
public let name: String
/// A description.
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift
index 9b5244b289e..af77a2f7c91 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/RawJSON.swift
@@ -7,7 +7,7 @@ import Foundation
/// A `RawJSON` type. The type used for handling extra data.
/// Used to store and operate objects of unknown structure that's not possible to decode.
/// https://forums.swift.org/t/new-unevaluated-type-for-decoder-to-allow-later-re-encoding-of-data-with-unknown-structure/11117
-public indirect enum RawJSON: Codable, Hashable {
+public indirect enum RawJSON: Codable, Hashable, Sendable {
case number(Double)
case string(String)
case bool(Bool)
diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift
index 8210005270c..fa93278602e 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift
@@ -34,7 +34,7 @@ enum UserPayloadsCodingKeys: String, CodingKey, CaseIterable {
// MARK: - GET users
/// An object describing the incoming user JSON payload.
-class UserPayload: Decodable {
+class UserPayload: Decodable, @unchecked Sendable {
let id: String
let name: String?
let imageURL: URL?
@@ -118,7 +118,7 @@ class UserPayload: Decodable {
}
/// An object describing the outgoing user JSON payload.
-class UserRequestBody: Encodable {
+final class UserRequestBody: Encodable, Sendable {
let id: String
let name: String?
let imageURL: URL?
diff --git a/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift b/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift
index 11ab1791110..86014f3628c 100644
--- a/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift
+++ b/Sources/StreamChat/APIClient/Endpoints/Requests/CustomEventRequestBody.swift
@@ -15,3 +15,5 @@ struct CustomEventRequestBody: Encodable {
try json.encode(to: encoder)
}
}
+
+extension CustomEventRequestBody: Sendable where Payload: Sendable {}
diff --git a/Sources/StreamChat/APIClient/RequestDecoder.swift b/Sources/StreamChat/APIClient/RequestDecoder.swift
index fd0f34da3de..07f9dda9b6c 100644
--- a/Sources/StreamChat/APIClient/RequestDecoder.swift
+++ b/Sources/StreamChat/APIClient/RequestDecoder.swift
@@ -5,7 +5,7 @@
import Foundation
/// An object responsible for handling incoming URL request response and decoding it.
-protocol RequestDecoder {
+protocol RequestDecoder: Sendable {
/// Decodes an incoming URL request response.
///
/// - Parameters:
@@ -86,11 +86,11 @@ struct DefaultRequestDecoder: RequestDecoder {
}
extension ClientError {
- final class ExpiredToken: ClientError {}
- final class RefreshingToken: ClientError {}
- final class TokenRefreshed: ClientError {}
- final class ConnectionError: ClientError {}
- final class ResponseBodyEmpty: ClientError {
+ final class ExpiredToken: ClientError, @unchecked Sendable {}
+ final class RefreshingToken: ClientError, @unchecked Sendable {}
+ final class TokenRefreshed: ClientError, @unchecked Sendable {}
+ final class ConnectionError: ClientError, @unchecked Sendable {}
+ final class ResponseBodyEmpty: ClientError, @unchecked Sendable {
override var localizedDescription: String { "Response body is empty." }
}
diff --git a/Sources/StreamChat/APIClient/RequestEncoder.swift b/Sources/StreamChat/APIClient/RequestEncoder.swift
index bb6ac26b3d7..7490a31487d 100644
--- a/Sources/StreamChat/APIClient/RequestEncoder.swift
+++ b/Sources/StreamChat/APIClient/RequestEncoder.swift
@@ -5,7 +5,7 @@
import Foundation
/// On object responsible for creating a `URLRequest`, and encoding all required and `Endpoint` specific data to it.
-protocol RequestEncoder {
+protocol RequestEncoder: Sendable {
/// A delegate the encoder uses for obtaining the current `connectionId`.
///
/// Trying to encode an `Endpoint` with the `requiresConnectionId` set to `true` without setting the delegate
@@ -19,7 +19,7 @@ protocol RequestEncoder {
/// - completion: Called when the encoded `URLRequest` is ready. Called with en `Error` if the encoding fails.
func encodeRequest(
for endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
)
/// Creates a new `RequestEncoder`.
@@ -45,7 +45,7 @@ extension RequestEncoder {
subsystems: .httpRequests
)
- var result: Result = .failure(
+ nonisolated(unsafe) var result: Result = .failure(
ClientError("Unexpected error. The result was not changed after encoding the request.")
)
@@ -70,7 +70,7 @@ extension RequestEncoder {
}
/// The default implementation of `RequestEncoder`.
-class DefaultRequestEncoder: RequestEncoder {
+final class DefaultRequestEncoder: RequestEncoder, Sendable {
let baseURL: URL
let apiKey: APIKey
@@ -83,11 +83,18 @@ class DefaultRequestEncoder: RequestEncoder {
/// On the other hand, any big number for a timeout here would be "to much". In normal situations, the requests should be back in less than a second,
/// otherwise we have a connection problem, which is handled as described above.
private let waiterTimeout: TimeInterval = 10
- weak var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate?
+ private let queue = DispatchQueue(label: "io.getstream.default-request-encoder", target: .global(qos: .userInitiated))
+
+ var connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate? {
+ get { queue.sync { _connectionDetailsProviderDelegate } }
+ set { queue.sync { _connectionDetailsProviderDelegate = newValue } }
+ }
+
+ nonisolated(unsafe) private weak var _connectionDetailsProviderDelegate: ConnectionDetailsProviderDelegate?
func encodeRequest(
for endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
var request: URLRequest
@@ -135,7 +142,7 @@ class DefaultRequestEncoder: RequestEncoder {
private func addAuthorizationHeader(
request: URLRequest,
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
guard endpoint.requiresToken else {
var updatedRequest = request
@@ -175,7 +182,7 @@ class DefaultRequestEncoder: RequestEncoder {
private func addConnectionIdIfNeeded(
request: URLRequest,
endpoint: Endpoint,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
guard endpoint.requiresConnectionId else {
completion(.success(request))
@@ -242,7 +249,7 @@ class DefaultRequestEncoder: RequestEncoder {
}
}
- private func encodeJSONToQueryItems(request: inout URLRequest, data: Encodable) throws {
+ private func encodeJSONToQueryItems(request: inout URLRequest, data: Encodable & Sendable) throws {
let data = try (data as? Data) ?? JSONEncoder.stream.encode(AnyEncodable(data))
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw ClientError.InvalidJSON("Data is not a valid JSON: \(String(data: data, encoding: .utf8) ?? "nil")")
@@ -299,12 +306,12 @@ private extension URL {
typealias WaiterToken = String
protocol ConnectionDetailsProviderDelegate: AnyObject {
- func provideConnectionId(timeout: TimeInterval, completion: @escaping (Result) -> Void)
- func provideToken(timeout: TimeInterval, completion: @escaping (Result) -> Void)
+ func provideConnectionId(timeout: TimeInterval, completion: @escaping @Sendable(Result) -> Void)
+ func provideToken(timeout: TimeInterval, completion: @escaping @Sendable(Result) -> Void)
}
public extension ClientError {
- final class InvalidURL: ClientError {}
- final class InvalidJSON: ClientError {}
- final class MissingConnectionId: ClientError {}
+ final class InvalidURL: ClientError, @unchecked Sendable {}
+ final class InvalidJSON: ClientError, @unchecked Sendable {}
+ final class MissingConnectionId: ClientError, @unchecked Sendable {}
}
diff --git a/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift b/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift
index 82f5699d91f..0681700194b 100644
--- a/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift
+++ b/Sources/StreamChat/Audio/Analysis/AudioAnalysing.swift
@@ -7,7 +7,7 @@ import AVFoundation
/// Describes an object that given an `AudioAnalysisContext` will analyse and process it in order to
/// generate a set of data points that describe some characteristics of the audio track provided from the
/// context.
-protocol AudioAnalysing {
+protocol AudioAnalysing: Sendable {
/// Analyse and process the provided context and provide data points limited to the number of the
/// targetSamples.
/// - Parameters:
@@ -26,7 +26,7 @@ final class StreamAudioWaveformAnalyser: AudioAnalysing {
private let audioSamplesExtractor: AudioSamplesExtractor
private let audioSamplesProcessor: AudioSamplesProcessor
private let audioSamplesPercentageNormaliser: AudioValuePercentageNormaliser
- private let outputSettings: [String: Any]
+ nonisolated(unsafe) private let outputSettings: [String: Any]
init(
audioSamplesExtractor: AudioSamplesExtractor,
@@ -148,7 +148,7 @@ final class StreamAudioWaveformAnalyser: AudioAnalysing {
// MARK: - Errors
-final class AudioAnalysingError: ClientError {
+final class AudioAnalysingError: ClientError, @unchecked Sendable {
/// Failed to read the asset provided by the `AudioAnalysisContext`
static func failedToReadAsset(file: StaticString = #file, line: UInt = #line) -> AudioAnalysingError {
.init("Failed to read AVAsset.", file, line)
diff --git a/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift b/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift
index 689d4704a36..8522ce71aef 100644
--- a/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift
+++ b/Sources/StreamChat/Audio/Analysis/AudioAnalysisEngine.swift
@@ -5,7 +5,7 @@
import AVFoundation
/// An object responsible to coordinate the audio analysis pipeline
-public struct AudioAnalysisEngine {
+public struct AudioAnalysisEngine: Sendable {
/// The loader that will be called to when loading asset properties is required
private let assetPropertiesLoader: AssetPropertyLoading
@@ -53,7 +53,7 @@ public struct AudioAnalysisEngine {
public func waveformVisualisation(
fromAudioURL audioURL: URL,
for targetSamples: Int,
- completionHandler: @escaping (Result<[Float], Error>) -> Void
+ completionHandler: @escaping @Sendable(Result<[Float], Error>) -> Void
) {
let asset = AVURLAsset(
url: audioURL,
@@ -112,7 +112,7 @@ public struct AudioAnalysisEngine {
// MARK: - Errors
-public final class AudioAnalysisEngineError: ClientError {
+public final class AudioAnalysisEngineError: ClientError, @unchecked Sendable {
/// An error occurred when the Audio track cannot be loaded from the AudioFile provided.
public static func failedToLoadAVAssetTrack(file: StaticString = #file, line: UInt = #line) -> AudioAnalysisEngineError {
.init("Failed to load AVAssetTrack.", file, line)
diff --git a/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift b/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift
index ee2c46dd009..d69ded2bbad 100644
--- a/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift
+++ b/Sources/StreamChat/Audio/Analysis/AudioSamplesExtractor.swift
@@ -8,7 +8,7 @@ import AVFoundation
/// of samples to process based on the provided downsamplingRate.
///
/// - Note: Audio samples are expected to be stored in Int16 format.
-internal class AudioSamplesExtractor {
+internal class AudioSamplesExtractor: @unchecked Sendable {
/// A struct to represent the result of the extractSamples method
struct Result: Equatable { var samplesToProcess, downSampledLength: Int }
diff --git a/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift b/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift
index 83e28d7acd1..df620c51e95 100644
--- a/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift
+++ b/Sources/StreamChat/Audio/Analysis/AudioSamplesProcessor.swift
@@ -6,7 +6,7 @@ import Accelerate
import AVFoundation
/// An object with the purpose to prepare the provided audio data for visualisation or further processing.
-internal class AudioSamplesProcessor {
+class AudioSamplesProcessor: @unchecked Sendable {
let noiseFloor: Float
/// Creates a new instances with the desired noiseFloor value
diff --git a/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift b/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift
index c7e68a4632d..fba466ba85f 100644
--- a/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift
+++ b/Sources/StreamChat/Audio/Analysis/AudioValuePercentageNormaliser.swift
@@ -5,7 +5,7 @@
import Foundation
/// The normaliser computes the percentage values or value of the provided array or value.
-internal class AudioValuePercentageNormaliser {
+class AudioValuePercentageNormaliser: @unchecked Sendable {
internal let valueRange: ClosedRange = -50...0
/// Compute the range between the min and max values
diff --git a/Sources/StreamChat/Audio/AssetPropertyLoading.swift b/Sources/StreamChat/Audio/AssetPropertyLoading.swift
index 71ff0b03453..4608089f4a6 100644
--- a/Sources/StreamChat/Audio/AssetPropertyLoading.swift
+++ b/Sources/StreamChat/Audio/AssetPropertyLoading.swift
@@ -71,7 +71,7 @@ public struct AssetPropertyLoadingCompositeError: Error {
}
/// Defines a type that represents the properties of an asset that can be loaded
-public struct AssetProperty: CustomStringConvertible {
+public struct AssetProperty: CustomStringConvertible, Sendable {
/// The property's name
public var name: String
@@ -84,7 +84,7 @@ public struct AssetProperty: CustomStringConvertible {
}
/// A protocol that describes an object that can be used to load properties from an AVAsset
-public protocol AssetPropertyLoading {
+public protocol AssetPropertyLoading: Sendable {
/// A method that loads the property of an AVAsset asynchronously and
/// returns a result through a completion handler
/// - Parameters:
@@ -94,7 +94,7 @@ public protocol AssetPropertyLoading {
func loadProperties(
_ properties: [AssetProperty],
of asset: Asset,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
)
}
@@ -105,16 +105,19 @@ public struct StreamAssetPropertyLoader: AssetPropertyLoading {
public func loadProperties(
_ properties: [AssetProperty],
of asset: Asset,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
// it's worth noting here that according to the documentation, the completion
// handler will be invoked only once, regardless of the number of
// properties we are loading.
// https://developer.apple.com/documentation/avfoundation/avasynchronouskeyvalueloading/1387321-loadvaluesasynchronously
+
+ // On iOS 15 we should switch to async load methods
+ nonisolated(unsafe) let unsafeAsset = asset
asset.loadValuesAsynchronously(forKeys: properties.map(\.name)) {
handlePropertiesLoadingResult(
properties,
- of: asset,
+ of: unsafeAsset,
completion: completion
)
}
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift
index b77fdf9b95a..e5def1c4a93 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContext.swift
@@ -5,7 +5,7 @@
import Foundation
/// A struct that represents the current state of an audio player
-public struct AudioPlaybackContext: Equatable {
+public struct AudioPlaybackContext: Equatable, Sendable {
public var assetLocation: URL?
/// The duration of the audio track in seconds
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift
index 318a89298e9..2409973e4d1 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackContextAccessor.swift
@@ -5,7 +5,7 @@
import Foundation
/// Provides thread-safe access to the value's storage
-final class AudioPlaybackContextAccessor {
+final class AudioPlaybackContextAccessor: @unchecked Sendable {
/// The queue that thread-safe access to the value's storage
private var accessQueue: DispatchQueue
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift
index 8808eecb84c..e51ebf14b0e 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackRate.swift
@@ -5,7 +5,7 @@
import Foundation
/// Defines a struct that describes the audio playback rate with a raw value of Float
-public struct AudioPlaybackRate: Comparable, Equatable {
+public struct AudioPlaybackRate: Comparable, Equatable, Sendable {
public let rawValue: Float
public init(rawValue: Float) {
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift
index 122e952835e..0e192a06b37 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaybackState.swift
@@ -5,7 +5,7 @@
import Foundation
/// Defines an struct which describes an audio player's playback state
-public struct AudioPlaybackState: Equatable, CustomStringConvertible {
+public struct AudioPlaybackState: Equatable, CustomStringConvertible, Sendable {
/// The name that describes the state
public let name: String
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift
index b4514071cd9..e466d23c4d1 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlayerObserving.swift
@@ -14,7 +14,7 @@ protocol AudioPlayerObserving {
/// - block: The block to call once a `timeControlStatus` update occurs
func addTimeControlStatusObserver(
_ player: AVPlayer,
- using block: @escaping (AVPlayer.TimeControlStatus?) -> Void
+ using block: @escaping @Sendable(AVPlayer.TimeControlStatus?) -> Void
)
/// Registers an observer that will periodically invoke the given block during playback to
@@ -28,7 +28,7 @@ protocol AudioPlayerObserving {
_ player: AVPlayer,
forInterval interval: CMTime,
queue: DispatchQueue?,
- using block: @escaping () -> Void
+ using block: @escaping @Sendable() -> Void
)
/// Registers and observer that will be called once the playback of an item stops
@@ -37,7 +37,7 @@ protocol AudioPlayerObserving {
/// - block: The block to call once a player's item has stopped
func addStoppedPlaybackObserver(
queue: OperationQueue?,
- using block: @escaping (AVPlayerItem) -> Void
+ using block: @escaping @Sendable(AVPlayerItem) -> Void
)
}
@@ -86,7 +86,7 @@ final class StreamPlayerObserver: AudioPlayerObserving {
func addTimeControlStatusObserver(
_ player: AVPlayer,
- using block: @escaping (AVPlayer.TimeControlStatus?) -> Void
+ using block: @escaping @Sendable(AVPlayer.TimeControlStatus?) -> Void
) {
timeControlStatusObserver = player.observe(
\.timeControlStatus,
@@ -98,7 +98,7 @@ final class StreamPlayerObserver: AudioPlayerObserving {
_ player: AVPlayer,
forInterval interval: CMTime,
queue: DispatchQueue?,
- using block: @escaping () -> Void
+ using block: @escaping @Sendable() -> Void
) {
periodicTimeObservationToken = player.addPeriodicTimeObserver(
forInterval: interval,
@@ -118,7 +118,7 @@ final class StreamPlayerObserver: AudioPlayerObserving {
func addStoppedPlaybackObserver(
queue: OperationQueue?,
- using block: @escaping (AVPlayerItem) -> Void
+ using block: @escaping @Sendable(AVPlayerItem) -> Void
) {
stoppedPlaybackObservationToken = notificationCenter.addObserver(
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift
index d1879d7aae6..4b8f9bf638e 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift
@@ -6,7 +6,7 @@ import AVFoundation
import Foundation
/// A protocol describing an object that can be manage the playback of an audio file or stream.
-public protocol AudioPlaying: AnyObject {
+public protocol AudioPlaying: AnyObject, Sendable {
init()
/// Subscribes the provided object on AudioPlayer's updates
@@ -41,7 +41,7 @@ public protocol AudioPlaying: AnyObject {
}
/// An implementation of ``AudioPlaying`` that can be used to stream audio files from a URL
-open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
+open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate, @unchecked Sendable {
// MARK: - Properties
/// Provides thread-safe access to context storage
@@ -131,7 +131,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
open func play() {
do {
try audioSessionConfigurator.activatePlaybackSession()
- player.play()
+ MainActor.ensureIsolated {
+ player.play()
+ }
} catch {
log.error(error)
stop()
@@ -139,7 +141,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
}
open func pause() {
- player.pause()
+ MainActor.ensureIsolated {
+ player.pause()
+ }
}
open func stop() {
@@ -166,7 +170,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
}
open func updateRate(_ newRate: AudioPlaybackRate) {
- player.rate = newRate.rawValue
+ MainActor.ensureIsolated {
+ player.rate = newRate.rawValue
+ }
}
open func seek(to time: TimeInterval) {
@@ -196,13 +202,14 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
// MARK: - Helpers
func playbackWillStop(_ playerItem: AVPlayerItem) {
- guard
- let playerItemURL = (playerItem.asset as? AVURLAsset)?.url,
- let currentItemURL = (player.currentItem?.asset as? AVURLAsset)?.url,
- playerItemURL == currentItemURL
- else {
- return
- }
+ let matchesCurrentItem: Bool = {
+ MainActor.ensureIsolated {
+ let playerItemURL = (playerItem.asset as? AVURLAsset)?.url
+ let currentItemURL = (player.currentItem?.asset as? AVURLAsset)?.url
+ return playerItemURL != nil && playerItemURL == currentItemURL
+ }
+ }()
+ guard matchesCurrentItem else { return }
updateContext { value in
value.state = .stopped
value.currentTime = 0
@@ -229,7 +236,7 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
guard let self = self, self.context.isSeeking == false else {
return
}
-
+ let rate = MainActor.ensureIsolated { player.rate }
self.updateContext { value in
let currentTime = player.currentTime().seconds
value.currentTime = currentTime.isFinite && !currentTime.isNaN
@@ -238,7 +245,7 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
value.isSeeking = false
- value.rate = .init(rawValue: player.rate)
+ value.rate = .init(rawValue: rate)
}
}
@@ -274,7 +281,9 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
}
private func notifyDelegates() {
- multicastDelegate.invoke { $0.audioPlayer(self, didUpdateContext: context) }
+ MainActor.ensureIsolated {
+ multicastDelegate.invoke { $0.audioPlayer(self, didUpdateContext: context) }
+ }
}
/// Provides thread-safe updates for the player's context and makes sure to forward any updates
@@ -299,7 +308,8 @@ open class StreamAudioPlayer: AudioPlaying, AppStateObserverDelegate {
/// We are going to check if the URL requested to load, represents the currentItem that we
/// have already loaded (if any). In this case, we will try either to resume the existing playback
/// or restart it, if possible.
- if let currentItem = player.currentItem?.asset as? AVURLAsset,
+ let currentItem = MainActor.ensureIsolated { player.currentItem?.asset as? AVURLAsset }
+ if let currentItem,
asset.url == currentItem.url,
context.assetLocation == asset.url {
/// If the currentItem is paused, we want to continue the playback
diff --git a/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift b/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift
index f51c70c6a15..39dc484ba57 100644
--- a/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift
+++ b/Sources/StreamChat/Audio/AudioPlayer/AudioQueuePlayer.swift
@@ -19,7 +19,7 @@ public protocol AudioQueuePlayerDatasource: AnyObject {
) -> URL?
}
-open class StreamAudioQueuePlayer: StreamAudioPlayer {
+open class StreamAudioQueuePlayer: StreamAudioPlayer, @unchecked Sendable {
open weak var dataSource: AudioQueuePlayerDatasource?
override open func playbackWillStop(_ playerItem: AVPlayerItem) {
diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift
index 586bd9a845f..f0f4c5b82f8 100644
--- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift
+++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift
@@ -22,7 +22,7 @@ public protocol AudioRecording {
/// - Note: If the recording permission has been answered before
/// the completionHandler will be called immediately, otherwise it will be called once the user has
/// replied on the request permission prompt.
- func beginRecording(_ completionHandler: @escaping (() -> Void))
+ func beginRecording(_ completionHandler: @escaping @Sendable() -> Void)
/// Pause the currently active recording process
func pauseRecording()
@@ -37,7 +37,7 @@ public protocol AudioRecording {
// MARK: - Implementation
/// Definition of a class to handle audio recording
-open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegate, AppStateObserverDelegate {
+open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegate, AppStateObserverDelegate, @unchecked Sendable {
/// Contains the configuration properties required by the AudioRecorder
public struct Configuration {
/// The settings that will be used to create **internally** the AVAudioRecorder instances
@@ -79,7 +79,7 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat
}
/// The default Configuration that is being bused by `StreamAudioRecorder`
- public static let `default` = Configuration(
+ nonisolated(unsafe) public static let `default` = Configuration(
audioRecorderSettings: [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
@@ -198,7 +198,7 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat
multicastDelegate.add(additionalDelegate: subscriber)
}
- open func beginRecording(_ completionHandler: @escaping (() -> Void)) {
+ open func beginRecording(_ completionHandler: @escaping @Sendable() -> Void) {
do {
/// Enable recording on `AudioSession`
try audioSessionConfigurator.activateRecordingSession()
@@ -483,7 +483,7 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat
// MARK: - Error
/// An enum that acts as a namespace for various audio recording errors that might occur
-public final class AudioRecorderError: ClientError {
+public final class AudioRecorderError: ClientError, @unchecked Sendable {
/// An unknown error occurred
public static func unknown(file: StaticString = #file, line: UInt = #line) -> AudioRecorderError { .init("An unknown error occurred.", file, line) }
diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift
index 58b6e91f5b2..75752f6132a 100644
--- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift
+++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContext.swift
@@ -5,7 +5,7 @@
import Foundation
/// A struct that represents the current state of an audio recording session
-public struct AudioRecordingContext: Hashable {
+public struct AudioRecordingContext: Hashable, Sendable {
/// The current state of the audio recording session
public var state: AudioRecordingState
diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift
index 5a8330e91ed..256ce6415f3 100644
--- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift
+++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingContextAccessor.swift
@@ -5,11 +5,11 @@
import Foundation
/// Provides thread-safe access to the value's storage
-final class AudioRecordingContextAccessor {
+final class AudioRecordingContextAccessor: Sendable {
/// The queue that thread-safe access to the value's storage
- private var accessQueue: DispatchQueue
+ private let accessQueue: DispatchQueue
- private var _value: AudioRecordingContext
+ nonisolated(unsafe) private var _value: AudioRecordingContext
var value: AudioRecordingContext {
get { readValue() }
set { writeValue(newValue) }
diff --git a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift
index 9f469bb3b51..f057130ddd8 100644
--- a/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift
+++ b/Sources/StreamChat/Audio/AudioRecorder/AudioRecordingState.swift
@@ -6,7 +6,7 @@ import Foundation
/// Defines a struct named AudioRecordingState that is being used to describe the state of a recording
/// session.
-public struct AudioRecordingState: Hashable {
+public struct AudioRecordingState: Hashable, Sendable {
var rawValue: String
/// Defines a static constant property called recording of type AudioRecordingState with a raw value
diff --git a/Sources/StreamChat/Audio/AudioSessionConfiguring.swift b/Sources/StreamChat/Audio/AudioSessionConfiguring.swift
index 3c10f6f6831..3f8ef28d4fc 100644
--- a/Sources/StreamChat/Audio/AudioSessionConfiguring.swift
+++ b/Sources/StreamChat/Audio/AudioSessionConfiguring.swift
@@ -27,7 +27,7 @@ public protocol AudioSessionConfiguring {
/// with a result, call the completionHandler to continue the flow.
/// - Parameter completionHandler: The completion handler that will be called to continue the flow.
func requestRecordPermission(
- _ completionHandler: @escaping (Bool) -> Void
+ _ completionHandler: @escaping @Sendable(Bool) -> Void
)
}
@@ -46,10 +46,10 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
public func deactivatePlaybackSession() throws { /* No-op */ }
- public func requestRecordPermission(_ completionHandler: @escaping (Bool) -> Void) { completionHandler(true) }
+ public func requestRecordPermission(_ completionHandler: @escaping @Sendable(Bool) -> Void) { completionHandler(true) }
}
#else
-open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
+open class StreamAudioSessionConfigurator: AudioSessionConfiguring, @unchecked Sendable {
/// The audioSession with which the configurator will interact.
private let audioSession: AudioSessionProtocol
@@ -120,7 +120,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
/// with a response.
/// - Note: The closure's invocation will be dispatched on the MainThread.
open func requestRecordPermission(
- _ completionHandler: @escaping (Bool) -> Void
+ _ completionHandler: @escaping @Sendable(Bool) -> Void
) {
audioSession.requestRecordPermission { [weak self] in
self?.handleRecordPermissionResponse($0, completionHandler: completionHandler)
@@ -141,7 +141,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
private func handleRecordPermissionResponse(
_ permissionGranted: Bool,
- completionHandler: @escaping (Bool) -> Void
+ completionHandler: @escaping @Sendable(Bool) -> Void
) {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
@@ -178,7 +178,7 @@ open class StreamAudioSessionConfigurator: AudioSessionConfiguring {
// MARK: - Errors
-final class AudioSessionConfiguratorError: ClientError {
+final class AudioSessionConfiguratorError: ClientError, @unchecked Sendable {
/// An unknown error occurred
static func noAvailableInputsFound(
file: StaticString = #file,
diff --git a/Sources/StreamChat/ChatClient+Environment.swift b/Sources/StreamChat/ChatClient+Environment.swift
index 3fa6d723ce6..25c6c9c59ff 100644
--- a/Sources/StreamChat/ChatClient+Environment.swift
+++ b/Sources/StreamChat/ChatClient+Environment.swift
@@ -6,16 +6,24 @@ import Foundation
extension ChatClient {
/// An object containing all dependencies of `Client`
- struct Environment {
- var apiClientBuilder: (
+ struct Environment: Sendable {
+ var apiClientBuilder: @Sendable(
_ sessionConfiguration: URLSessionConfiguration,
_ requestEncoder: RequestEncoder,
_ requestDecoder: RequestDecoder,
_ attachmentDownloader: AttachmentDownloader,
_ attachmentUploader: AttachmentUploader
- ) -> APIClient = APIClient.init
+ ) -> APIClient = {
+ APIClient(
+ sessionConfiguration: $0,
+ requestEncoder: $1,
+ requestDecoder: $2,
+ attachmentDownloader: $3,
+ attachmentUploader: $4
+ )
+ }
- var webSocketClientBuilder: ((
+ var webSocketClientBuilder: (@Sendable(
_ sessionConfiguration: URLSessionConfiguration,
_ requestEncoder: RequestEncoder,
_ eventDecoder: AnyEventDecoder,
@@ -29,7 +37,7 @@ extension ChatClient {
)
}
- var databaseContainerBuilder: (
+ var databaseContainerBuilder: @Sendable(
_ kind: DatabaseContainer.Kind,
_ chatClientConfig: ChatClientConfig
) -> DatabaseContainer = {
@@ -39,19 +47,19 @@ extension ChatClient {
)
}
- var reconnectionHandlerBuilder: (_ chatClientConfig: ChatClientConfig) -> StreamTimer? = {
+ var reconnectionHandlerBuilder: @Sendable(_ chatClientConfig: ChatClientConfig) -> StreamTimer? = {
guard let reconnectionTimeout = $0.reconnectionTimeout else { return nil }
return ScheduledStreamTimer(interval: reconnectionTimeout, fireOnStart: false, repeats: false)
}
- var requestEncoderBuilder: (_ baseURL: URL, _ apiKey: APIKey) -> RequestEncoder = DefaultRequestEncoder.init
- var requestDecoderBuilder: () -> RequestDecoder = DefaultRequestDecoder.init
+ var requestEncoderBuilder: @Sendable(_ baseURL: URL, _ apiKey: APIKey) -> RequestEncoder = { DefaultRequestEncoder(baseURL: $0, apiKey: $1) }
+ var requestDecoderBuilder: @Sendable() -> RequestDecoder = { DefaultRequestDecoder() }
- var eventDecoderBuilder: () -> EventDecoder = EventDecoder.init
+ var eventDecoderBuilder: @Sendable() -> EventDecoder = { EventDecoder() }
- var notificationCenterBuilder = EventNotificationCenter.init
+ var notificationCenterBuilder: @Sendable(_ database: DatabaseContainer) -> EventNotificationCenter = { EventNotificationCenter(database: $0) }
- var internetConnection: (_ center: NotificationCenter, _ monitor: InternetConnectionMonitor) -> InternetConnection = {
+ var internetConnection: @Sendable(_ center: NotificationCenter, _ monitor: InternetConnectionMonitor) -> InternetConnection = {
InternetConnection(notificationCenter: $0, monitor: $1)
}
@@ -65,9 +73,23 @@ extension ChatClient {
var monitor: InternetConnectionMonitor?
- var connectionRepositoryBuilder = ConnectionRepository.init
+ var connectionRepositoryBuilder: @Sendable(
+ _ isClientInActiveMode: Bool,
+ _ syncRepository: SyncRepository,
+ _ webSocketClient: WebSocketClient?,
+ _ apiClient: APIClient,
+ _ timerType: Timer.Type
+ ) -> ConnectionRepository = {
+ ConnectionRepository(
+ isClientInActiveMode: $0,
+ syncRepository: $1,
+ webSocketClient: $2,
+ apiClient: $3,
+ timerType: $4
+ )
+ }
- var backgroundTaskSchedulerBuilder: () -> BackgroundTaskScheduler? = {
+ var backgroundTaskSchedulerBuilder: @Sendable() -> BackgroundTaskScheduler? = {
if Bundle.main.isAppExtension {
// No background task scheduler exists for app extensions.
return nil
@@ -85,7 +107,7 @@ extension ChatClient {
var tokenExpirationRetryStrategy: RetryStrategy = DefaultRetryStrategy()
- var connectionRecoveryHandlerBuilder: (
+ var connectionRecoveryHandlerBuilder: @Sendable(
_ webSocketClient: WebSocketClient,
_ eventNotificationCenter: EventNotificationCenter,
_ syncRepository: SyncRepository,
@@ -105,9 +127,23 @@ extension ChatClient {
)
}
- var authenticationRepositoryBuilder = AuthenticationRepository.init
+ var authenticationRepositoryBuilder: @Sendable(
+ _ apiClient: APIClient,
+ _ databaseContainer: DatabaseContainer,
+ _ connectionRepository: ConnectionRepository,
+ _ tokenExpirationRetryStrategy: RetryStrategy,
+ _ timerType: Timer.Type
+ ) -> AuthenticationRepository = {
+ AuthenticationRepository(
+ apiClient: $0,
+ databaseContainer: $1,
+ connectionRepository: $2,
+ tokenExpirationRetryStrategy: $3,
+ timerType: $4
+ )
+ }
- var syncRepositoryBuilder: (
+ var syncRepositoryBuilder: @Sendable(
_ config: ChatClientConfig,
_ offlineRequestsRepository: OfflineRequestsRepository,
_ eventNotificationCenter: EventNotificationCenter,
@@ -125,42 +161,42 @@ extension ChatClient {
)
}
- var channelRepositoryBuilder: (
+ var channelRepositoryBuilder: @Sendable(
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> ChannelRepository = {
ChannelRepository(database: $0, apiClient: $1)
}
- var pollsRepositoryBuilder: (
+ var pollsRepositoryBuilder: @Sendable(
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> PollsRepository = {
PollsRepository(database: $0, apiClient: $1)
}
- var draftMessagesRepositoryBuilder: (
+ var draftMessagesRepositoryBuilder: @Sendable(
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> DraftMessagesRepository = {
DraftMessagesRepository(database: $0, apiClient: $1)
}
- var channelListUpdaterBuilder: (
+ var channelListUpdaterBuilder: @Sendable(
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> ChannelListUpdater = {
ChannelListUpdater(database: $0, apiClient: $1)
}
- var messageRepositoryBuilder: (
+ var messageRepositoryBuilder: @Sendable(
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> MessageRepository = {
MessageRepository(database: $0, apiClient: $1)
}
- var offlineRequestsRepositoryBuilder: (
+ var offlineRequestsRepositoryBuilder: @Sendable(
_ messageRepository: MessageRepository,
_ database: DatabaseContainer,
_ apiClient: APIClient,
diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift
index 9048ea8202b..c8ecf7e1ba3 100644
--- a/Sources/StreamChat/ChatClient.swift
+++ b/Sources/StreamChat/ChatClient.swift
@@ -12,7 +12,7 @@ import Foundation
/// case requires it (i.e. more than one window with different workspaces in a Slack-like app).
///
/// - Important: When using multiple instances of `ChatClient` at the same time, it is required to use a different ``ChatClientConfig/localStorageFolderURL`` for each instance. For example, adding an additional path component to the default URL.
-public class ChatClient {
+public class ChatClient: @unchecked Sendable {
/// The `UserId` of the currently logged in user.
public var currentUserId: UserId? {
authenticationRepository.currentUserId
@@ -25,7 +25,7 @@ public class ChatClient {
/// The app configuration settings. It is automatically fetched when `connectUser` is called.
/// Can be manually refetched by calling `loadAppSettings()`.
- public private(set) var appSettings: AppSettings?
+ @Atomic public private(set) var appSettings: AppSettings?
/// The current connection status of the client.
///
@@ -46,17 +46,22 @@ public class ChatClient {
///
/// `ChatClient` initializes a set of background workers that keep observing the current state of the system and perform
/// work if needed (i.e. when a new message pending sent appears in the database, a worker tries to send it.)
- private(set) var backgroundWorkers: [Worker] = []
+ @Atomic private(set) var backgroundWorkers: [Worker] = []
/// Background worker that takes care about client connection recovery when the Internet comes back OR app transitions from background to foreground.
- private(set) var connectionRecoveryHandler: ConnectionRecoveryHandler?
+ @Atomic private(set) var connectionRecoveryHandler: ConnectionRecoveryHandler?
/// The notification center used to send and receive notifications about incoming events.
private(set) var eventNotificationCenter: EventNotificationCenter
/// The registry that contains all the attachment payloads associated with their attachment types.
/// For the meantime this is a static property to avoid breaking changes. On v5, this can be changed.
- private(set) static var attachmentTypesRegistry: [AttachmentType: AttachmentPayload.Type] = [
+ private(set) static var attachmentTypesRegistry: [AttachmentType: AttachmentPayload.Type] {
+ get { Self.queue.sync { _attachmentTypesRegistry } }
+ set { Self.queue.sync { _attachmentTypesRegistry = newValue } }
+ }
+
+ nonisolated(unsafe) private(set) static var _attachmentTypesRegistry: [AttachmentType: AttachmentPayload.Type] = [
.image: ImageAttachmentPayload.self,
.video: VideoAttachmentPayload.self,
.audio: AudioAttachmentPayload.self,
@@ -96,17 +101,27 @@ public class ChatClient {
let databaseContainer: DatabaseContainer
/// The component responsible to timeout the user connection if it takes more time than the `ChatClientConfig.reconnectionTimeout`.
- var reconnectionTimeoutHandler: StreamTimer?
+ var reconnectionTimeoutHandler: StreamTimer? {
+ get { Self.queue.sync { _reconnectionTimeoutHandler } }
+ set { Self.queue.sync { _reconnectionTimeoutHandler = newValue } }
+ }
+
+ private var _reconnectionTimeoutHandler: StreamTimer?
/// The environment object containing all dependencies of this `Client` instance.
private let environment: Environment
- @Atomic static var activeLocalStorageURLs = Set()
-
/// The default configuration of URLSession to be used for both the `APIClient` and `WebSocketClient`. It contains all
/// required header auth parameters to make a successful request.
- private var urlSessionConfiguration: URLSessionConfiguration
+ private let urlSessionConfiguration: URLSessionConfiguration
+ static var activeLocalStorageURLs: Set {
+ queue.sync { _activeLocalStorageURLs }
+ }
+
+ nonisolated(unsafe) private static var _activeLocalStorageURLs = Set()
+ private static let queue = DispatchQueue(label: "io.getstream.chat-client", target: .global())
+
/// Creates a new instance of `ChatClient`.
/// - Parameter config: The config object for the `Client`. See `ChatClientConfig` for all configuration options.
public convenience init(
@@ -229,7 +244,9 @@ public class ChatClient {
}
deinit {
- Self._activeLocalStorageURLs.mutate { $0.subtract(databaseContainer.persistentStoreDescriptions.compactMap(\.url)) }
+ Self.queue.sync {
+ Self._activeLocalStorageURLs.subtract(databaseContainer.persistentStoreDescriptions.compactMap(\.url))
+ }
completeConnectionIdWaiters(connectionId: nil)
completeTokenWaiters(token: nil)
reconnectionTimeoutHandler?.stop()
@@ -266,10 +283,10 @@ public class ChatClient {
}
private func validateIntegrity() {
- Self._activeLocalStorageURLs.mutate { urls in
- let existingCount = urls.count
- urls.formUnion(databaseContainer.persistentStoreDescriptions.compactMap(\.url).filter { $0.path != "/dev/null" })
- guard existingCount == urls.count, !urls.isEmpty else { return }
+ Self.queue.async { [databaseContainer] in
+ let existingCount = Self._activeLocalStorageURLs.count
+ Self._activeLocalStorageURLs.formUnion(databaseContainer.persistentStoreDescriptions.compactMap(\.url).filter { $0.path != "/dev/null" })
+ guard existingCount == Self._activeLocalStorageURLs.count, !Self._activeLocalStorageURLs.isEmpty else { return }
log.error(
"""
There are multiple ChatClient instances using the same `ChatClientConfig.localStorageFolderURL` - this is disallowed.
@@ -304,7 +321,7 @@ public class ChatClient {
public func connectUser(
userInfo: UserInfo,
tokenProvider: @escaping TokenProvider,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
reconnectionTimeoutHandler?.start()
connectionRecoveryHandler?.start()
@@ -353,7 +370,7 @@ public class ChatClient {
public func connectUser(
userInfo: UserInfo,
token: Token,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
guard token.expiration == nil else {
let error = ClientError.MissingTokenProvider()
@@ -399,7 +416,7 @@ public class ChatClient {
/// - completion: The completion that will be called once the **first** user session for the given token is setup.
public func connectGuestUser(
userInfo: UserInfo,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
connectionRepository.initialize()
connectionRecoveryHandler?.start()
@@ -426,7 +443,7 @@ public class ChatClient {
/// Connects an anonymous user
/// - Parameter completion: The completion that will be called once the **first** user session for the given token is setup.
- public func connectAnonymousUser(completion: ((Error?) -> Void)? = nil) {
+ public func connectAnonymousUser(completion: (@Sendable(Error?) -> Void)? = nil) {
connectionRepository.initialize()
reconnectionTimeoutHandler?.start()
connectionRecoveryHandler?.start()
@@ -464,7 +481,7 @@ public class ChatClient {
/// Disconnects the chat client from the chat servers. No further updates from the servers
/// are received.
- public func disconnect(completion: @escaping () -> Void) {
+ public func disconnect(completion: @escaping @Sendable() -> Void) {
connectionRepository.disconnect(source: .userInitiated) {
log.info("The `ChatClient` has been disconnected.", subsystems: .webSocket)
completion()
@@ -587,7 +604,7 @@ public class ChatClient {
/// Fetches the app settings and updates the ``ChatClient/appSettings``.
/// - Parameter completion: The completion block once the app settings has finished fetching.
public func loadAppSettings(
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
apiClient.request(endpoint: .appSettings()) { [weak self] result in
switch result {
@@ -646,7 +663,7 @@ public class ChatClient {
/// Starts the process to refresh the token
/// - Parameter completion: A block to be executed when the process is completed. Contains an error if something went wrong
- private func refreshToken(completion: ((Error?) -> Void)?) {
+ private func refreshToken(completion: (@Sendable(Error?) -> Void)?) {
authenticationRepository.refreshToken {
completion?($0)
}
@@ -705,11 +722,11 @@ extension ChatClient: ConnectionStateDelegate {
/// `Client` provides connection details for the `RequestEncoder`s it creates.
extension ChatClient: ConnectionDetailsProviderDelegate {
- func provideToken(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) {
+ func provideToken(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) {
authenticationRepository.provideToken(timeout: timeout, completion: completion)
}
- func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) {
+ func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) {
connectionRepository.provideConnectionId(timeout: timeout, completion: completion)
}
@@ -739,11 +756,11 @@ extension ChatClient {
}
extension ClientError {
- public final class MissingLocalStorageURL: ClientError {
+ public final class MissingLocalStorageURL: ClientError, @unchecked Sendable {
override public var localizedDescription: String { "The URL provided in ChatClientConfig is `nil`." }
}
- public final class ConnectionNotSuccessful: ClientError {
+ public final class ConnectionNotSuccessful: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"""
Connection to the API has failed.
@@ -755,7 +772,7 @@ extension ClientError {
}
}
- public final class ReconnectionTimeout: ClientError {
+ public final class ReconnectionTimeout: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"""
The reconnection process has timed out after surpassing the value from `ChatClientConfig.reconnectionTimeout`.
@@ -763,10 +780,10 @@ extension ClientError {
}
}
- public final class MissingToken: ClientError {}
- final class WaiterTimeout: ClientError {}
+ public final class MissingToken: ClientError, @unchecked Sendable {}
+ final class WaiterTimeout: ClientError, @unchecked Sendable {}
- public final class ClientIsNotInActiveMode: ClientError {
+ public final class ClientIsNotInActiveMode: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"""
ChatClient is in connectionless mode, it cannot connect to websocket.
@@ -775,7 +792,7 @@ extension ClientError {
}
}
- public final class ConnectionWasNotInitiated: ClientError {
+ public final class ConnectionWasNotInitiated: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"""
Before performing any other actions on chat client it's required to connect by using \
@@ -784,13 +801,13 @@ extension ClientError {
}
}
- public final class ClientHasBeenDeallocated: ClientError {
+ public final class ClientHasBeenDeallocated: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"ChatClient has been deallocated, make sure to keep at least one strong reference to it."
}
}
- public final class MissingTokenProvider: ClientError {
+ public final class MissingTokenProvider: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"""
Missing token refresh provider to get a new token
diff --git a/Sources/StreamChat/Config/BaseURL.swift b/Sources/StreamChat/Config/BaseURL.swift
index 728e6dd3df5..ef8633bfdff 100644
--- a/Sources/StreamChat/Config/BaseURL.swift
+++ b/Sources/StreamChat/Config/BaseURL.swift
@@ -5,7 +5,7 @@
import Foundation
/// A struct representing base URL for `ChatClient`.
-public struct BaseURL: CustomStringConvertible {
+public struct BaseURL: CustomStringConvertible, Sendable {
/// The default base URL for StreamChat service.
public static let `default` = BaseURL(urlString: "https://chat.stream-io-api.com/")!
diff --git a/Sources/StreamChat/Config/ChatClientConfig.swift b/Sources/StreamChat/Config/ChatClientConfig.swift
index be56244de93..3a9bd89628f 100644
--- a/Sources/StreamChat/Config/ChatClientConfig.swift
+++ b/Sources/StreamChat/Config/ChatClientConfig.swift
@@ -13,7 +13,7 @@ import Foundation
/// config.channel.keystrokeEventTimeout = 15
/// ```
///
-public struct ChatClientConfig {
+public struct ChatClientConfig: Sendable {
/// The `APIKey` unique for your chat app.
///
/// The API key can be obtained by registering on [our website](https://getstream.io/chat/\).
@@ -155,7 +155,7 @@ public struct ChatClientConfig {
public var maxAttachmentCountPerMessage = 30
/// Specifies the visibility of deleted messages.
- public enum DeletedMessageVisibility: String, CustomStringConvertible, CustomDebugStringConvertible {
+ public enum DeletedMessageVisibility: String, CustomStringConvertible, CustomDebugStringConvertible, Sendable {
/// All deleted messages are always hidden.
case alwaysHidden
/// Deleted message by current user are visible, other deleted messages are hidden.
@@ -222,13 +222,13 @@ extension ChatClientConfig {
extension ChatClientConfig {
/// Advanced settings for the local caching and model serialization.
- public struct LocalCaching: Equatable {
+ public struct LocalCaching: Equatable, Sendable {
/// `ChatChannel` specific local caching and model serialization settings.
public var chatChannel = ChatChannel()
}
/// `ChatChannel` specific local caching and model serialization settings.
- public struct ChatChannel: Equatable {
+ public struct ChatChannel: Equatable, Sendable {
/// Limit the max number of watchers included in `ChatChannel.lastActiveWatchers`.
public var lastActiveWatchersLimit = 100
/// Limit the max number of members included in `ChatChannel.lastActiveMembers`.
@@ -242,7 +242,7 @@ extension ChatClientConfig {
///
/// An API key can be obtained by registering on [our website](https://getstream.io/chat/trial/\).
///
-public struct APIKey: Equatable {
+public struct APIKey: Equatable, Sendable {
/// The string representation of the API key
public let apiKeyString: String
diff --git a/Sources/StreamChat/Config/StreamModelsTransformer.swift b/Sources/StreamChat/Config/StreamModelsTransformer.swift
index 90f51650f17..bcb15e3c305 100644
--- a/Sources/StreamChat/Config/StreamModelsTransformer.swift
+++ b/Sources/StreamChat/Config/StreamModelsTransformer.swift
@@ -25,7 +25,7 @@ import Foundation
/// )
/// }
/// ```
-public protocol StreamModelsTransformer {
+public protocol StreamModelsTransformer: Sendable {
/// Transforms the given `ChatChannel` model.
func transform(channel: ChatChannel) -> ChatChannel
diff --git a/Sources/StreamChat/Config/StreamRuntimeCheck.swift b/Sources/StreamChat/Config/StreamRuntimeCheck.swift
index ea8917dc7d0..187af5e28e7 100644
--- a/Sources/StreamChat/Config/StreamRuntimeCheck.swift
+++ b/Sources/StreamChat/Config/StreamRuntimeCheck.swift
@@ -8,8 +8,13 @@ public enum StreamRuntimeCheck {
/// Enables assertions thrown by the Stream SDK.
///
/// When set to false, a message will be logged on console, but the assertion will not be thrown.
- public static var assertionsEnabled = false
+ public static var assertionsEnabled: Bool {
+ get { queue.sync { _assertionsEnabled } }
+ set { queue.async { _assertionsEnabled = newValue } }
+ }
+ nonisolated(unsafe) private static var _assertionsEnabled = false
+
/// For *internal use* only
///
/// Established the maximum depth of relationships to fetch when performing a mapping
@@ -18,7 +23,12 @@ public enum StreamRuntimeCheck {
/// Relationship: Message ---> QuotedMessage ---> QuotedMessage ---X--- NIL
/// Relationship: Channel ---> Message ---> QuotedMessage ---X--- NIL
/// Depth: 0 1 2 3
- static var _backgroundMappingRelationshipsMaxDepth = 2
+ static var _backgroundMappingRelationshipsMaxDepth: Int {
+ get { queue.sync { __backgroundMappingRelationshipsMaxDepth } }
+ set { queue.async { __backgroundMappingRelationshipsMaxDepth = newValue } }
+ }
+
+ nonisolated(unsafe) private static var __backgroundMappingRelationshipsMaxDepth = 2
/// For *internal use* only
///
@@ -30,5 +40,14 @@ public enum StreamRuntimeCheck {
/// For *internal use* only
///
/// Core Data prefetches data used for creating immutable model objects (faulting is disabled).
- public static var _isDatabasePrefetchingEnabled = false
+ public static var _isDatabasePrefetchingEnabled: Bool {
+ get { queue.sync { __isDatabasePrefetchingEnabled } }
+ set { queue.async { __isDatabasePrefetchingEnabled = newValue } }
+ }
+
+ nonisolated(unsafe) private static var __isDatabasePrefetchingEnabled = false
+
+ // MARK: -
+
+ private static let queue = DispatchQueue(label: "io.getstream.stream-runtime-check", target: .global())
}
diff --git a/Sources/StreamChat/Config/Token.swift b/Sources/StreamChat/Config/Token.swift
index cc73ff0452e..11c797c9d92 100644
--- a/Sources/StreamChat/Config/Token.swift
+++ b/Sources/StreamChat/Config/Token.swift
@@ -5,7 +5,7 @@
import Foundation
/// The type is designed to store the JWT and the user it is related to.
-public struct Token: Decodable, Equatable, ExpressibleByStringLiteral {
+public struct Token: Decodable, Equatable, ExpressibleByStringLiteral, Sendable {
public let rawValue: String
public let userId: UserId
public let expiration: Date?
@@ -83,7 +83,7 @@ public extension Token {
}
extension ClientError {
- public final class InvalidToken: ClientError {}
+ public final class InvalidToken: ClientError, @unchecked Sendable {}
}
private extension String {
diff --git a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
index 396c055a51f..42b071b7377 100644
--- a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
+++ b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
@@ -11,14 +11,14 @@ import Foundation
/// getting new messages in the channel), and for quick channel mutations (like adding a member to a channel).
///
/// - Note: For an async-await alternative of the `ChatChannelController`, please check ``Chat`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatChannelController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The ChannelQuery this controller observes.
@Atomic public private(set) var channelQuery: ChannelQuery
-
+
/// The channel list query the channel is related to.
/// It's `nil` when this controller wasn't created by a `ChannelListController`
public let channelListQuery: ChannelListQuery?
-
+
/// Flag indicating whether channel is created on backend. We need this flag to restrict channel modification requests
/// before channel is created on backend.
/// There are 2 ways of creating new channel:
@@ -28,7 +28,12 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// In this case `cid` on `channelQuery `will be valid but all channel modifications will
/// fail because channel with provided `id` will be missing on backend side.
/// That is why we need to check both flag and valid `cid` before modifications.
- private var isChannelAlreadyCreated: Bool
+ private var isChannelAlreadyCreated: Bool {
+ get { queue.sync { _isChannelAlreadyCreated } }
+ set { queue.sync { _isChannelAlreadyCreated = newValue } }
+ }
+
+ private var _isChannelAlreadyCreated: Bool
/// The identifier of a channel this controller observes.
/// Will be `nil` when we want to create direct message channel and `id`
@@ -70,16 +75,9 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
private let channelMemberUpdater: ChannelMemberUpdater
- private lazy var eventSender: TypingEventsSender = self.environment.eventSenderBuilder(
- client.databaseContainer,
- client.apiClient
- )
+ private let eventSender: TypingEventsSender
- private lazy var readStateHandler: ReadStateHandler = self.environment.readStateHandlerBuilder(
- client.authenticationRepository,
- updater,
- client.messageRepository
- )
+ private let readStateHandler: ReadStateHandler
/// A Boolean value that returns whether the oldest messages have all been loaded or not.
public var hasLoadedAllPreviousMessages: Bool {
@@ -148,7 +146,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// A boolean value indicating if it should send typing events.
/// It is `true` if the channel typing events are enabled as well as the user privacy settings.
- func shouldSendTypingEvents(completion: @escaping (Bool) -> Void) {
+ func shouldSendTypingEvents(completion: @escaping @Sendable(Bool) -> Void) {
guard channel?.canSendTypingEvents ?? true else {
completion(false)
return
@@ -182,14 +180,19 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
private var channelObserver: BackgroundEntityDatabaseObserver?
private var messagesObserver: BackgroundListDatabaseObserver?
- private var eventObservers: [EventObserver] = []
+ private var eventObservers: [EventObserver] {
+ get { queue.sync { _eventObservers } }
+ set { queue.sync { _eventObservers = newValue } }
+ }
+
+ private var _eventObservers: [EventObserver] = []
private let environment: Environment
private let pollsRepository: PollsRepository
private let draftsRepository: DraftMessagesRepository
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -220,8 +223,12 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
self.channelListQuery = channelListQuery
self.client = client
self.environment = environment
- self.isChannelAlreadyCreated = isChannelAlreadyCreated
+ _isChannelAlreadyCreated = isChannelAlreadyCreated
self.messageOrdering = messageOrdering
+ eventSender = environment.eventSenderBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
updater = self.environment.channelUpdaterBuilder(
client.channelRepository,
client.messageRepository,
@@ -229,6 +236,11 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
client.databaseContainer,
client.apiClient
)
+ readStateHandler = environment.readStateHandlerBuilder(
+ client.authenticationRepository,
+ updater,
+ client.messageRepository
+ )
channelMemberUpdater = self.environment.memberUpdaterBuilder(
client.databaseContainer,
client.apiClient
@@ -242,7 +254,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
setMessagesObserver()
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
client.syncRepository.startTrackingChannelController(self)
synchronize(isInRecoveryMode: false, completion)
}
@@ -266,7 +278,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
members: Set = [],
invites: Set = [],
extraData: [String: RawJSON] = [:],
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
@@ -310,7 +322,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
invites: Set = [],
extraData: [String: RawJSON] = [:],
unsetProperties: [String] = [],
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
@@ -342,7 +354,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func muteChannel(expiration: Int? = nil, completion: ((Error?) -> Void)? = nil) {
+ public func muteChannel(expiration: Int? = nil, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -362,7 +374,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func unmuteChannel(completion: ((Error?) -> Void)? = nil) {
+ public func unmuteChannel(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -385,7 +397,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - scope: The scope of the archiving action. Default is archiving for the current user only.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- public func archive(scope: ChannelArchivingScope = .me, completion: ((Error?) -> Void)? = nil) {
+ public func archive(scope: ChannelArchivingScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else {
channelModificationFailed(completion)
return
@@ -406,7 +418,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - scope: The scope of the unarchiving action. The default scope is unarchived only for me.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- public func unarchive(scope: ChannelArchivingScope = .me, completion: ((Error?) -> Void)? = nil) {
+ public func unarchive(scope: ChannelArchivingScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else {
channelModificationFailed(completion)
return
@@ -425,7 +437,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- public func deleteChannel(completion: ((Error?) -> Void)? = nil) {
+ public func deleteChannel(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -454,7 +466,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
skipPush: Bool = false,
hardDelete: Bool = true,
systemMessage: String? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
@@ -481,7 +493,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func hideChannel(clearHistory: Bool = false, completion: ((Error?) -> Void)? = nil) {
+ public func hideChannel(clearHistory: Bool = false, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -500,7 +512,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func showChannel(completion: ((Error?) -> Void)? = nil) {
+ public func showChannel(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -525,7 +537,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
public func loadPreviousMessages(
before messageId: MessageId? = nil,
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard cid != nil, isChannelAlreadyCreated else {
@@ -576,7 +588,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
public func loadNextMessages(
after messageId: MessageId? = nil,
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard cid != nil, isChannelAlreadyCreated else {
@@ -623,7 +635,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - messageId: The message id of the message to jump to.
/// - limit: The number of messages to load in total, including the message to jump to.
/// - completion: Callback when the API call is completed.
- public func loadPageAroundMessageId(_ messageId: MessageId, limit: Int? = nil, completion: ((Error?) -> Void)? = nil) {
+ public func loadPageAroundMessageId(_ messageId: MessageId, limit: Int? = nil, completion: (@Sendable(Error?) -> Void)? = nil) {
guard isChannelAlreadyCreated else {
channelModificationFailed(completion)
return
@@ -653,7 +665,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// Cleans the current state and loads the first page again.
/// - Parameter completion: Callback when the API call is completed.
- public func loadFirstPage(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ public func loadFirstPage(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
channelQuery.pagination = .init(
pageSize: channelQuery.pagination?.pageSize ?? .messagesPageSize,
parameter: nil
@@ -668,7 +680,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
///
/// - Parameter completion: a completion block with an error if the request was failed.
///
- public func sendKeystrokeEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) {
+ public func sendKeystrokeEvent(parentMessageId: MessageId? = nil, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion?($0) }
@@ -694,7 +706,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
///
/// - Parameter completion: a completion block with an error if the request was failed.
///
- public func sendStartTypingEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) {
+ public func sendStartTypingEvent(parentMessageId: MessageId? = nil, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion?($0) }
@@ -720,7 +732,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
///
/// - Parameter completion: a completion block with an error if the request was failed.
///
- public func sendStopTypingEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) {
+ public func sendStopTypingEvent(parentMessageId: MessageId? = nil, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion?($0) }
@@ -766,7 +778,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
skipEnrichUrl: Bool = false,
restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:],
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
var transformableInfo = NewMessageTransformableInfo(
text: text,
@@ -807,7 +819,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
messageId: MessageId? = nil,
restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:],
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { error in
@@ -864,7 +876,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
quotedMessageId: MessageId? = nil,
command: Command? = nil,
extraData: [String: RawJSON] = [:],
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { error in
@@ -896,7 +908,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
///
/// It is not necessary to call this method if the channel list query was called before.
public func loadDraftMessage(
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { error in
@@ -913,7 +925,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
}
/// Deletes the draft message of this channel.
- public func deleteDraftMessage(completion: ((Error?) -> Void)? = nil) {
+ public func deleteDraftMessage(completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { error in
completion?(error)
@@ -951,7 +963,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
votingVisibility: VotingVisibility? = nil,
options: [PollOption]? = nil,
extraData: [String: RawJSON]? = nil,
- completion: ((Result) -> Void)?
+ completion: (@Sendable(Result) -> Void)?
) {
pollsRepository.createPoll(
name: name,
@@ -984,7 +996,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - pollId: The id of the poll to be deleted.
/// - completion: A closure to be executed once the poll is deleted, returning either an `Error` on failure or `nil` on success.
- public func deletePoll(pollId: String, completion: ((Error?) -> Void)? = nil) {
+ public func deletePoll(pollId: String, completion: (@Sendable(Error?) -> Void)? = nil) {
pollsRepository.deletePoll(pollId: pollId) { [weak self] error in
self?.callback {
completion?(error)
@@ -1005,7 +1017,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
_ members: [MemberInfo],
hideHistory: Bool = false,
message: String? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
@@ -1038,7 +1050,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
userIds: Set,
hideHistory: Bool = false,
message: String? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
addMembers(
userIds.map { .init(userId: $0, extraData: nil) },
@@ -1059,7 +1071,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
public func removeMembers(
userIds: Set,
message: String? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
@@ -1083,7 +1095,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - userIds: Set of ids of users to be invited to the channel
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func inviteMembers(userIds: Set, completion: ((Error?) -> Void)? = nil) {
+ public func inviteMembers(userIds: Set, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1103,7 +1115,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - userId: userId
/// - message: message
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func acceptInvite(message: String? = nil, completion: ((Error?) -> Void)? = nil) {
+ public func acceptInvite(message: String? = nil, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1120,7 +1132,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - cid: The channel identifier.
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func rejectInvite(completion: ((Error?) -> Void)? = nil) {
+ public func rejectInvite(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1139,7 +1151,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: The completion will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func markRead(completion: ((Error?) -> Void)? = nil) {
+ public func markRead(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let channel = channel else {
channelModificationFailed(completion)
@@ -1164,7 +1176,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - messageId: The id of the first message id that will be marked as unread.
/// - completion: The completion will be called on a **callbackQueue** when the network request is finished.
- public func markUnread(from messageId: MessageId, completion: ((Result) -> Void)? = nil) {
+ public func markUnread(from messageId: MessageId, completion: (@Sendable(Result) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let channel = channel else {
let error = ClientError.ChannelNotCreatedYet()
@@ -1204,7 +1216,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - completion: The completion to be called on **callbackQueue** when request is completed.
public func loadChannelReads(
pagination: Pagination? = nil,
- completion: @escaping (Error?) -> Void
+ completion: @escaping @Sendable(Error?) -> Void
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion($0 ?? ClientError.ChannelNotCreatedYet()) }
@@ -1228,7 +1240,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - limit: The number of channel reads to load. The default is 100.
/// - completion: The completion to be called on **callbackQueue** when request is completed.
- public func loadMoreChannelReads(limit: Int? = nil, completion: @escaping (Error?) -> Void) {
+ public func loadMoreChannelReads(limit: Int? = nil, completion: @escaping @Sendable(Error?) -> Void) {
let pagination = Pagination(pageSize: limit ?? 100, offset: channel?.reads.count ?? 0)
loadChannelReads(pagination: pagination, completion: completion)
}
@@ -1243,7 +1255,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - cooldownDuration: Duration of the time interval users have to wait between messages.
/// Specified in seconds. Should be between 1-120.
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func enableSlowMode(cooldownDuration: Int, completion: ((Error?) -> Void)? = nil) {
+ public func enableSlowMode(cooldownDuration: Int, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1268,7 +1280,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
///
/// - Parameters:
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func disableSlowMode(completion: ((Error?) -> Void)? = nil) {
+ public func disableSlowMode(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1291,7 +1303,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
///
///
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func startWatching(isInRecoveryMode: Bool, completion: ((Error?) -> Void)? = nil) {
+ public func startWatching(isInRecoveryMode: Bool, completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1325,7 +1337,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// such as updating channel data.
///
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
- public func stopWatching(completion: ((Error?) -> Void)? = nil) {
+ public func stopWatching(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1350,7 +1362,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func freezeChannel(completion: ((Error?) -> Void)? = nil) {
+ public func freezeChannel(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1372,7 +1384,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func unfreezeChannel(completion: ((Error?) -> Void)? = nil) {
+ public func unfreezeChannel(completion: (@Sendable(Error?) -> Void)? = nil) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
@@ -1395,7 +1407,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - scope: The scope of the pinning action. Default is pinning for the current user only.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- public func pin(scope: ChannelPinningScope = .me, completion: ((Error?) -> Void)? = nil) {
+ public func pin(scope: ChannelPinningScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else {
channelModificationFailed(completion)
return
@@ -1416,7 +1428,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - scope: The scope of the unpinning action. The default scope is unpinned only for me.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- public func unpin(scope: ChannelPinningScope = .me, completion: ((Error?) -> Void)? = nil) {
+ public func unpin(scope: ChannelPinningScope = .me, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid, isChannelAlreadyCreated, let userId = client.currentUserId else {
channelModificationFailed(completion)
return
@@ -1440,8 +1452,8 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
public func uploadAttachment(
localFileURL: URL,
type: AttachmentType,
- progress: ((Double) -> Void)? = nil,
- completion: @escaping ((Result) -> Void)
+ progress: (@Sendable(Double) -> Void)? = nil,
+ completion: @escaping @Sendable(Result) -> Void
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { error in
@@ -1460,7 +1472,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// Get the link attachment preview data from the provided url.
///
/// This will return the data present in the OG Metadata.
- public func enrichUrl(_ url: URL, completion: @escaping (Result) -> Void) {
+ public func enrichUrl(_ url: URL, completion: @escaping @Sendable(Result) -> Void) {
updater.enrichUrl(url) { result in
self.callback {
completion(result)
@@ -1479,7 +1491,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
pageSize: Int = .messagesPageSize,
sorting: [Sorting] = [],
pagination: PinnedMessagesPagination? = nil,
- completion: @escaping (Result<[ChatMessage], Error>) -> Void
+ completion: @escaping @Sendable(Result<[ChatMessage], Error>) -> Void
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion(.failure($0 ?? ClientError.ChannelNotCreatedYet())) }
@@ -1524,7 +1536,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - url: The URL of the file to be deleted.
/// - completion: An optional closure to be called when the delete operation is complete.
/// If an error occurs during deletion, the error will be passed to this closure.
- public func deleteFile(url: String, completion: ((Error?) -> Void)? = nil) {
+ public func deleteFile(url: String, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
return
@@ -1538,7 +1550,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - url: The URL of the image to be deleted.
/// - completion: An optional closure to be called when the delete operation is complete.
/// If an error occurs during deletion, the error will be passed to this closure.
- public func deleteImage(url: String, completion: ((Error?) -> Void)? = nil) {
+ public func deleteImage(url: String, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed(completion)
return
@@ -1558,7 +1570,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
// MARK: - Internal
- func recoverWatchedChannel(recovery: Bool, completion: @escaping (Error?) -> Void) {
+ func recoverWatchedChannel(recovery: Bool, completion: @escaping @Sendable(Error?) -> Void) {
if cid != nil, isChannelAlreadyCreated {
startWatching(isInRecoveryMode: recovery, completion: completion)
} else {
@@ -1579,7 +1591,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
restrictedVisibility: [UserId] = [],
extraData: [String: RawJSON] = [:],
poll: PollPayload?,
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
@@ -1659,7 +1671,7 @@ extension ChatChannelController {
}
/// Describes the flow of the messages in the list
-public enum MessageOrdering {
+public enum MessageOrdering: Sendable {
/// New messages appears on the top of the list.
case topToBottom
@@ -1670,7 +1682,7 @@ public enum MessageOrdering {
// MARK: - Helpers
private extension ChatChannelController {
- func synchronize(isInRecoveryMode: Bool, _ completion: ((_ error: Error?) -> Void)? = nil) {
+ func synchronize(isInRecoveryMode: Bool, _ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
let channelCreatedCallback = isChannelAlreadyCreated ? nil : channelCreated(forwardErrorTo: setLocalStateBasedOnError)
updater.update(
channelQuery: channelQuery,
@@ -1845,7 +1857,7 @@ private extension ChatChannelController {
itemReuseKeyPaths: (\ChatMessage.id, \MessageDTO.id)
)
observer.onDidChange = { [weak self] changes in
- self?.delegateCallback {
+ self?.delegateCallback { [weak self] in
guard let self = self else { return }
log.debug("didUpdateMessages: \(changes.map(\.debugDescription))")
@@ -1858,7 +1870,7 @@ private extension ChatChannelController {
/// A convenience method that invokes the completion? with a ChannelFeatureDisabled error
/// ie. VCs should use the `are{FEATURE_NAME}Enabled` props (ie. `areReadEventsEnabled`) before using any feature
- private func channelFeatureDisabled(feature: String, completion: ((Error?) -> Void)?) {
+ private func channelFeatureDisabled(feature: String, completion: (@Sendable(Error?) -> Void)?) {
let error = ClientError.ChannelFeatureDisabled("Channel feature: \(feature) is disabled for this channel.")
log.error(error.localizedDescription)
callback {
@@ -1868,7 +1880,7 @@ private extension ChatChannelController {
// It's impossible to perform any channel modification before it's creation on backend.
// So before any modification attempt we need to check if channel is already created and call this function if not.
- private func channelModificationFailed(_ completion: ((Error?) -> Void)?) {
+ private func channelModificationFailed(_ completion: (@Sendable(Error?) -> Void)?) {
let error = ClientError.ChannelNotCreatedYet()
log.error(error.localizedDescription)
callback {
@@ -1879,7 +1891,7 @@ private extension ChatChannelController {
/// This callback is called after channel is created on backend but before channel is saved to DB. When channel is created
/// we receive backend generated cid and setting up current `ChannelController` to observe this channel DB changes.
/// Completion will be called if DB fetch will fail after setting new `ChannelQuery`.
- private func channelCreated(forwardErrorTo completion: ((_ error: Error?) -> Void)?) -> ((ChannelId) -> Void) {
+ private func channelCreated(forwardErrorTo completion: (@Sendable(_ error: Error?) -> Void)?) -> (@Sendable(ChannelId) -> Void) {
return { [weak self] cid in
guard let self = self else { return }
self.isChannelAlreadyCreated = true
@@ -1888,7 +1900,7 @@ private extension ChatChannelController {
}
/// Helper for updating state after fetching local data.
- private var setLocalStateBasedOnError: ((_ error: Error?) -> Void) {
+ private var setLocalStateBasedOnError: (@Sendable(_ error: Error?) -> Void) {
return { [weak self] error in
// Update observing state
self?.state = error == nil ? .localDataFetched : .localDataFetchFailed(ClientError(with: error))
@@ -1917,25 +1929,25 @@ private extension ChatChannelController {
// MARK: - Errors
extension ClientError {
- final class ChannelNotCreatedYet: ClientError {
+ final class ChannelNotCreatedYet: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"You can't modify the channel because the channel hasn't been created yet. Call `synchronize()` to create the channel and wait for the completion block to finish. Alternatively, you can observe the `state` changes of the controller and wait for the `remoteDataFetched` state."
}
}
- final class ChannelEmptyMembers: ClientError {
+ final class ChannelEmptyMembers: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"You can't create direct messaging channel with empty members."
}
}
- final class ChannelEmptyMessages: ClientError {
+ final class ChannelEmptyMessages: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"You can't load new messages when there is no messages in the channel."
}
}
- final class InvalidCooldownDuration: ClientError {
+ final class InvalidCooldownDuration: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"You can't specify a value outside the range 1-120 for cooldown duration."
}
@@ -1943,7 +1955,7 @@ extension ClientError {
}
extension ClientError {
- final class ChannelFeatureDisabled: ClientError {}
+ final class ChannelFeatureDisabled: ClientError, @unchecked Sendable {}
}
// MARK: - Deprecations
@@ -1995,8 +2007,8 @@ public extension ChatChannelController {
@available(*, deprecated, message: "use uploadAttachment() instead.")
func uploadFile(
localFileURL: URL,
- progress: ((Double) -> Void)? = nil,
- completion: @escaping ((Result) -> Void)
+ progress: (@Sendable(Double) -> Void)? = nil,
+ completion: @escaping @Sendable(Result) -> Void
) {
uploadAttachment(localFileURL: localFileURL, type: .file, progress: progress) { result in
completion(result.map(\.remoteURL))
@@ -2011,8 +2023,8 @@ public extension ChatChannelController {
@available(*, deprecated, message: "use uploadAttachment() instead.")
func uploadImage(
localFileURL: URL,
- progress: ((Double) -> Void)? = nil,
- completion: @escaping ((Result) -> Void)
+ progress: (@Sendable(Double) -> Void)? = nil,
+ completion: @escaping @Sendable(Result) -> Void
) {
uploadAttachment(localFileURL: localFileURL, type: .image, progress: progress) { result in
completion(result.map(\.remoteURL))
diff --git a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
index ca743167295..1d2a3131f54 100644
--- a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
+++ b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
@@ -24,7 +24,7 @@ extension ChatClient {
/// - Returns: A new instance of `ChatChannelListController`
public func channelListController(
query: ChannelListQuery,
- filter: ((ChatChannel) -> Bool)? = nil
+ filter: (@Sendable(ChatChannel) -> Bool)? = nil
) -> ChatChannelListController {
.init(query: query, client: self, filter: filter)
}
@@ -33,7 +33,7 @@ extension ChatClient {
/// `ChatChannelListController` is a controller class which allows observing a list of chat channels based on the provided query.
///
/// - Note: For an async-await alternative of the `ChatChannelListController`, please check ``ChannelList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying and filtering the list of channels.
public let query: ChannelListQuery
@@ -51,14 +51,15 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
}
/// The worker used to fetch the remote data and communicate with servers.
- private lazy var worker: ChannelListUpdater = self.environment
- .channelQueryUpdaterBuilder(
- client.databaseContainer,
- client.apiClient
- )
+ private let worker: ChannelListUpdater
/// A Boolean value that returns whether pagination is finished
- public private(set) var hasLoadedAllPreviousChannels: Bool = false
+ public private(set) var hasLoadedAllPreviousChannels: Bool {
+ get { queue.sync { _hasLoadedAllPreviousChannels } }
+ set { queue.sync { _hasLoadedAllPreviousChannels = newValue } }
+ }
+
+ private var _hasLoadedAllPreviousChannels: Bool = false
/// A type-erased delegate.
var multicastDelegate: MulticastDelegate = .init() {
@@ -105,7 +106,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
return observer
}()
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -119,10 +120,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
private let filter: ((ChatChannel) -> Bool)?
private let environment: Environment
- private lazy var channelListLinker: ChannelListLinker = self.environment
- .channelListLinkerBuilder(
- query, filter, client.config, client.databaseContainer, worker
- )
+ private let channelListLinker: ChannelListLinker
/// Creates a new `ChannelListController`.
///
@@ -133,17 +131,24 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
init(
query: ChannelListQuery,
client: ChatClient,
- filter: ((ChatChannel) -> Bool)? = nil,
+ filter: (@Sendable(ChatChannel) -> Bool)? = nil,
environment: Environment = .init()
) {
self.client = client
self.query = query
self.filter = filter
self.environment = environment
+ worker = environment.channelQueryUpdaterBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
+ channelListLinker = environment.channelListLinkerBuilder(
+ query, filter, client.config, client.databaseContainer, worker
+ )
super.init()
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startChannelListObserverIfNeeded()
channelListLinker.start(with: client.eventNotificationCenter)
client.syncRepository.startTrackingChannelListController(self)
@@ -161,7 +166,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
///
public func loadNextChannels(
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
if hasLoadedAllPreviousChannels {
completion?(nil)
@@ -183,7 +188,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
}
@available(*, deprecated, message: "Please use `markAllRead` available in `CurrentChatUserController`")
- public func markAllRead(completion: ((Error?) -> Void)? = nil) {
+ public func markAllRead(completion: (@Sendable(Error?) -> Void)? = nil) {
worker.markAllRead { error in
self.callback {
completion?(error)
@@ -193,7 +198,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
// MARK: - Internal
- func refreshLoadedChannels(completion: @escaping (Result, Error>) -> Void) {
+ func refreshLoadedChannels(completion: @escaping @Sendable(Result, Error>) -> Void) {
let channelCount = channelListObserver.items.count
worker.refreshLoadedChannels(for: query, channelCount: channelCount, completion: completion)
}
@@ -201,7 +206,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
// MARK: - Helpers
private func updateChannelList(
- _ completion: ((_ error: Error?) -> Void)? = nil
+ _ completion: (@Sendable(_ error: Error?) -> Void)? = nil
) {
let limit = query.pagination.pageSize
worker.update(
@@ -246,7 +251,7 @@ extension ChatChannelListController {
var channelListLinkerBuilder: (
_ query: ChannelListQuery,
- _ filter: ((ChatChannel) -> Bool)?,
+ _ filter: (@Sendable(ChatChannel) -> Bool)?,
_ clientConfig: ChatClientConfig,
_ databaseContainer: DatabaseContainer,
_ worker: ChannelListUpdater
diff --git a/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift b/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift
index 6a631a71ceb..3abd92697fe 100644
--- a/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift
+++ b/Sources/StreamChat/Controllers/ChannelWatcherListController/ChatChannelWatcherListController.swift
@@ -17,7 +17,7 @@ extension ChatClient {
/// `ChatChannelWatcherListController` is a controller class which allows observing
/// a list of chat watchers based on the provided query.
///
-public class ChatChannelWatcherListController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatChannelWatcherListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying sorting and filtering for the list of channel watchers.
@Atomic public private(set) var query: ChannelWatcherListQuery
@@ -32,7 +32,7 @@ public class ChatChannelWatcherListController: DataController, DelegateCallable,
return watchersObserver.items
}
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -58,13 +58,7 @@ public class ChatChannelWatcherListController: DataController, DelegateCallable,
private lazy var watchersObserver: BackgroundListDatabaseObserver = createWatchersObserver()
/// The worker used to fetch the remote data and communicate with servers.
- private lazy var updater: ChannelUpdater = self.environment.channelUpdaterBuilder(
- client.channelRepository,
- client.messageRepository,
- client.makeMessagesPaginationStateHandler(),
- client.databaseContainer,
- client.apiClient
- )
+ private let updater: ChannelUpdater
private let environment: Environment
@@ -77,12 +71,19 @@ public class ChatChannelWatcherListController: DataController, DelegateCallable,
self.client = client
self.query = query
self.environment = environment
+ updater = environment.channelUpdaterBuilder(
+ client.channelRepository,
+ client.messageRepository,
+ client.makeMessagesPaginationStateHandler(),
+ client.databaseContainer,
+ client.apiClient
+ )
}
/// Synchronizes the channel's watchers with the backend.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObservingIfNeeded()
if case let .localDataFetchFailed(error) = state {
@@ -174,10 +175,10 @@ public extension ChatChannelWatcherListController {
/// - limit: Limit for page size. Offset is defined automatically by the controller.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- func loadNextWatchers(limit: Int = .channelWatchersPageSize, completion: ((Error?) -> Void)? = nil) {
+ func loadNextWatchers(limit: Int = .channelWatchersPageSize, completion: (@Sendable(Error?) -> Void)? = nil) {
var updatedQuery = query
updatedQuery.pagination = .init(pageSize: limit, offset: watchers.count)
- updater.channelWatchers(query: updatedQuery) { result in
+ updater.channelWatchers(query: updatedQuery) { [updatedQuery] result in
self.query = updatedQuery
self.callback { completion?(result.error) }
}
diff --git a/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift b/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift
index d954faea8cb..56c3aec140d 100644
--- a/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift
+++ b/Sources/StreamChat/Controllers/ConnectionController/ConnectionController.swift
@@ -17,10 +17,15 @@ public extension ChatClient {
/// `ChatConnectionController` is a controller class which allows to explicitly
/// connect/disconnect the `ChatClient` and observe connection events.
-public class ChatConnectionController: Controller, DelegateCallable, DataStoreProvider {
- public var callbackQueue: DispatchQueue = .main
-
- var _basePublishers: Any?
+public class ChatConnectionController: Controller, DelegateCallable, DataStoreProvider, @unchecked Sendable {
+ public var callbackQueue: DispatchQueue {
+ get { queue.sync { _callbackQueue } }
+ set { queue.sync { _callbackQueue = newValue } }
+ }
+
+ private let queue = DispatchQueue(label: "io.getstream.chat-connection-controller", target: .global(qos: .userInteractive))
+ private var _callbackQueue: DispatchQueue = .main
+ @Atomic private 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.
@@ -42,7 +47,7 @@ public class ChatConnectionController: Controller, DelegateCallable, DataStorePr
}
/// The connection event observer for the connection status updates.
- private var connectionEventObserver: ConnectionEventObserver?
+ private var _connectionEventObserver: ConnectionEventObserver?
/// A type-erased delegate.
var multicastDelegate: MulticastDelegate = .init()
@@ -63,7 +68,7 @@ public class ChatConnectionController: Controller, DelegateCallable, DataStorePr
self.connectionRepository = connectionRepository
self.webSocketClient = webSocketClient
self.client = client
- connectionEventObserver = setupObserver()
+ _connectionEventObserver = setupObserver()
}
private func setupObserver() -> ConnectionEventObserver? {
@@ -91,7 +96,7 @@ public extension ChatConnectionController {
/// - Parameter completion: Called when the connection is established. If the connection fails, the completion is
/// called with an error.
///
- func connect(completion: ((Error?) -> Void)? = nil) {
+ func connect(completion: (@Sendable(Error?) -> Void)? = nil) {
connectionRepository.connect { [weak self] error in
self?.callback {
completion?(error)
@@ -132,8 +137,8 @@ public extension ChatConnectionController {
private class ConnectionEventObserver: EventObserver {
init(
notificationCenter: NotificationCenter,
- filter: ((ConnectionStatusUpdated) -> Bool)? = nil,
- callback: @escaping (ConnectionStatusUpdated) -> Void
+ filter: (@Sendable(ConnectionStatusUpdated) -> Bool)? = nil,
+ callback: @escaping @Sendable(ConnectionStatusUpdated) -> Void
) {
super.init(notificationCenter: notificationCenter, transform: { $0 as? ConnectionStatusUpdated }) {
guard filter == nil || filter?($0) == true else { return }
diff --git a/Sources/StreamChat/Controllers/Controller.swift b/Sources/StreamChat/Controllers/Controller.swift
index 062b694c20d..79c95ea0cd2 100644
--- a/Sources/StreamChat/Controllers/Controller.swift
+++ b/Sources/StreamChat/Controllers/Controller.swift
@@ -15,7 +15,7 @@ public protocol Controller {
extension Controller {
/// A helper function to ensure the callback is performed on the callback queue.
- func callback(_ action: @escaping () -> Void) {
+ func callback(_ action: @escaping @Sendable() -> Void) {
// We perform the callback synchronously if we're on main & `callbackQueue` is on main, too.
if Thread.current.isMainThread && callbackQueue == .main {
action()
diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
index 83ec7c2b45c..40385a8a074 100644
--- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
+++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
@@ -19,13 +19,13 @@ public extension ChatClient {
/// user of `ChatClient`.
///
/// - Note: For an async-await alternative of the `CurrentChatUserController`, please check ``ConnectedUser`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class CurrentChatUserController: DataController, DelegateCallable, DataStoreProvider {
+public class CurrentChatUserController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
-
+
private let environment: Environment
-
- var _basePublishers: Any?
+
+ @Atomic private 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.
@@ -36,7 +36,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
_basePublishers = BasePublishers(controller: self)
return _basePublishers as? BasePublishers ?? .init(controller: self)
}
-
+
/// Used for observing the current user changes in a database.
private lazy var currentUserObserver = createUserObserver()
.onChange { [weak self] change in
@@ -57,7 +57,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
$0.currentUserController(self, didChangeCurrentUserUnreadCount: change.unreadCount)
}
}
-
+
/// A type-erased delegate.
var multicastDelegate: MulticastDelegate = .init()
@@ -68,37 +68,54 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
startObservingIfNeeded()
return currentUserObserver.item
}
-
+
/// The unread messages and channels count for the current user.
///
/// Returns `noUnread` if `currentUser` doesn't exist yet.
public var unreadCount: UnreadCount {
currentUser?.unreadCount ?? .noUnread
}
-
+
/// The worker used to update the current user.
- private lazy var currentUserUpdater = environment.currentUserUpdaterBuilder(
- client.databaseContainer,
- client.apiClient
- )
-
+ private let currentUserUpdater: CurrentUserUpdater
+
/// The worker used to update the current user member for a given channel.
- private lazy var currentMemberUpdater = createMemberUpdater()
-
+ private let currentMemberUpdater: ChannelMemberUpdater
+
/// The query used for fetching the draft messages.
- private var draftListQuery = DraftListQuery()
+ private var draftListQuery: DraftListQuery {
+ get { queue.sync { _draftListQuery } }
+ set { queue.sync { _draftListQuery = newValue } }
+ }
+ private var _draftListQuery = DraftListQuery()
+
/// Use for observing the current user's draft messages changes.
- private var draftMessagesObserver: BackgroundListDatabaseObserver?
+ private var draftMessagesObserver: BackgroundListDatabaseObserver? {
+ get { queue.sync { _draftMessagesObserver } }
+ set { queue.sync { _draftMessagesObserver = newValue } }
+ }
+ private var _draftMessagesObserver: BackgroundListDatabaseObserver?
+
/// The repository for draft messages.
- private var draftMessagesRepository: DraftMessagesRepository
-
+ private let draftMessagesRepository: DraftMessagesRepository
+
/// The token for the next page of draft messages.
- private var draftMessagesNextCursor: String?
+ private var draftMessagesNextCursor: String? {
+ get { queue.sync { _draftMessagesNextCursor } }
+ set { queue.sync { _draftMessagesNextCursor = newValue } }
+ }
+ private var _draftMessagesNextCursor: String?
+
/// A flag to indicate whether all draft messages have been loaded.
- public private(set) var hasLoadedAllDrafts: Bool = false
+ public private(set) var hasLoadedAllDrafts: Bool {
+ get { queue.sync { _hasLoadedAllDrafts } }
+ set { queue.sync { _hasLoadedAllDrafts = newValue } }
+ }
+
+ private var _hasLoadedAllDrafts: Bool = false
/// The current user's draft messages.
public var draftMessages: [DraftMessage] {
@@ -119,6 +136,14 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
init(client: ChatClient, environment: Environment = .init()) {
self.client = client
self.environment = environment
+ currentMemberUpdater = ChannelMemberUpdater(
+ database: client.databaseContainer,
+ apiClient: client.apiClient
+ )
+ currentUserUpdater = environment.currentUserUpdaterBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
draftMessagesRepository = client.draftMessagesRepository
}
@@ -127,7 +152,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
///
/// - Parameter completion: Called when the controller has finished fetching the local data
/// and the client connection is established.
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObservingIfNeeded()
if case let .localDataFetchFailed(error) = state {
@@ -145,7 +170,7 @@ public class CurrentChatUserController: DataController, DelegateCallable, DataSt
}
self?.state = error == nil ? .remoteDataFetched : .remoteDataFetchFailed(error!)
- self?.callback { completion?(error) }
+ self?.callback { [error] in completion?(error) }
}
}
@@ -173,7 +198,7 @@ public extension CurrentChatUserController {
/// database will be flushed.
///
/// - Parameter completion: The completion to be called when the operation is completed.
- func reloadUserIfNeeded(completion: ((Error?) -> Void)? = nil) {
+ func reloadUserIfNeeded(completion: (@Sendable(Error?) -> Void)? = nil) {
client.authenticationRepository.refreshToken { error in
self.callback {
completion?(error)
@@ -204,7 +229,7 @@ public extension CurrentChatUserController {
teamsRole: [TeamId: UserRole]? = nil,
userExtraData: [String: RawJSON] = [:],
unsetProperties: Set = [],
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
guard let currentUserId = client.currentUserId else {
completion?(ClientError.CurrentUserDoesNotExist())
@@ -240,7 +265,7 @@ public extension CurrentChatUserController {
_ extraData: [String: RawJSON],
unsetProperties: [String]? = nil,
in channelId: ChannelId,
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
guard let currentUserId = client.currentUserId else {
completion?(.failure(ClientError.CurrentUserDoesNotExist()))
@@ -261,7 +286,7 @@ public extension CurrentChatUserController {
/// Fetches the most updated devices and syncs with the local database.
/// - Parameter completion: Called when the devices are synced successfully, or with error.
- func synchronizeDevices(completion: ((Error?) -> Void)? = nil) {
+ func synchronizeDevices(completion: (@Sendable(Error?) -> Void)? = nil) {
guard let currentUserId = client.currentUserId else {
completion?(ClientError.CurrentUserDoesNotExist())
return
@@ -276,7 +301,7 @@ public extension CurrentChatUserController {
/// - Parameters:
/// - pushDevice: The device information required for the desired push provider.
/// - completion: Callback when device is successfully registered, or failed with error.
- func addDevice(_ pushDevice: PushDevice, completion: ((Error?) -> Void)? = nil) {
+ func addDevice(_ pushDevice: PushDevice, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let currentUserId = client.currentUserId else {
completion?(ClientError.CurrentUserDoesNotExist())
return
@@ -300,7 +325,7 @@ public extension CurrentChatUserController {
/// - id: Device id to be removed. You can obtain registered devices via `currentUser.devices`.
/// If `currentUser.devices` is not up-to-date, please make an `synchronize` call.
/// - completion: Called when device is successfully deregistered, or with error.
- func removeDevice(id: DeviceId, completion: ((Error?) -> Void)? = nil) {
+ func removeDevice(id: DeviceId, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let currentUserId = client.currentUserId else {
completion?(ClientError.CurrentUserDoesNotExist())
return
@@ -317,7 +342,7 @@ public extension CurrentChatUserController {
///
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
///
- func markAllRead(completion: ((Error?) -> Void)? = nil) {
+ func markAllRead(completion: (@Sendable(Error?) -> Void)? = nil) {
currentUserUpdater.markAllRead { error in
self.callback {
completion?(error)
@@ -328,7 +353,7 @@ public extension CurrentChatUserController {
/// Deletes all the local downloads of file attachments.
///
/// - Parameter completion: Called when files have been deleted or when an error occured.
- func deleteAllLocalAttachmentDownloads(completion: ((Error?) -> Void)? = nil) {
+ func deleteAllLocalAttachmentDownloads(completion: (@Sendable(Error?) -> Void)? = nil) {
currentUserUpdater.deleteAllLocalAttachmentDownloads { error in
guard let completion else { return }
self.callback {
@@ -343,7 +368,7 @@ public extension CurrentChatUserController {
/// Returns the current user unreads or an error if the API call fails.
///
/// Note: This is a one-time request, it is not observable.
- func loadAllUnreads(completion: @escaping ((Result) -> Void)) {
+ func loadAllUnreads(completion: @escaping (@Sendable(Result) -> Void)) {
currentUserUpdater.loadAllUnreads { result in
self.callback {
completion(result)
@@ -355,7 +380,7 @@ public extension CurrentChatUserController {
///
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
///
- func loadBlockedUsers(completion: @escaping (Result<[BlockedUserDetails], Error>) -> Void) {
+ func loadBlockedUsers(completion: @escaping @Sendable(Result<[BlockedUserDetails], Error>) -> Void) {
currentUserUpdater.loadBlockedUsers { result in
self.callback {
completion(result)
@@ -374,7 +399,7 @@ public extension CurrentChatUserController {
/// It is optional since it can be observed from the delegate events.
func loadDraftMessages(
query: DraftListQuery = DraftListQuery(),
- completion: ((Result<[DraftMessage], Error>) -> Void)? = nil
+ completion: (@Sendable(Result<[DraftMessage], Error>) -> Void)? = nil
) {
draftListQuery = query
createDraftMessagesObserver(query: query)
@@ -400,7 +425,7 @@ public extension CurrentChatUserController {
/// It is optional since it can be observed from the delegate events.
func loadMoreDraftMessages(
limit: Int? = nil,
- completion: ((Result<[DraftMessage], Error>) -> Void)? = nil
+ completion: (@Sendable(Result<[DraftMessage], Error>) -> Void)? = nil
) {
guard let nextCursor = draftMessagesNextCursor else {
completion?(.success([]))
@@ -429,7 +454,7 @@ public extension CurrentChatUserController {
func deleteDraftMessage(
for cid: ChannelId,
threadId: MessageId? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
draftMessagesRepository.deleteDraft(for: cid, threadId: threadId) { error in
self.callback {
@@ -487,10 +512,6 @@ private extension CurrentChatUserController {
)
}
- private func createMemberUpdater() -> ChannelMemberUpdater {
- .init(database: client.databaseContainer, apiClient: client.apiClient)
- }
-
@discardableResult
private func createDraftMessagesObserver(query: DraftListQuery) -> BackgroundListDatabaseObserver {
let observer = environment.draftMessagesObserverBuilder(
@@ -554,7 +575,7 @@ public extension CurrentChatUserController {
deprecated,
message: "use addDevice(_pushDevice:) instead. This deprecated function doesn't correctly support multiple push providers."
)
- func addDevice(token: Data, pushProvider: PushProvider = .apn, completion: ((Error?) -> Void)? = nil) {
+ func addDevice(token: Data, pushProvider: PushProvider = .apn, completion: (@Sendable(Error?) -> Void)? = nil) {
addDevice(.apn(token: token), completion: completion)
}
}
diff --git a/Sources/StreamChat/Controllers/DataController.swift b/Sources/StreamChat/Controllers/DataController.swift
index 2511e6286be..55045a2a95a 100644
--- a/Sources/StreamChat/Controllers/DataController.swift
+++ b/Sources/StreamChat/Controllers/DataController.swift
@@ -5,9 +5,9 @@
import Foundation
/// The base class for controllers which represent and control a data entity. Not meant to be used directly.
-public class DataController: Controller {
+public class DataController: Controller, @unchecked Sendable {
/// Describes the possible states of `DataController`
- public enum State: Equatable {
+ public enum State: Equatable, Sendable {
/// The controller is created but no data fetched.
case initialized
/// The controllers already fetched local data if any.
@@ -20,14 +20,22 @@ public class DataController: Controller {
case remoteDataFetchFailed(ClientError)
}
+ let queue = DispatchQueue(label: "io.getstream.data-controller", target: .global(qos: .userInteractive))
+
/// The current state of the controller.
- public internal(set) var state: State = .initialized {
- didSet {
+ public internal(set) var state: State {
+ get {
+ queue.sync { _state }
+ }
+ set {
+ queue.sync { _state = newValue }
callback {
self.stateMulticastDelegate.invoke { $0.controller(self, didChangeState: self.state) }
}
}
}
+
+ private var _state: State = .initialized
/// Determines whether the controller can be recovered. A failure fetching remote data can mean that we failed to fetch the data that is present on the server, or
/// that we failed to synchronize a locally created channel
@@ -50,7 +58,7 @@ public class DataController: Controller {
/// the `error` variable contains more details about the problem.
///
// swiftlint:disable unavailable_function
- public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
fatalError("`synchronize` method must be overriden by the subclass.")
}
@@ -80,9 +88,9 @@ public extension DataControllerStateDelegate {
}
/// A helper protocol allowing calling delegate using existing `callback` method.
-protocol DelegateCallable {
+protocol DelegateCallable: Sendable {
associatedtype Delegate
- func callback(_ action: @escaping () -> Void)
+ func callback(_ action: @escaping @Sendable() -> Void)
/// The multicast delegate wrapper for all delegates of the controller
var multicastDelegate: MulticastDelegate { get }
@@ -90,7 +98,7 @@ protocol DelegateCallable {
extension DelegateCallable {
/// A helper function to ensure the delegate callback is performed using the `callback` method.
- func delegateCallback(_ callback: @escaping (Delegate) -> Void) {
+ func delegateCallback(_ callback: @escaping @Sendable(Delegate) -> Void) {
self.callback {
self.multicastDelegate.invoke(callback)
}
diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift
index 4e795a5c04d..ed26b65400e 100644
--- a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift
+++ b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundDatabaseObserver.swift
@@ -5,14 +5,14 @@
import CoreData
import Foundation
-class BackgroundDatabaseObserver- {
+class BackgroundDatabaseObserver: @unchecked Sendable {
/// Called with the aggregated changes after the internal `NSFetchResultsController` calls `controllerWillChangeContent`
/// on its delegate.
- var onWillChange: (() -> Void)?
+ var onWillChange: (@Sendable() -> Void)?
/// Called with the aggregated changes after the internal `NSFetchResultsController` calls `controllerDidChangeContent`
/// on its delegate.
- var onDidChange: (([ListChange
- ]) -> Void)?
+ var onDidChange: (@Sendable([ListChange
- ]) -> Void)?
/// Used to convert the `DTO`s to the resulting `Item`s.
private let itemCreator: (DTO) throws -> Item
diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift
index 4dc8b5be191..3920c7efbf7 100644
--- a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift
+++ b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundEntityDatabaseObserver.swift
@@ -7,7 +7,7 @@ import Foundation
/// Observes changes of a single entity specified using an `NSFetchRequest`in the provided `NSManagedObjectContext`.
/// This observation is performed on the background
-class BackgroundEntityDatabaseObserver
- : BackgroundDatabaseObserver
- {
+class BackgroundEntityDatabaseObserver: BackgroundDatabaseObserver
- , @unchecked Sendable {
var item: Item? {
rawItems.first
}
diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift
index cf1b18803b6..00f03661463 100644
--- a/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift
+++ b/Sources/StreamChat/Controllers/DatabaseObserver/BackgroundListDatabaseObserver.swift
@@ -5,7 +5,7 @@
import CoreData
import Foundation
-class BackgroundListDatabaseObserver
- : BackgroundDatabaseObserver
- {
+class BackgroundListDatabaseObserver: BackgroundDatabaseObserver
- , @unchecked Sendable {
var items: LazyCachedMapCollection
- {
LazyCachedMapCollection(elements: rawItems)
}
diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift b/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift
index 4bbcad34ce0..0d9f902b7f3 100644
--- a/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift
+++ b/Sources/StreamChat/Controllers/DatabaseObserver/EntityChange.swift
@@ -58,6 +58,7 @@ extension EntityChange {
}
extension EntityChange: Equatable where Item: Equatable {}
+extension EntityChange: Sendable where Item: Sendable {}
extension EntityChange {
/// Create a `EntityChange` value from the provided `ListChange`. It simply transforms `ListChange` to `EntityChange`
diff --git a/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift b/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift
index d0007a0cdd4..93d4de21114 100644
--- a/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift
+++ b/Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift
@@ -136,6 +136,7 @@ extension ListChange {
}
extension ListChange: Equatable where Item: Equatable {}
+extension ListChange: Sendable where Item: Sendable {}
/// When this object is set as `NSFetchedResultsControllerDelegate`, it aggregates the callbacks from the fetched results
/// controller and forwards them in the way of `[Change
- ]`. You can set the `onDidChange` callback to receive these updates.
diff --git a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift
index 3305983cb47..155e7e801f7 100644
--- a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift
+++ b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift
@@ -38,7 +38,7 @@ public extension ChatChannelController {
/// `ChannelEventsController` is a controller class which allows to observe channel
/// events and send custom events.
-public class ChannelEventsController: EventsController {
+public class ChannelEventsController: EventsController, @unchecked Sendable {
// A channel identifier provider.
private let cidProvider: () -> ChannelId?
@@ -69,7 +69,7 @@ public class ChannelEventsController: EventsController {
/// - Parameters:
/// - payload: A custom event payload to be sent.
/// - completion: A completion.
- public func sendEvent(_ payload: T, completion: ((Error?) -> Void)? = nil) {
+ public func sendEvent(_ payload: T, completion: (@Sendable(Error?) -> Void)? = nil) {
guard let cid = cid else {
callback { completion?(ClientError.ChannelNotCreatedYet()) }
return
diff --git a/Sources/StreamChat/Controllers/EventsController/EventsController.swift b/Sources/StreamChat/Controllers/EventsController/EventsController.swift
index f0c50edc9d3..cee9691df70 100644
--- a/Sources/StreamChat/Controllers/EventsController/EventsController.swift
+++ b/Sources/StreamChat/Controllers/EventsController/EventsController.swift
@@ -23,14 +23,14 @@ public protocol EventsControllerDelegate: AnyObject {
}
/// `EventsController` is a controller class which allows to observe custom and system events.
-public class EventsController: Controller, DelegateCallable {
+public class EventsController: Controller, DelegateCallable, @unchecked Sendable {
// An underlaying observer listening for events.
private var observer: EventObserver!
/// A callback queue on which delegate methods are invoked.
public var callbackQueue: DispatchQueue = .main
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -50,6 +50,8 @@ public class EventsController: Controller, DelegateCallable {
get { multicastDelegate.mainDelegate }
set { multicastDelegate.set(mainDelegate: newValue) }
}
+
+ private let queue = DispatchQueue(label: "io.getstream.events-controller", target: .global())
/// Create a new instance of `EventsController`.
///
diff --git a/Sources/StreamChat/Controllers/MemberController/MemberController.swift b/Sources/StreamChat/Controllers/MemberController/MemberController.swift
index 3f5039af1e3..1d0ae2c94a7 100644
--- a/Sources/StreamChat/Controllers/MemberController/MemberController.swift
+++ b/Sources/StreamChat/Controllers/MemberController/MemberController.swift
@@ -18,7 +18,7 @@ public extension ChatClient {
/// `ChatChannelMemberController` is a controller class which allows mutating and observing changes of a specific chat member.
///
-public class ChatChannelMemberController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatChannelMemberController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The identifier of the user this controller observes.
public let userId: UserId
@@ -37,7 +37,7 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data
return memberObserver.item
}
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -59,10 +59,10 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data
}
/// The worker used to update channel members.
- private lazy var memberUpdater = createMemberUpdater()
+ private let memberUpdater: ChannelMemberUpdater
/// The worker used to fetch channel members.
- private lazy var memberListUpdater = createMemberListUpdater()
+ private let memberListUpdater: ChannelMemberListUpdater
/// The observer used to track the user changes in the database.
private lazy var memberObserver = createMemberObserver()
@@ -95,9 +95,17 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data
self.cid = cid
self.client = client
self.environment = environment
+ memberUpdater = environment.memberUpdaterBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
+ memberListUpdater = environment.memberListUpdaterBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObservingIfNeeded()
if case let .localDataFetchFailed(error) = state {
@@ -113,20 +121,6 @@ public class ChatChannelMemberController: DataController, DelegateCallable, Data
// MARK: - Private
- private func createMemberUpdater() -> ChannelMemberUpdater {
- environment.memberUpdaterBuilder(
- client.databaseContainer,
- client.apiClient
- )
- }
-
- private func createMemberListUpdater() -> ChannelMemberListUpdater {
- environment.memberListUpdaterBuilder(
- client.databaseContainer,
- client.apiClient
- )
- }
-
private func createMemberObserver() -> BackgroundEntityDatabaseObserver {
environment.memberObserverBuilder(
client.databaseContainer,
@@ -162,7 +156,7 @@ public extension ChatChannelMemberController {
func partialUpdate(
extraData: [String: RawJSON]?,
unsetProperties: [String]? = nil,
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
memberUpdater.partialUpdate(
userId: userId,
@@ -186,7 +180,7 @@ public extension ChatChannelMemberController {
func ban(
for timeoutInMinutes: Int? = nil,
reason: String? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
memberUpdater.banMember(
userId,
@@ -211,7 +205,7 @@ public extension ChatChannelMemberController {
func shadowBan(
for timeoutInMinutes: Int? = nil,
reason: String? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
memberUpdater.banMember(
userId,
@@ -229,7 +223,7 @@ public extension ChatChannelMemberController {
/// Unbans the channel member.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- func unban(completion: ((Error?) -> Void)? = nil) {
+ func unban(completion: (@Sendable(Error?) -> Void)? = nil) {
memberUpdater.unbanMember(userId, in: cid) { error in
self.callback {
completion?(error)
diff --git a/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift b/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift
index 244fb03bc4d..15e288ffa0a 100644
--- a/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift
+++ b/Sources/StreamChat/Controllers/MemberListController/MemberListController.swift
@@ -19,7 +19,7 @@ extension ChatClient {
/// `ChatChannelMemberListController` is a controller class which allows observing
/// a list of chat users based on the provided query.
/// - Note: For an async-await alternative of the `ChatChannelMemberListControler`, please check ``MemberList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatChannelMemberListController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatChannelMemberListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying sorting and filtering for the list of channel members.
@Atomic public private(set) var query: ChannelMemberListQuery
@@ -35,7 +35,7 @@ public class ChatChannelMemberListController: DataController, DelegateCallable,
}
/// The worker used to fetch the remote data and communicate with servers.
- private lazy var memberListUpdater = createMemberListUpdater()
+ private let memberListUpdater: ChannelMemberListUpdater
/// The observer used to observe the changes in the database.
private lazy var memberListObserver = createMemberListObserver()
@@ -50,7 +50,7 @@ public class ChatChannelMemberListController: DataController, DelegateCallable,
}
}
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -73,9 +73,13 @@ public class ChatChannelMemberListController: DataController, DelegateCallable,
self.client = client
self.query = query
self.environment = environment
+ memberListUpdater = environment.memberListUpdaterBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObservingIfNeeded()
if case let .localDataFetchFailed(error) = state {
@@ -89,13 +93,6 @@ public class ChatChannelMemberListController: DataController, DelegateCallable,
}
}
- private func createMemberListUpdater() -> ChannelMemberListUpdater {
- environment.memberListUpdaterBuilder(
- client.databaseContainer,
- client.apiClient
- )
- }
-
private func createMemberListObserver() -> BackgroundListDatabaseObserver {
let observer = environment.memberListObserverBuilder(
client.databaseContainer,
@@ -139,12 +136,13 @@ public extension ChatChannelMemberListController {
/// If request fails, the completion will be called with an error.
func loadNextMembers(
limit: Int = 25,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
- var updatedQuery = query
- updatedQuery.pagination = Pagination(pageSize: limit, offset: members.count)
- memberListUpdater.load(updatedQuery) { result in
- self.query = updatedQuery
+ let pagination = Pagination(pageSize: limit, offset: members.count)
+ let updatedQuery = query.withPagination(pagination)
+ memberListUpdater.load(updatedQuery) { [weak self] result in
+ guard let self else { return }
+ self.query = self.query.withPagination(pagination)
self.callback {
completion?(result.error)
}
diff --git a/Sources/StreamChat/Controllers/MessageController/MessageController.swift b/Sources/StreamChat/Controllers/MessageController/MessageController.swift
index de88b4c0f67..205bd0be39e 100644
--- a/Sources/StreamChat/Controllers/MessageController/MessageController.swift
+++ b/Sources/StreamChat/Controllers/MessageController/MessageController.swift
@@ -18,7 +18,7 @@ public extension ChatClient {
/// `ChatMessageController` is a controller class which allows observing and mutating a chat message entity.
///
/// - Note: For an async-await alternative of the `ChatMessageController`, please check ``Chat`` and ``MessageState`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatMessageController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatMessageController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
@@ -29,7 +29,12 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public let messageId: MessageId
/// The amount of replies fetched per page.
- public var repliesPageSize: Int = .messagesPageSize
+ public var repliesPageSize: Int {
+ get { queue.sync { _repliesPageSize } }
+ set { queue.sync { _repliesPageSize = newValue } }
+ }
+
+ private var _repliesPageSize: Int = .messagesPageSize
/// The message object this controller represents.
///
@@ -56,8 +61,10 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// To observe changes of the reactions, set your class as a delegate of this controller or use the provided
/// `Combine` publishers.
///
- public var reactions: [ChatMessageReaction] = [] {
- didSet {
+ public var reactions: [ChatMessageReaction] {
+ get { queue.sync { _reactions } }
+ set {
+ queue.sync { _reactions = newValue }
delegateCallback { [weak self] in
guard let self = self else {
log.warning("Callback called while self is nil")
@@ -69,8 +76,15 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
}
+ private var _reactions: [ChatMessageReaction] = []
+
/// A Boolean value that returns whether the reactions have all been loaded or not.
- public internal(set) var hasLoadedAllReactions = false
+ public internal(set) var hasLoadedAllReactions: Bool {
+ get { queue.sync { _hasLoadedAllReactions } }
+ set { queue.sync { _hasLoadedAllReactions = newValue } }
+ }
+
+ private var _hasLoadedAllReactions = false
/// Describes the ordering the replies are presented.
///
@@ -78,8 +92,10 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// the `listOrdering` value to reflect the changes. Further updates to the replies will be delivered using the delegate
/// methods, as usual.
///
- public var listOrdering: MessageOrdering = .topToBottom {
- didSet {
+ public var listOrdering: MessageOrdering {
+ get { queue.sync { _listOrdering } }
+ set {
+ queue.sync { _listOrdering = newValue }
if state != .initialized {
setRepliesObserver()
@@ -98,6 +114,8 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
}
+ private var _listOrdering: MessageOrdering = .topToBottom
+
/// A Boolean value that returns whether the oldest replies have all been loaded or not.
public var hasLoadedAllPreviousReplies: Bool {
replyPaginationState.hasLoadedAllPreviousMessages
@@ -142,7 +160,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
private let environment: Environment
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -215,7 +233,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
setRepliesObserver()
}
- override public func synchronize(_ completion: ((Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(Error?) -> Void)? = nil) {
startObserversIfNeeded()
messageUpdater.getMessage(cid: cid, messageId: messageId) { result in
@@ -261,7 +279,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
skipEnrichUrl: Bool = false,
attachments: [AnyAttachmentPayload] = [],
extraData: [String: RawJSON]? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
var transformableInfo = NewMessageTransformableInfo(
text: text,
@@ -294,7 +312,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
- public func deleteMessage(hard: Bool = false, completion: ((Error?) -> Void)? = nil) {
+ public func deleteMessage(hard: Bool = false, completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.deleteMessage(messageId: messageId, hard: hard) { error in
self.callback {
completion?(error)
@@ -333,7 +351,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
skipPush: Bool = false,
skipEnrichUrl: Bool = false,
extraData: [String: RawJSON] = [:],
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
let parentMessageId = self.messageId
@@ -384,7 +402,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public func loadPreviousReplies(
before replyId: MessageId? = nil,
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
if hasLoadedAllPreviousReplies || isLoadingPreviousReplies {
completion?(nil)
@@ -445,7 +463,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public func loadPageAroundReplyId(
_ replyId: MessageId,
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
if isLoadingMiddleReplies {
completion?(nil)
@@ -481,7 +499,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public func loadNextReplies(
after replyId: MessageId? = nil,
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
if isLoadingNextReplies || hasLoadedAllNextReplies {
completion?(nil)
@@ -514,7 +532,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// Cleans the current state and loads the first page again.
/// - Parameter limit: Limit for page size
/// - Parameter completion: Callback when the API call is completed.
- public func loadFirstPage(limit: Int? = nil, _ completion: ((_ error: Error?) -> Void)? = nil) {
+ public func loadFirstPage(limit: Int? = nil, _ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
let pageSize = limit ?? repliesPageSize
messageUpdater.loadReplies(
cid: cid,
@@ -535,7 +553,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// called without an error and the delegate is notified of reactions changes.
public func loadNextReactions(
limit: Int = 25,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
if hasLoadedAllReactions {
callback { completion?(nil) }
@@ -583,7 +601,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public func loadReactions(
limit: Int,
offset: Int = 0,
- completion: @escaping (Result<[ChatMessageReaction], Error>) -> Void
+ completion: @escaping @Sendable(Result<[ChatMessageReaction], Error>) -> Void
) {
messageUpdater.loadReactions(
cid: cid,
@@ -609,7 +627,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public func flag(
reason: String? = nil,
extraData: [String: RawJSON]? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
messageUpdater.flagMessage(true, with: messageId, in: cid, reason: reason, extraData: extraData) { error in
self.callback {
@@ -622,7 +640,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
///
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
///
- public func unflag(completion: ((Error?) -> Void)? = nil) {
+ public func unflag(completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.flagMessage(false, with: messageId, in: cid, reason: nil, extraData: nil) { error in
self.callback {
completion?(error)
@@ -642,7 +660,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
score: Int = 1,
enforceUnique: Bool = false,
extraData: [String: RawJSON] = [:],
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
messageUpdater.addReaction(
type,
@@ -663,7 +681,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
public func deleteReaction(
_ type: MessageReactionType,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
messageUpdater.deleteReaction(type, messageId: messageId) { error in
self.callback {
@@ -676,7 +694,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - pinning: The pinning expiration information. It supports setting an infinite expiration, setting a date, or the amount of time a message is pinned.
/// - completion: A completion block with an error if the request was failed.
- public func pin(_ pinning: MessagePinning, completion: ((Error?) -> Void)? = nil) {
+ public func pin(_ pinning: MessagePinning, completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.pinMessage(messageId: messageId, pinning: pinning) { result in
self.callback {
completion?(result.error)
@@ -687,7 +705,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// Unpins the message this controller manages.
/// - Parameters:
/// - completion: A completion block with an error if the request was failed.
- public func unpin(completion: ((Error?) -> Void)? = nil) {
+ public func unpin(completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.unpinMessage(messageId: messageId) { result in
self.callback {
completion?(result.error)
@@ -704,7 +722,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - Note: The local storage URL (`attachment.downloadingState?.localFileURL`) can change between app launches.
public func downloadAttachment(
_ attachment: ChatMessageAttachment,
- completion: @escaping (Result, Error>) -> Void
+ completion: @escaping @Sendable(Result, Error>) -> Void
) where Payload: DownloadableAttachmentPayload {
messageUpdater.downloadAttachment(attachment) { result in
self.callback {
@@ -720,7 +738,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - attachmentId: The id of the attachment.
/// - completion: A completion block with an error if the deletion failed.
- public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: ((Error?) -> Void)? = nil) {
+ public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.deleteLocalAttachmentDownload(for: attachmentId) { error in
self.callback {
completion?(error)
@@ -735,7 +753,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// If operation fails, the completion will be called with an error.
public func restartFailedAttachmentUploading(
with id: AttachmentId,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
messageUpdater.restartFailedAttachmentUploading(with: id) { error in
self.callback {
@@ -747,7 +765,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// Changes local message from `.sendingFailed` to `.pendingSend` so it is enqueued by message sender worker.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the database operation is finished.
/// If operation fails, the completion will be called with an error.
- public func resendMessage(completion: ((Error?) -> Void)? = nil) {
+ public func resendMessage(completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.resendMessage(with: messageId) { error in
self.callback {
completion?(error)
@@ -760,7 +778,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - action: The action to take.
/// - completion: The completion. Will be called on a **callbackQueue** when the operation is finished.
/// If operation fails, the completion is called with the error.
- public func dispatchEphemeralMessageAction(_ action: AttachmentAction, completion: ((Error?) -> Void)? = nil) {
+ public func dispatchEphemeralMessageAction(_ action: AttachmentAction, completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.dispatchEphemeralMessageAction(cid: cid, messageId: messageId, action: action) { error in
self.callback {
completion?(error)
@@ -775,7 +793,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - language: The language message text should be translated to.
/// - completion: The completion. Will be called on a **callbackQueue** when the operation is finished.
/// If operation fails, the completion is called with the error.
- public func translate(to language: TranslationLanguage, completion: ((Error?) -> Void)? = nil) {
+ public func translate(to language: TranslationLanguage, completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.translate(messageId: messageId, to: language) { result in
self.callback {
completion?(result.error)
@@ -784,7 +802,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
/// Marks the thread read if this message is the root of a thread.
- public func markThreadRead(completion: ((Error?) -> Void)? = nil) {
+ public func markThreadRead(completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.markThreadRead(cid: cid, threadId: messageId) { error in
self.callback {
completion?(error)
@@ -793,7 +811,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
/// Marks the thread unread if this message is the root of a thread.
- public func markThreadUnread(completion: ((Error?) -> Void)? = nil) {
+ public func markThreadUnread(completion: (@Sendable(Error?) -> Void)? = nil) {
messageUpdater.markThreadUnread(
cid: cid,
threadId: messageId
@@ -813,7 +831,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
public func loadThread(
replyLimit: Int? = nil,
participantLimit: Int? = nil,
- completion: @escaping ((Result) -> Void)
+ completion: @escaping (@Sendable(Result) -> Void)
) {
var query = ThreadQuery(
messageId: messageId,
@@ -841,7 +859,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
title: String?,
extraData: [String: RawJSON]? = nil,
unsetProperties: [String]? = nil,
- completion: @escaping ((Result) -> Void)
+ completion: @escaping @Sendable(Result) -> Void
) {
messageUpdater.updateThread(
for: messageId,
@@ -882,7 +900,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
showReplyInChannel: Bool = false,
command: Command? = nil,
extraData: [String: RawJSON] = [:],
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
draftsRepository.updateDraft(
for: cid,
@@ -907,7 +925,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
///
/// It is not necessary to call this method if the thread was loaded before.
public func loadDraftReply(
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
draftsRepository.getDraft(
for: cid,
@@ -920,7 +938,7 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
}
/// Deletes the draft message for this thread.
- public func deleteDraftReply(completion: ((Error?) -> Void)? = nil) {
+ public func deleteDraftReply(completion: (@Sendable(Error?) -> Void)? = nil) {
draftsRepository.deleteDraft(
for: cid,
threadId: messageId
@@ -1060,7 +1078,7 @@ public extension ChatMessageController {
}
extension ClientError {
- final class MessageEmptyReplies: ClientError {
+ final class MessageEmptyReplies: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"You can't load previous replies when there is no replies for the message."
}
diff --git a/Sources/StreamChat/Controllers/PollController/PollController.swift b/Sources/StreamChat/Controllers/PollController/PollController.swift
index feda6f50340..f8c9a4293c2 100644
--- a/Sources/StreamChat/Controllers/PollController/PollController.swift
+++ b/Sources/StreamChat/Controllers/PollController/PollController.swift
@@ -17,7 +17,7 @@ public extension ChatClient {
}
}
-public class PollController: DataController, DelegateCallable, DataStoreProvider {
+public class PollController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
@@ -106,7 +106,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
return observer
}()
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -138,7 +138,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
eventsController.delegate = self
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObserversIfNeeded()
pollsRepository.queryPollVotes(query: ownVotesQuery) { [weak self] result in
@@ -161,7 +161,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
public func castPollVote(
answerText: String?,
optionId: String?,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
if answerText == nil && optionId == nil {
completion?(ClientError.InvalidInput())
@@ -200,7 +200,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
/// - completion: A closure to be called upon completion, with an optional `Error` if something went wrong.
public func removePollVote(
voteId: String,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
pollsRepository.removePollVote(
messageId: messageId,
@@ -218,7 +218,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
///
/// - Parameters:
/// - completion: A closure to be called upon completion, with an optional `Error` if something went wrong.
- public func closePoll(completion: ((Error?) -> Void)? = nil) {
+ public func closePoll(completion: (@Sendable(Error?) -> Void)? = nil) {
pollsRepository.closePoll(pollId: pollId, completion: { [weak self] result in
self?.callback {
completion?(result)
@@ -237,7 +237,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
text: String,
position: Int? = nil,
extraData: [String: RawJSON]? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
pollsRepository.suggestPollOption(
pollId: pollId,
@@ -268,7 +268,7 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider
}
/// Represents the visibility of votes in a poll.
-public struct VotingVisibility: RawRepresentable, Equatable {
+public struct VotingVisibility: RawRepresentable, Equatable, Sendable {
public let rawValue: String
public init(rawValue: String) {
diff --git a/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift b/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift
index ab5d5f59942..1593bc6625c 100644
--- a/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift
+++ b/Sources/StreamChat/Controllers/PollController/PollVoteListController.swift
@@ -29,7 +29,7 @@ public protocol PollVoteListControllerDelegate: DataControllerStateDelegate {
}
/// A controller which allows querying and filtering the votes of a poll.
-public class PollVoteListController: DataController, DelegateCallable, DataStoreProvider {
+public class PollVoteListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying and filtering the list of users.
public let query: PollVoteListQuery
@@ -46,7 +46,12 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore
}
/// A Boolean value that returns whether pagination is finished.
- public private(set) var hasLoadedAllVotes: Bool = false
+ public private(set) var hasLoadedAllVotes: Bool {
+ get { queue.sync { _hasLoadedAllVotes } }
+ set { queue.sync { _hasLoadedAllVotes = newValue } }
+ }
+
+ private var _hasLoadedAllVotes: Bool = false
/// Set the delegate of `PollVoteListController` to observe the changes in the system.
public weak var delegate: PollVoteListControllerDelegate? {
@@ -89,7 +94,7 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore
return observer
}()
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -104,7 +109,12 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore
private let eventsController: EventsController
private let pollsRepository: PollsRepository
private let environment: Environment
- private var nextCursor: String?
+ private var nextCursor: String? {
+ get { queue.sync { _nextCursor } }
+ set { queue.sync { _nextCursor = newValue } }
+ }
+
+ private var _nextCursor: String?
/// Creates a new `PollVoteListController`.
///
@@ -121,7 +131,7 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore
eventsController.delegate = self
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startPollVotesListObserverIfNeeded()
pollsRepository.queryPollVotes(query: query) { [weak self] result in
@@ -162,7 +172,7 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore
/// - completion: The completion callback.
public func loadMoreVotes(
limit: Int? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
let limit = limit ?? query.pagination.pageSize
var updatedQuery = query
diff --git a/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift b/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift
index 767ed8ead32..c5a8d2160f5 100644
--- a/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift
+++ b/Sources/StreamChat/Controllers/ReactionListController/ReactionListController.swift
@@ -21,7 +21,7 @@ public protocol ChatReactionListControllerDelegate: DataControllerStateDelegate
/// A controller which allows querying and filtering the reactions of a message.
///
/// - Note: For an async-await alternative of the `ChatReactionListController`, please check ``ReactionList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatReactionListController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatReactionListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying and filtering the list of reactions.
public let query: ReactionListQuery
@@ -85,7 +85,7 @@ public class ChatReactionListController: DataController, DelegateCallable, DataS
return observer
}()
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -110,7 +110,7 @@ public class ChatReactionListController: DataController, DelegateCallable, DataS
self.environment = environment
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startReactionListObserverIfNeeded()
worker.loadReactions(query: query) { result in
@@ -148,7 +148,7 @@ public extension ChatReactionListController {
/// - completion: The completion callback.
func loadMoreReactions(
limit: Int = 25,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
var updatedQuery = query
updatedQuery.pagination = Pagination(pageSize: limit, offset: reactions.count)
diff --git a/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift b/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift
index 6e462d34e2d..ba10e335c93 100644
--- a/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift
+++ b/Sources/StreamChat/Controllers/SearchControllers/MessageSearchController/MessageSearchController.swift
@@ -20,7 +20,7 @@ public extension ChatClient {
/// `ChatMessageSearchController` is a controller class which allows observing a list of messages based on the provided query.
///
/// - Note: For an async-await alternative of the `ChatMessageSearchController`, please check ``MessageSearch`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatMessageSearchController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatMessageSearchController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
private let environment: Environment
@@ -28,7 +28,12 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
init(client: ChatClient, environment: Environment = .init()) {
self.client = client
self.environment = environment
-
+ messageUpdater = environment.messageUpdaterBuilder(
+ client.config.isLocalStorageEnabled,
+ client.messageRepository,
+ client.databaseContainer,
+ client.apiClient
+ )
super.init()
setMessagesObserver()
@@ -44,7 +49,12 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
/// Filter hash this controller observes.
let explicitFilterHash = UUID().uuidString
- private var nextPageCursor: String?
+ private var nextPageCursor: String? {
+ get { queue.sync { _nextPageCursor } }
+ set { queue.sync { _nextPageCursor = newValue } }
+ }
+
+ private var _nextPageCursor: String?
lazy var query: MessageSearchQuery = {
// Filter is just a mock, explicit hash will override it
@@ -55,7 +65,12 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
}()
/// Copy of last search query made, used for getting next page.
- var lastQuery: MessageSearchQuery?
+ var lastQuery: MessageSearchQuery? {
+ get { queue.sync { _lastQuery } }
+ set { queue.sync { _lastQuery = newValue } }
+ }
+
+ private var _lastQuery: MessageSearchQuery?
/// The messages matching the query of this controller.
///
@@ -66,13 +81,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
return messagesObserver?.items ?? []
}
- lazy var messageUpdater = self.environment
- .messageUpdaterBuilder(
- client.config.isLocalStorageEnabled,
- client.messageRepository,
- client.databaseContainer,
- client.apiClient
- )
+ let messageUpdater: MessageUpdater
/// Used for observing the database for changes.
private var messagesObserver: BackgroundListDatabaseObserver?
@@ -111,7 +120,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
messagesObserver = observer
}
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -145,7 +154,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
/// - text: The message text.
/// - completion: Called when the controller has finished fetching remote data.
/// If the data fetching fails, the error variable contains more details about the problem.
- public func search(text: String, completion: ((_ error: Error?) -> Void)? = nil) {
+ public func search(text: String, completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObserversIfNeeded()
guard let currentUserId = client.currentUserId else {
@@ -191,7 +200,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
/// - query: Search query.
/// - completion: Called when the controller has finished fetching remote data.
/// If the data fetching fails, the error variable contains more details about the problem.
- public func search(query: MessageSearchQuery, completion: ((_ error: Error?) -> Void)? = nil) {
+ public func search(query: MessageSearchQuery, completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
var query = query
query.filterHash = explicitFilterHash
@@ -222,7 +231,7 @@ public class ChatMessageSearchController: DataController, DelegateCallable, Data
///
public func loadNextMessages(
limit: Int = 25,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
guard let lastQuery = lastQuery else {
completion?(ClientError("You should make a search before calling for next page."))
diff --git a/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift b/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift
index 1ee78523bc0..dd6a729f2d3 100644
--- a/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift
+++ b/Sources/StreamChat/Controllers/SearchControllers/UserSearchController/UserSearchController.swift
@@ -20,18 +20,23 @@ extension ChatClient {
/// `ChatUserSearchController` is a controller class which allows observing a list of chat users based on the provided query.
///
/// - Note: For an async-await alternative of the `ChatUserSearchController`, please check ``UserSearch`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatUserSearchController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatUserSearchController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
/// Copy of last search query made, used for getting next page.
- public private(set) var query: UserListQuery?
+ public private(set) var query: UserListQuery? {
+ get { queue.sync { _query } }
+ set { queue.async { self._query = newValue } }
+ }
+
+ private var _query: UserListQuery?
/// The users matching the last query of this controller.
private var _users: [ChatUser] = []
public var userArray: [ChatUser] {
setLocalDataFetchedStateIfNeeded()
- return _users
+ return queue.sync { _users }
}
@available(*, deprecated, message: "Please, switch to `userArray: [ChatUser]`")
@@ -73,7 +78,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto
/// - term: Search term. If empty string or `nil`, all users are fetched.
/// - completion: Called when the controller has finished fetching remote data.
/// If the data fetching fails, the error variable contains more details about the problem.
- public func search(term: String?, completion: ((_ error: Error?) -> Void)? = nil) {
+ public func search(term: String?, completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
fetch(.search(term: term), completion: completion)
}
@@ -88,7 +93,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto
/// - query: Search query.
/// - completion: Called when the controller has finished fetching remote data.
/// If the data fetching fails, the error variable contains more details about the problem.
- public func search(query: UserListQuery, completion: ((_ error: Error?) -> Void)? = nil) {
+ public func search(query: UserListQuery, completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
fetch(query, completion: completion)
}
@@ -101,7 +106,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto
///
public func loadNextUsers(
limit: Int = 25,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
guard let lastQuery = query else {
completion?(ClientError("You should make a search before calling for next page."))
@@ -116,7 +121,7 @@ public class ChatUserSearchController: DataController, DelegateCallable, DataSto
/// Clears the current search results.
public func clearResults() {
- _users = []
+ queue.async { self._users = [] }
}
}
@@ -126,7 +131,7 @@ private extension ChatUserSearchController {
/// - Parameters:
/// - query: The query to fetch.
/// - completion: The completion that is triggered when the query is processed.
- func fetch(_ query: UserListQuery, completion: ((Error?) -> Void)? = nil) {
+ func fetch(_ query: UserListQuery, completion: (@Sendable(Error?) -> Void)? = nil) {
// TODO: Remove with the next major
//
// This is needed to make the delegate fire about state changes at the same time with the same
@@ -136,7 +141,7 @@ private extension ChatUserSearchController {
userQueryUpdater.fetch(userListQuery: query) { [weak self] result in
switch result {
case let .success(page):
- self?.save(page: page) { loadedUsers in
+ self?.save(page: page) { [weak self] loadedUsers in
let listChanges = self?.prepareListChanges(
loadedPage: loadedUsers,
updatePolicy: query.pagination?.offset == 0 ? .replace : .merge
@@ -144,11 +149,11 @@ private extension ChatUserSearchController {
self?.query = query
if let listChanges = listChanges, let users = self?.userList(after: listChanges) {
- self?._users = users
+ self?.queue.async { [weak self] in self?._users = users }
}
self?.state = .remoteDataFetched
- self?.callback {
+ self?.callback { [weak self] in
self?.multicastDelegate.invoke {
guard let self = self, let listChanges = listChanges else { return }
$0.controller(self, didChangeUsers: listChanges)
@@ -168,16 +173,13 @@ private extension ChatUserSearchController {
/// - Parameters:
/// - page: The page of users fetched from the API.
/// - completion: The completion that will be called with user models when database write is completed.
- func save(page: UserListPayload, completion: @escaping ([ChatUser]) -> Void) {
- var loadedUsers: [ChatUser] = []
-
- client.databaseContainer.write({ session in
- loadedUsers = page
+ func save(page: UserListPayload, completion: @escaping @Sendable([ChatUser]) -> Void) {
+ client.databaseContainer.write(converting: { session in
+ page
.users
.compactMap { try? session.saveUser(payload: $0).asModel() }
-
- }, completion: { _ in
- completion(loadedUsers)
+ }, completion: { result in
+ completion(result.value ?? [])
})
}
diff --git a/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift b/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift
index a582a66edb1..f3f8fffa374 100644
--- a/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift
+++ b/Sources/StreamChat/Controllers/ThreadListController/ThreadListController.swift
@@ -20,7 +20,7 @@ public protocol ChatThreadListControllerDelegate: DataControllerStateDelegate {
/// `ChatThreadListController` is a controller class which allows querying and
/// observing the threads that the current user is participating.
-public class ChatThreadListController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatThreadListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query of the thread list.
public let query: ThreadListQuery
@@ -28,7 +28,12 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto
public let client: ChatClient
/// The cursor of the next page in case there is more data.
- private var nextCursor: String?
+ private var nextCursor: String? {
+ get { queue.sync { _nextCursor } }
+ set { queue.sync { _nextCursor = newValue } }
+ }
+
+ private var _nextCursor: String?
/// The threads matching the query of this controller.
///
@@ -40,14 +45,15 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto
}
/// The repository used to fetch the data from remote and local cache.
- private lazy var threadsRepository: ThreadsRepository = self.environment
- .threadsRepositoryBuilder(
- client.databaseContainer,
- client.apiClient
- )
+ private let threadsRepository: ThreadsRepository
/// A Boolean value that returns whether pagination is finished.
- public private(set) var hasLoadedAllThreads: Bool = false
+ public private(set) var hasLoadedAllThreads: Bool {
+ get { queue.sync { _hasLoadedAllThreads } }
+ set { queue.sync { _hasLoadedAllThreads = newValue } }
+ }
+
+ private var _hasLoadedAllThreads: Bool = false
/// A type-erased delegate.
var multicastDelegate: MulticastDelegate = .init() {
@@ -80,7 +86,7 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto
return observer
}()
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -103,16 +109,20 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto
self.client = client
self.query = query
self.environment = environment
+ threadsRepository = environment.threadsRepositoryBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startThreadListObserverIfNeeded()
threadsRepository.loadThreads(
query: query
) { [weak self] result in
switch result {
case let .success(threadListResponse):
- self?.callback {
+ self?.callback { [weak self] in
self?.state = .remoteDataFetched
self?.nextCursor = threadListResponse.next
self?.hasLoadedAllThreads = threadListResponse.next == nil
@@ -134,7 +144,7 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto
/// - completion: The completion.
public func loadMoreThreads(
limit: Int? = nil,
- completion: ((Result<[ChatThread], Error>) -> Void)? = nil
+ completion: (@Sendable(Result<[ChatThread], Error>) -> Void)? = nil
) {
let limit = limit ?? query.limit
var updatedQuery = query
@@ -143,7 +153,7 @@ public class ChatThreadListController: DataController, DelegateCallable, DataSto
threadsRepository.loadThreads(query: updatedQuery) { [weak self] result in
switch result {
case let .success(threadListResponse):
- self?.callback {
+ self?.callback { [weak self] in
let threads = threadListResponse.threads
self?.nextCursor = threadListResponse.next
self?.hasLoadedAllThreads = threadListResponse.next == nil
diff --git a/Sources/StreamChat/Controllers/UserController/UserController.swift b/Sources/StreamChat/Controllers/UserController/UserController.swift
index 3721322bd78..f1a50b7ea41 100644
--- a/Sources/StreamChat/Controllers/UserController/UserController.swift
+++ b/Sources/StreamChat/Controllers/UserController/UserController.swift
@@ -19,7 +19,7 @@ public extension ChatClient {
///
/// `ChatUserController` objects are lightweight, and they can be used for both, continuous data change observations,
/// and for quick user actions (like mute/unmute).
-public class ChatUserController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatUserController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The identifier of tge user this controller observes.
public let userId: UserId
@@ -46,23 +46,45 @@ public class ChatUserController: DataController, DelegateCallable, DataStoreProv
}
/// The worker used to fetch the remote data and communicate with servers.
- private lazy var userUpdater = createUserUpdater()
+ private var userUpdater: UserUpdater {
+ queue.sync {
+ if let _userUpdater {
+ return _userUpdater
+ }
+ let updater = createUserUpdater()
+ _userUpdater = updater
+ return updater
+ }
+ }
+
+ private var _userUpdater: UserUpdater?
/// The observer used to track the user changes in the database.
- private lazy var userObserver = createUserObserver()
- .onChange { [weak self] change in
- self?.delegateCallback { [weak self] in
- guard let self = self else {
- log.warning("Callback called while self is nil")
- return
+ private var userObserver: BackgroundEntityDatabaseObserver {
+ queue.sync {
+ if let observer = _userObserver {
+ return observer
+ }
+ var observer = createUserObserver()
+ observer = observer.onChange { [weak self] change in
+ self?.delegateCallback { [weak self] in
+ guard let self = self else {
+ log.warning("Callback called while self is nil")
+ return
+ }
+ $0.userController(self, didUpdateUser: change)
}
- $0.userController(self, didUpdateUser: change)
}
+ _userObserver = observer
+ return observer
}
+ }
+
+ private var _userObserver: BackgroundEntityDatabaseObserver?
private let environment: Environment
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -89,7 +111,7 @@ public class ChatUserController: DataController, DelegateCallable, DataStoreProv
self.environment = environment
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startObservingIfNeeded()
if case let .localDataFetchFailed(error) = state {
@@ -140,7 +162,7 @@ public extension ChatUserController {
/// Mutes the user this controller manages.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- func mute(completion: ((Error?) -> Void)? = nil) {
+ func mute(completion: (@Sendable(Error?) -> Void)? = nil) {
userUpdater.muteUser(userId) { error in
self.callback {
completion?(error)
@@ -151,7 +173,7 @@ public extension ChatUserController {
/// Unmutes the user this controller manages.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
///
- func unmute(completion: ((Error?) -> Void)? = nil) {
+ func unmute(completion: (@Sendable(Error?) -> Void)? = nil) {
userUpdater.unmuteUser(userId) { error in
self.callback {
completion?(error)
@@ -162,7 +184,7 @@ public extension ChatUserController {
/// Blocks the user this controller manages.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
- func block(completion: ((Error?) -> Void)? = nil) {
+ func block(completion: (@Sendable(Error?) -> Void)? = nil) {
userUpdater.blockUser(userId) { error in
self.callback {
completion?(error)
@@ -173,7 +195,7 @@ public extension ChatUserController {
/// Unblocks the user this controller manages.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
///
- func unblock(completion: ((Error?) -> Void)? = nil) {
+ func unblock(completion: (@Sendable(Error?) -> Void)? = nil) {
userUpdater.unblockUser(userId) { error in
self.callback {
completion?(error)
@@ -191,7 +213,7 @@ public extension ChatUserController {
func flag(
reason: String? = nil,
extraData: [String: RawJSON]? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
userUpdater.flagUser(true, with: userId, reason: reason, extraData: extraData) { error in
self.callback {
@@ -203,7 +225,7 @@ public extension ChatUserController {
/// Unflags the user this controller manages.
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
///
- func unflag(completion: ((Error?) -> Void)? = nil) {
+ func unflag(completion: (@Sendable(Error?) -> Void)? = nil) {
userUpdater.flagUser(false, with: userId, reason: nil, extraData: nil) { error in
self.callback {
completion?(error)
diff --git a/Sources/StreamChat/Controllers/UserListController/UserListController.swift b/Sources/StreamChat/Controllers/UserListController/UserListController.swift
index 634fac7e08d..e844fa00c52 100644
--- a/Sources/StreamChat/Controllers/UserListController/UserListController.swift
+++ b/Sources/StreamChat/Controllers/UserListController/UserListController.swift
@@ -20,7 +20,7 @@ extension ChatClient {
/// `ChatUserListController` is a controller class which allows observing a list of chat users based on the provided query.
///
/// - Note: For an async-await alternative of the `ChatUserListController`, please check ``UserList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
-public class ChatUserListController: DataController, DelegateCallable, DataStoreProvider {
+public class ChatUserListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying and filtering the list of users.
public let query: UserListQuery
@@ -38,11 +38,7 @@ public class ChatUserListController: DataController, DelegateCallable, DataStore
}
/// The worker used to fetch the remote data and communicate with servers.
- private lazy var worker: UserListUpdater = self.environment
- .userQueryUpdaterBuilder(
- client.databaseContainer,
- client.apiClient
- )
+ private let worker: UserListUpdater
/// A type-erased delegate.
var multicastDelegate: MulticastDelegate = .init() {
@@ -79,7 +75,7 @@ public class ChatUserListController: DataController, DelegateCallable, DataStore
return observer
}()
- var _basePublishers: Any?
+ @Atomic private 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.
@@ -102,9 +98,13 @@ public class ChatUserListController: DataController, DelegateCallable, DataStore
self.client = client
self.query = query
self.environment = environment
+ worker = environment.userQueryUpdaterBuilder(
+ client.databaseContainer,
+ client.apiClient
+ )
}
- override public func synchronize(_ completion: ((_ error: Error?) -> Void)? = nil) {
+ override public func synchronize(_ completion: (@Sendable(_ error: Error?) -> Void)? = nil) {
startUserListObserverIfNeeded()
worker.update(userListQuery: query) { result in
@@ -143,7 +143,7 @@ public extension ChatUserListController {
///
func loadNextUsers(
limit: Int = 25,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
var updatedQuery = query
updatedQuery.pagination = Pagination(pageSize: limit, offset: users.count)
diff --git a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
index af60ce40118..5dd92beb995 100644
--- a/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
@@ -86,7 +86,7 @@ class AttachmentDTO: NSManagedObject {
}
static func load(id: AttachmentId, context: NSManagedObjectContext) -> AttachmentDTO? {
- load(by: id.rawValue, context: context).first
+ load(by: id.rawValue, context: context).first as? Self
}
static func loadOrCreate(id: AttachmentId, context: NSManagedObjectContext) -> AttachmentDTO {
@@ -374,27 +374,27 @@ extension LocalAttachmentDownloadState {
}
extension ClientError {
- final class AttachmentDoesNotExist: ClientError {
+ final class AttachmentDoesNotExist: ClientError, @unchecked Sendable {
init(id: AttachmentId) {
super.init("There is no `AttachmentDTO` instance in the DB matching id: \(id).")
}
}
- final class AttachmentUploadBlocked: ClientError {
+ final class AttachmentUploadBlocked: ClientError, @unchecked Sendable {
init(id: AttachmentId, attachmentType: AttachmentType, pathExtension: String) {
super.init("`AttachmentDTO` with \(id) and type \(attachmentType) and path extension \(pathExtension) is blocked on the Stream dashboard.")
}
}
- final class AttachmentEditing: ClientError {
+ final class AttachmentEditing: ClientError, @unchecked Sendable {
init(id: AttachmentId, reason: String) {
super.init("`AttachmentDTO` with id: \(id) can't be edited (\(reason))")
}
}
- final class AttachmentDecoding: ClientError {}
+ final class AttachmentDecoding: ClientError, @unchecked Sendable {}
- final class AttachmentDownloading: ClientError {
+ final class AttachmentDownloading: ClientError, @unchecked Sendable {
init(id: AttachmentId, reason: String) {
super.init(
"Failed to download attachment with id: \(id): \(reason)"
@@ -402,7 +402,7 @@ extension ClientError {
}
}
- final class AttachmentUploading: ClientError {
+ final class AttachmentUploading: ClientError, @unchecked Sendable {
init(id: AttachmentId) {
super.init(
"Failed to upload attachment with id: \(id)"
diff --git a/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift
index 2265f3d99ab..11c68b44d84 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift
@@ -21,7 +21,7 @@ class ChannelListQueryDTO: NSManagedObject {
keyPath: #keyPath(ChannelListQueryDTO.filterHash),
equalTo: filterHash,
context: context
- ).first
+ ).first as? Self
}
/// The fetch request that returns all existed queries from the database.
diff --git a/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift
index ced9facedd4..5f53fd47668 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelMemberListQueryDTO.swift
@@ -23,7 +23,7 @@ final class ChannelMemberListQueryDTO: NSManagedObject {
keyPath: #keyPath(ChannelMemberListQueryDTO.queryHash),
equalTo: queryHash,
context: context
- ).first
+ ).first as? Self
}
static func loadOrCreate(queryHash: String, context: NSManagedObjectContext) -> ChannelMemberListQueryDTO {
diff --git a/Sources/StreamChat/Database/DTOs/DeviceDTO.swift b/Sources/StreamChat/Database/DTOs/DeviceDTO.swift
index f4d3098b789..dd0e32a2cd5 100644
--- a/Sources/StreamChat/Database/DTOs/DeviceDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/DeviceDTO.swift
@@ -21,7 +21,7 @@ extension DeviceDTO {
/// - context: The context used to fetch `DeviceDTO`
///
static func load(id: String, context: NSManagedObjectContext) -> DeviceDTO? {
- load(by: id, context: context).first
+ load(by: id, context: context).first as? Self
}
/// If a Device with the given id exists in the context, fetches and returns it. Otherwise creates a new
diff --git a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
index 9694d71045d..1d81c18dffc 100644
--- a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
@@ -74,7 +74,7 @@ extension MemberDTO {
}
static func load(memberId: String, context: NSManagedObjectContext) -> MemberDTO? {
- load(by: memberId, context: context).first
+ load(by: memberId, context: context).first as? Self
}
/// If a User with the given id exists in the context, fetches and returns it. Otherwise create a new
diff --git a/Sources/StreamChat/Database/DTOs/MessageDTO.swift b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
index 83c8ff2bfe5..c949b4d1017 100644
--- a/Sources/StreamChat/Database/DTOs/MessageDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
@@ -483,7 +483,7 @@ class MessageDTO: NSManagedObject {
}
static func load(id: String, context: NSManagedObjectContext) -> MessageDTO? {
- load(by: id, context: context).first
+ load(by: id, context: context).first as? Self
}
static func loadOrCreate(id: String, context: NSManagedObjectContext, cache: PreWarmedCache?) -> MessageDTO {
@@ -1799,16 +1799,16 @@ private extension ChatMessage {
}
extension ClientError {
- final class CurrentUserDoesNotExist: ClientError {
+ final class CurrentUserDoesNotExist: ClientError, @unchecked Sendable {
override var localizedDescription: String {
"There is no `CurrentUserDTO` instance in the DB."
+ "Make sure to call `client.currentUserController.reloadUserIfNeeded()`"
}
}
- final class MessagePayloadSavingFailure: ClientError {}
+ final class MessagePayloadSavingFailure: ClientError, @unchecked Sendable {}
- final class ChannelDoesNotExist: ClientError {
+ final class ChannelDoesNotExist: ClientError, @unchecked Sendable {
init(cid: ChannelId) {
super.init("There is no `ChannelDTO` instance in the DB matching cid: \(cid).")
}
diff --git a/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift b/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift
index 7d6d9d6ece5..115c15b5313 100644
--- a/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MessageReactionDTO.swift
@@ -66,17 +66,17 @@ extension MessageReactionDTO {
}
static func load(reactionId: String, context: NSManagedObjectContext) -> MessageReactionDTO? {
- load(by: reactionId, context: context).first
+ load(by: reactionId, context: context).first as? Self
}
- static let notLocallyDeletedPredicates: NSPredicate = {
+ static var notLocallyDeletedPredicates: NSPredicate {
NSCompoundPredicate(orPredicateWithSubpredicates: [
NSPredicate(format: "localStateRaw == %@", LocalReactionState.unknown.rawValue),
NSPredicate(format: "localStateRaw == %@", LocalReactionState.sending.rawValue),
NSPredicate(format: "localStateRaw == %@", LocalReactionState.pendingSend.rawValue),
NSPredicate(format: "localStateRaw == %@", LocalReactionState.deletingFailed.rawValue)
])
- }()
+ }
static func loadReactions(ids: [String], context: NSManagedObjectContext) -> [MessageReactionDTO] {
guard !ids.isEmpty else {
diff --git a/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift b/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift
index 50284afb7ac..5af2f13f826 100644
--- a/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MessageSearchQueryDTO.swift
@@ -16,7 +16,7 @@ class MessageSearchQueryDTO: NSManagedObject {
keyPath: #keyPath(MessageSearchQueryDTO.filterHash),
equalTo: filterHash,
context: context
- ).first
+ ).first as? Self
}
}
diff --git a/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift
index b1d9338cc01..94a19a60632 100644
--- a/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/PollVoteListQueryDTO.swift
@@ -21,7 +21,7 @@ class PollVoteListQueryDTO: NSManagedObject {
keyPath: #keyPath(PollVoteListQueryDTO.filterHash),
equalTo: filterHash,
context: context
- ).first
+ ).first as? Self
}
static func loadOrCreate(filterHash: String, context: NSManagedObjectContext) -> PollVoteListQueryDTO {
diff --git a/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift
index dbb93e750ae..1c21431ce44 100644
--- a/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ReactionListQueryDTO.swift
@@ -21,7 +21,7 @@ class ReactionListQueryDTO: NSManagedObject {
keyPath: #keyPath(ReactionListQueryDTO.filterHash),
equalTo: filterHash,
context: context
- ).first
+ ).first as? Self
}
static func loadOrCreate(filterHash: String, context: NSManagedObjectContext) -> ReactionListQueryDTO {
diff --git a/Sources/StreamChat/Database/DTOs/UserDTO.swift b/Sources/StreamChat/Database/DTOs/UserDTO.swift
index 476d1abc3f5..ab64cc7fbcb 100644
--- a/Sources/StreamChat/Database/DTOs/UserDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/UserDTO.swift
@@ -106,7 +106,7 @@ extension UserDTO {
/// - context: The context used to fetch `UserDTO`
///
static func load(id: String, context: NSManagedObjectContext) -> UserDTO? {
- load(by: id, context: context).first
+ load(by: id, context: context).first as? Self
}
/// If a User with the given id exists in the context, fetches and returns it. Otherwise creates a new
diff --git a/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift b/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift
index 92958a353b9..e9d0a5dbe02 100644
--- a/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/UserListQueryDTO.swift
@@ -25,7 +25,7 @@ class UserListQueryDTO: NSManagedObject {
keyPath: #keyPath(UserListQueryDTO.filterHash),
equalTo: filterHash,
context: context
- ).first
+ ).first as? Self
}
}
diff --git a/Sources/StreamChat/Database/DatabaseContainer.swift b/Sources/StreamChat/Database/DatabaseContainer.swift
index 0648a5779a6..4016ca719eb 100644
--- a/Sources/StreamChat/Database/DatabaseContainer.swift
+++ b/Sources/StreamChat/Database/DatabaseContainer.swift
@@ -2,7 +2,7 @@
// Copyright © 2025 Stream.io Inc. All rights reserved.
//
-import CoreData
+@preconcurrency import CoreData
import Foundation
/// Convenience subclass of `NSPersistentContainer` allowing easier setup of the database stack.
@@ -70,7 +70,8 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
let chatClientConfig: ChatClientConfig
- static var cachedModels = [String: NSManagedObjectModel]()
+ private static let cachedModelsQueue = DispatchQueue(label: "io.getstream.database-container", target: .global())
+ private nonisolated(unsafe) static var _cachedModels = [String: NSManagedObjectModel]()
/// All `NSManagedObjectContext`s this container owns.
private(set) lazy var allContext: [NSManagedObjectContext] = [viewContext, backgroundReadOnlyContext, stateLayerContext, writableContext]
@@ -94,7 +95,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
chatClientConfig: ChatClientConfig
) {
let managedObjectModel: NSManagedObjectModel
- if let cachedModel = Self.cachedModels[modelName] {
+ if let cachedModel = Self.cachedModelsQueue.sync(execute: { Self._cachedModels[modelName] }) {
managedObjectModel = cachedModel
} else {
// It's safe to unwrap the following values because this is not settable by users and it's always a programmer error.
@@ -102,7 +103,9 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
let modelURL = bundle.url(forResource: modelName, withExtension: "momd")!
let model = NSManagedObjectModel(contentsOf: modelURL)!
managedObjectModel = model
- Self.cachedModels[modelName] = model
+ Self.cachedModelsQueue.async {
+ Self._cachedModels[modelName] = model
+ }
}
self.chatClientConfig = chatClientConfig
@@ -173,7 +176,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
/// Use this method to safely mutate the content of the database. This method is asynchronous.
///
/// - Parameter actions: A block that performs the actual mutation.
- func write(_ actions: @escaping (DatabaseSession) throws -> Void) {
+ func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void) {
write(actions, completion: { _ in })
}
@@ -185,7 +188,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
/// - Parameters:
/// - actions: A block that performs the actual mutation.
/// - completion: Called when the changes are saved to the DB. If the changes can't be saved, called with an error.
- func write(_ actions: @escaping (DatabaseSession) throws -> Void, completion: @escaping (Error?) -> Void) {
+ func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void, completion: @escaping @Sendable(Error?) -> Void) {
writableContext.perform {
log.debug("Starting a database session.", subsystems: .database)
do {
@@ -211,7 +214,7 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
}
}
- func write(_ actions: @escaping (DatabaseSession) throws -> Void) async throws {
+ func write(_ actions: @escaping @Sendable(DatabaseSession) throws -> Void) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
write(actions) { error in
if let error {
@@ -223,23 +226,34 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
}
}
- func write(converting actions: @escaping (DatabaseSession) throws -> T, completion: @escaping (Result) -> Void) {
- var result: T?
+ func write(converting actions: @escaping @Sendable(DatabaseSession) throws -> T, completion: @escaping @Sendable(Result) -> Void) where T: Sendable {
+ nonisolated(unsafe) var result: T?
write { session in
result = try actions(session)
} completion: { error in
- if let result {
+ // Note: actions can pass which returns a value, but underlying write can fail
+ if let error {
+ completion(.failure(error))
+ } else if let result {
completion(.success(result))
} else {
- completion(.failure(error ?? ClientError.Unknown()))
+ completion(.failure(ClientError.Unknown()))
+ }
+ }
+ }
+
+ func writeConverting(_ actions: @escaping @Sendable(DatabaseSession) throws -> T) async throws -> T where T: Sendable {
+ try await withCheckedThrowingContinuation { continuation in
+ write(converting: actions) { result in
+ continuation.resume(with: result)
}
}
}
private func read(
from context: NSManagedObjectContext,
- _ actions: @escaping (DatabaseSession) throws -> T,
- completion: @escaping (Result) -> Void
+ _ actions: @escaping @Sendable(DatabaseSession) throws -> T,
+ completion: @escaping @Sendable(Result) -> Void
) {
context.perform {
do {
@@ -255,11 +269,11 @@ class DatabaseContainer: NSPersistentContainer, @unchecked Sendable {
}
}
- func read(_ actions: @escaping (DatabaseSession) throws -> T, completion: @escaping (Result) -> Void) {
+ func read(_ actions: @escaping @Sendable(DatabaseSession) throws -> T, completion: @escaping @Sendable(Result) -> Void) {
read(from: backgroundReadOnlyContext, actions, completion: completion)
}
- func read(_ actions: @escaping (DatabaseSession) throws -> T) async throws -> T {
+ func read(_ actions: @escaping @Sendable(DatabaseSession) throws -> T) async throws -> T where T: Sendable {
try await withCheckedThrowingContinuation { continuation in
read(from: stateLayerContext, actions) { result in
continuation.resume(with: result)
diff --git a/Sources/StreamChat/Errors/ClientError.swift b/Sources/StreamChat/Errors/ClientError.swift
index bc8d066cc39..4d664daddff 100644
--- a/Sources/StreamChat/Errors/ClientError.swift
+++ b/Sources/StreamChat/Errors/ClientError.swift
@@ -5,7 +5,7 @@
import Foundation
/// A Client error.
-public class ClientError: Error, CustomStringConvertible {
+public class ClientError: Error, CustomStringConvertible, @unchecked Sendable {
public struct Location: Equatable {
public let file: String
public let line: Int
@@ -56,10 +56,10 @@ public class ClientError: Error, CustomStringConvertible {
extension ClientError {
/// An unexpected error.
- public final class Unexpected: ClientError {}
+ public final class Unexpected: ClientError, @unchecked Sendable {}
/// An unknown error.
- public final class Unknown: ClientError {}
+ public final class Unknown: ClientError, @unchecked Sendable {}
}
// This should probably live only in the test target since it's not "true" equatable
diff --git a/Sources/StreamChat/Extensions/Task+Extensions.swift b/Sources/StreamChat/Extensions/Task+Extensions.swift
index a142f2d575e..bbd732f6c0a 100644
--- a/Sources/StreamChat/Extensions/Task+Extensions.swift
+++ b/Sources/StreamChat/Extensions/Task+Extensions.swift
@@ -5,7 +5,7 @@
import Foundation
extension Task {
- @discardableResult static func mainActor(priority: TaskPriority? = nil, operation: @escaping @MainActor() async throws -> Success) -> Task where Failure == any Error {
+ @discardableResult static func mainActor(priority: TaskPriority? = nil, operation: @escaping @MainActor @Sendable() async throws -> Success) -> Task where Failure == any Error {
Task(priority: priority) { @MainActor in
try await operation()
}
diff --git a/Sources/StreamChat/Models/AppSettings.swift b/Sources/StreamChat/Models/AppSettings.swift
index d3cd113fdf0..341e4a46139 100644
--- a/Sources/StreamChat/Models/AppSettings.swift
+++ b/Sources/StreamChat/Models/AppSettings.swift
@@ -6,7 +6,7 @@ import CoreServices
import Foundation
/// A type representing the app settings.
-public struct AppSettings {
+public struct AppSettings: Sendable {
/// The name of the app.
public let name: String
/// The the file uploading configuration.
@@ -18,7 +18,7 @@ public struct AppSettings {
/// A boolean value determining if async url enrichment is enabled.
public let asyncUrlEnrichEnabled: Bool
- public struct UploadConfig {
+ public struct UploadConfig: Sendable {
/// The allowed file extensions.
public let allowedFileExtensions: [String]
/// The blocked file extensions.
diff --git a/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift b/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift
index 217fa503013..808cab5ec64 100644
--- a/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift
+++ b/Sources/StreamChat/Models/Attachments/AnyAttachmentPayload.swift
@@ -6,7 +6,7 @@ import Foundation
/// A protocol an attachment payload type has to conform in order it can be
/// attached to/exposed on the message.
-public protocol AttachmentPayload: Codable {
+public protocol AttachmentPayload: Codable, Sendable {
/// A type of resulting attachment.
static var type: AttachmentType { get }
}
@@ -14,12 +14,12 @@ public protocol AttachmentPayload: Codable {
/// A type-erased type that wraps either a local file URL that has to be uploaded
/// and attached to the message OR a custom payload which the message attachment
/// should contain.
-public struct AnyAttachmentPayload {
+public struct AnyAttachmentPayload: Sendable {
/// A type of attachment that will be created when the message is sent.
public let type: AttachmentType
/// A payload that will exposed on attachment when the message is sent.
- public let payload: Encodable
+ public let payload: (Encodable & Sendable)
/// A URL referencing to the local file that should be uploaded.
public let localFileURL: URL?
@@ -104,7 +104,7 @@ public extension AnyAttachmentPayload {
localFileURL: URL,
attachmentType: AttachmentType,
localMetadata: AnyAttachmentLocalMetadata? = nil,
- extraData: Encodable? = nil
+ extraData: (Encodable & Sendable)? = nil
) throws {
let file = try AttachmentFile(url: localFileURL)
let extraData = try extraData
@@ -166,7 +166,7 @@ public extension AnyAttachmentPayload {
}
extension ClientError {
- public final class UnsupportedUploadableAttachmentType: ClientError {
+ public final class UnsupportedUploadableAttachmentType: ClientError, @unchecked Sendable {
init(_ type: AttachmentType) {
super.init(
"For uploadable attachments only image/video/audio/file/voiceRecording types are supported."
diff --git a/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift b/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift
index b7d4fa84a1a..755215e9c73 100644
--- a/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift
+++ b/Sources/StreamChat/Models/Attachments/AnyAttachmentUpdater.swift
@@ -5,7 +5,7 @@
import Foundation
/// Helper component to update the payload of a type-erased attachment.
-public struct AnyAttachmentUpdater {
+public struct AnyAttachmentUpdater: Sendable {
public init() {}
/// Updates the underlying payload of a type-erased attachment.
diff --git a/Sources/StreamChat/Models/Attachments/AttachmentId.swift b/Sources/StreamChat/Models/Attachments/AttachmentId.swift
index d6cc57fdea7..0c2f05fb6bf 100644
--- a/Sources/StreamChat/Models/Attachments/AttachmentId.swift
+++ b/Sources/StreamChat/Models/Attachments/AttachmentId.swift
@@ -5,7 +5,7 @@
import Foundation
/// An object that uniquely identifies a message attachment.
-public struct AttachmentId: Hashable {
+public struct AttachmentId: Hashable, Sendable {
/// The cid of the channel the attachment belongs to.
public let cid: ChannelId
diff --git a/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift
index d1941d4cdab..25f470c8eeb 100644
--- a/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift
+++ b/Sources/StreamChat/Models/Attachments/AttachmentTypes.swift
@@ -23,7 +23,7 @@ enum AttachmentCodingKeys: String, CodingKey, CaseIterable {
}
/// A local state of the attachment. Applies only for attachments linked to the new messages sent from current device.
-public enum LocalAttachmentState: Hashable {
+public enum LocalAttachmentState: Hashable, Sendable {
/// The current state is unknown
case unknown
/// The attachment is waiting to be uploaded.
@@ -37,7 +37,7 @@ public enum LocalAttachmentState: Hashable {
}
/// A local download state of the attachment.
-public enum LocalAttachmentDownloadState: Hashable {
+public enum LocalAttachmentDownloadState: Hashable, Sendable {
/// The attachment is being downloaded.
case downloading(progress: Double)
/// The attachment download failed.
@@ -47,7 +47,7 @@ public enum LocalAttachmentDownloadState: Hashable {
}
/// An attachment action, e.g. send, shuffle.
-public struct AttachmentAction: Codable, Hashable {
+public struct AttachmentAction: Codable, Hashable, Sendable {
/// A name.
public let name: String
/// A value of an action.
@@ -84,12 +84,12 @@ public struct AttachmentAction: Codable, Hashable {
public var isCancel: Bool { value.lowercased() == "cancel" }
/// An attachment action type, e.g. button.
- public enum ActionType: String, Codable {
+ public enum ActionType: String, Codable, Sendable {
case button
}
/// An attachment action style, e.g. primary button.
- public enum ActionStyle: String, Codable {
+ public enum ActionStyle: String, Codable, Sendable {
case `default`
case primary
}
@@ -97,7 +97,7 @@ public struct AttachmentAction: Codable, Hashable {
/// An attachment type.
/// There are some predefined types on backend but any type can be introduced and sent to backend.
-public struct AttachmentType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral {
+public struct AttachmentType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable {
public let rawValue: String
public init(rawValue: String) {
@@ -140,7 +140,7 @@ public extension AttachmentType {
}
/// An attachment file description.
-public struct AttachmentFile: Codable, Hashable {
+public struct AttachmentFile: Codable, Hashable, Sendable {
enum CodingKeys: String, CodingKey, CaseIterable {
case mimeType = "mime_type"
case size = "file_size"
@@ -153,7 +153,7 @@ public struct AttachmentFile: Codable, Hashable {
/// A mime type.
public let mimeType: String?
/// A file size formatter.
- public static let sizeFormatter = ByteCountFormatter()
+ public static var sizeFormatter: ByteCountFormatter { ByteCountFormatter() }
// TODO: This should be deprecated in the future. UI Formatting should not belong to domain models.
// All formatting logic should come from `Appearance.formatters`.
@@ -213,7 +213,7 @@ public struct AttachmentFile: Codable, Hashable {
}
/// An attachment file type.
-public enum AttachmentFileType: String, Codable, Equatable, CaseIterable {
+public enum AttachmentFileType: String, Codable, Equatable, CaseIterable, Sendable {
/// File
case generic, doc, docx, pdf, ppt, pptx, tar, xls, zip, x7z, xz, ods, odt, xlsx
/// Text
@@ -318,7 +318,7 @@ public enum AttachmentFileType: String, Codable, Equatable, CaseIterable {
}
extension ClientError {
- final class InvalidAttachmentFileURL: ClientError {
+ final class InvalidAttachmentFileURL: ClientError, @unchecked Sendable {
init(_ url: URL) {
super.init("The \(url) is invalid since it is not a file URL.")
}
diff --git a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift
index d7baa0ce87a..39528a44643 100644
--- a/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift
+++ b/Sources/StreamChat/Models/Attachments/ChatMessageAttachment.swift
@@ -53,9 +53,10 @@ public extension ChatMessageAttachment {
extension ChatMessageAttachment: Equatable where Payload: Equatable {}
extension ChatMessageAttachment: Hashable where Payload: Hashable {}
+extension ChatMessageAttachment: Sendable where Payload: Sendable {}
/// A type represeting the downloading state for attachments.
-public struct AttachmentDownloadingState: Hashable {
+public struct AttachmentDownloadingState: Hashable, Sendable {
/// The local file URL of the downloaded attachment.
///
/// - Note: The local file URL is available when the state is `.downloaded`.
@@ -72,7 +73,7 @@ public struct AttachmentDownloadingState: Hashable {
}
/// A type representing the uploading state for attachments that require prior uploading.
-public struct AttachmentUploadingState: Hashable {
+public struct AttachmentUploadingState: Hashable, Sendable {
/// The local file URL that is being uploaded.
public let localFileURL: URL
diff --git a/Sources/StreamChat/Models/BanEnabling.swift b/Sources/StreamChat/Models/BanEnabling.swift
index 85640e4da47..bbe19396c4c 100644
--- a/Sources/StreamChat/Models/BanEnabling.swift
+++ b/Sources/StreamChat/Models/BanEnabling.swift
@@ -3,7 +3,7 @@
//
/// An option to enable ban users.
-public enum BanEnabling {
+public enum BanEnabling: Sendable {
/// Disabled for everyone.
case disabled
diff --git a/Sources/StreamChat/Models/BlockedUserDetails.swift b/Sources/StreamChat/Models/BlockedUserDetails.swift
index ccc908db837..52ee1787821 100644
--- a/Sources/StreamChat/Models/BlockedUserDetails.swift
+++ b/Sources/StreamChat/Models/BlockedUserDetails.swift
@@ -5,7 +5,7 @@
import Foundation
/// A type representing a blocked user.
-public struct BlockedUserDetails {
+public struct BlockedUserDetails: Sendable {
/// The unique identifier of the blocked user.
public let userId: UserId
diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift
index 77f811bba60..41ae3291d81 100644
--- a/Sources/StreamChat/Models/Channel.swift
+++ b/Sources/StreamChat/Models/Channel.swift
@@ -7,7 +7,7 @@ import Foundation
/// A type representing a chat channel. `ChatChannel` is an immutable snapshot of a channel entity at the given time.
///
-public struct ChatChannel {
+public struct ChatChannel: Sendable {
/// The `ChannelId` of the channel.
public let cid: ChannelId
@@ -331,7 +331,7 @@ extension ChatChannel: Hashable {
}
/// A struct describing unread counts for a channel.
-public struct ChannelUnreadCount: Decodable, Equatable {
+public struct ChannelUnreadCount: Decodable, Equatable, Sendable {
/// The default value representing no unread messages.
public static let noUnread = ChannelUnreadCount(messages: 0, mentions: 0)
@@ -353,7 +353,7 @@ public extension ChannelUnreadCount {
}
/// An action that can be performed in a channel.
-public struct ChannelCapability: RawRepresentable, ExpressibleByStringLiteral, Hashable {
+public struct ChannelCapability: RawRepresentable, ExpressibleByStringLiteral, Hashable, Sendable {
public var rawValue: String
public init?(rawValue: String) {
diff --git a/Sources/StreamChat/Models/ChannelId.swift b/Sources/StreamChat/Models/ChannelId.swift
index b2ffd0ccac5..cf0b5ac68b7 100644
--- a/Sources/StreamChat/Models/ChannelId.swift
+++ b/Sources/StreamChat/Models/ChannelId.swift
@@ -69,7 +69,7 @@ extension ChannelId: Codable {
}
extension ClientError {
- public final class InvalidChannelId: ClientError {}
+ public final class InvalidChannelId: ClientError, @unchecked Sendable {}
}
extension ChannelId: APIPathConvertible {
diff --git a/Sources/StreamChat/Models/ChannelRead.swift b/Sources/StreamChat/Models/ChannelRead.swift
index fa034ecfe0a..09e51c6fc7c 100644
--- a/Sources/StreamChat/Models/ChannelRead.swift
+++ b/Sources/StreamChat/Models/ChannelRead.swift
@@ -5,7 +5,7 @@
import Foundation
/// A type representing a user's last read action on a channel.
-public struct ChatChannelRead: Equatable {
+public struct ChatChannelRead: Equatable, Sendable {
/// The last time the user has read the channel.
public let lastReadAt: Date
diff --git a/Sources/StreamChat/Models/ChannelType.swift b/Sources/StreamChat/Models/ChannelType.swift
index f3016284224..b43baa01046 100644
--- a/Sources/StreamChat/Models/ChannelType.swift
+++ b/Sources/StreamChat/Models/ChannelType.swift
@@ -5,7 +5,7 @@
import Foundation
/// An enum describing possible types of a channel.
-public enum ChannelType: Codable, Hashable {
+public enum ChannelType: Codable, Hashable, Sendable {
/// Sensible defaults in case you want to build livestream chat like Instagram Livestream or Periscope.
case livestream
diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift
index f75be99d7f1..ce8123d24fb 100644
--- a/Sources/StreamChat/Models/ChatMessage.swift
+++ b/Sources/StreamChat/Models/ChatMessage.swift
@@ -9,7 +9,7 @@ import Foundation
public typealias MessageId = String
/// A type representing a chat message. `ChatMessage` is an immutable snapshot of a chat message entity at the given time.
-public struct ChatMessage {
+public struct ChatMessage: Sendable {
/// A unique identifier of the message.
public let id: MessageId
@@ -61,8 +61,8 @@ public struct ChatMessage {
///
/// If message is inline reply this property will contain the message quoted by this reply.
///
- public var quotedMessage: ChatMessage? { _quotedMessage() }
- let _quotedMessage: () -> ChatMessage?
+ public var quotedMessage: ChatMessage? { _quotedMessage?.value as? ChatMessage }
+ let _quotedMessage: BoxedAny?
/// The draft reply to this message. Applies only for the messages of the current user.
public let draftReply: DraftMessage?
@@ -257,7 +257,7 @@ public struct ChatMessage {
self.currentUserReactions = currentUserReactions
self.readBy = readBy
_attachments = attachments
- _quotedMessage = { quotedMessage }
+ _quotedMessage = BoxedAny(quotedMessage)
self.draftReply = draftReply
}
@@ -447,7 +447,7 @@ extension ChatMessage: Hashable {
}
/// A type of the message.
-public enum MessageType: String, Codable {
+public enum MessageType: String, Codable, Sendable {
/// A regular message created in the channel.
case regular
@@ -471,7 +471,7 @@ public enum MessageType: String, Codable {
}
// The pinning information of a message.
-public struct MessagePinDetails {
+public struct MessagePinDetails: Sendable {
/// Date when the message got pinned
public let pinnedAt: Date
@@ -483,7 +483,7 @@ public struct MessagePinDetails {
}
/// A possible additional local state of the message. Applies only for the messages of the current user.
-public enum LocalMessageState: String {
+public enum LocalMessageState: String, Sendable {
/// The message is waiting to be synced.
case pendingSync
/// The message is currently being synced
@@ -509,7 +509,7 @@ public enum LocalMessageState: String {
}
}
-public enum LocalReactionState: String {
+public enum LocalReactionState: String, Sendable {
/// The reaction state is unknown
case unknown = ""
@@ -533,7 +533,7 @@ public enum LocalReactionState: String {
}
/// The type describing message delivery status.
-public struct MessageDeliveryStatus: RawRepresentable, Hashable {
+public struct MessageDeliveryStatus: RawRepresentable, Hashable, Sendable {
public let rawValue: String
public init(rawValue: String) {
diff --git a/Sources/StreamChat/Models/CurrentUser.swift b/Sources/StreamChat/Models/CurrentUser.swift
index 5c1dde2daa3..a0e9c8638a1 100644
--- a/Sources/StreamChat/Models/CurrentUser.swift
+++ b/Sources/StreamChat/Models/CurrentUser.swift
@@ -22,7 +22,7 @@ extension UserId {
/// A type representing the currently logged-in user. `CurrentChatUser` is an immutable snapshot of a current user entity at
/// the given time.
///
-public class CurrentChatUser: ChatUser {
+public class CurrentChatUser: ChatUser, @unchecked Sendable {
/// A list of devices associcated with the user.
public let devices: [Device]
@@ -119,7 +119,7 @@ public class CurrentChatUser: ChatUser {
}
/// The total unread information from the current user.
-public struct CurrentUserUnreads {
+public struct CurrentUserUnreads: Sendable {
/// The total number of unread messages.
public let totalUnreadMessagesCount: Int
/// The total number of unread channels.
@@ -135,7 +135,7 @@ public struct CurrentUserUnreads {
}
/// The unread information of a channel.
-public struct UnreadChannel {
+public struct UnreadChannel: Sendable {
/// The channel id.
public let channelId: ChannelId
/// The number of unread messages inside the channel.
@@ -145,7 +145,7 @@ public struct UnreadChannel {
}
/// The unread information from channels with a specific type.
-public struct UnreadChannelByType {
+public struct UnreadChannelByType: Sendable {
/// The channel type.
public let channelType: ChannelType
/// The number of unread channels of this channel type.
@@ -155,7 +155,7 @@ public struct UnreadChannelByType {
}
/// The unread information of a thread.
-public struct UnreadThread {
+public struct UnreadThread: Sendable {
/// The message id of the root of the thread.
public let parentMessageId: MessageId
/// The number of unread replies inside the thread.
diff --git a/Sources/StreamChat/Models/Device.swift b/Sources/StreamChat/Models/Device.swift
index 569329aa02d..3ca388f30aa 100644
--- a/Sources/StreamChat/Models/Device.swift
+++ b/Sources/StreamChat/Models/Device.swift
@@ -13,7 +13,7 @@ extension Data {
}
/// An object representing a device which can receive push notifications.
-public struct Device: Codable, Equatable {
+public struct Device: Codable, Equatable, Sendable {
private enum CodingKeys: String, CodingKey {
case id
case createdAt = "created_at"
diff --git a/Sources/StreamChat/Models/DraftMessage.swift b/Sources/StreamChat/Models/DraftMessage.swift
index 8742794a357..2e636049925 100644
--- a/Sources/StreamChat/Models/DraftMessage.swift
+++ b/Sources/StreamChat/Models/DraftMessage.swift
@@ -4,7 +4,7 @@
import Foundation
-public struct DraftMessage {
+public struct DraftMessage: Sendable {
/// A unique identifier of the message.
public let id: MessageId
@@ -41,8 +41,8 @@ public struct DraftMessage {
/// Quoted message.
///
/// If message is inline reply this property will contain the message quoted by this reply.
- public var quotedMessage: ChatMessage? { _quotedMessage() }
- let _quotedMessage: () -> ChatMessage?
+ public var quotedMessage: ChatMessage? { _quotedMessage?.value as? ChatMessage }
+ let _quotedMessage: BoxedAny?
/// A list of users that are mentioned in this message.
public let mentionedUsers: Set
@@ -65,7 +65,7 @@ public struct DraftMessage {
showReplyInChannel: Bool,
extraData: [String: RawJSON],
currentUser: ChatUser,
- quotedMessage: @escaping () -> ChatMessage?,
+ quotedMessage: ChatMessage?,
mentionedUsers: Set,
attachments: [AnyChatMessageAttachment]
) {
@@ -79,7 +79,7 @@ public struct DraftMessage {
self.arguments = arguments
self.showReplyInChannel = showReplyInChannel
self.extraData = extraData
- _quotedMessage = quotedMessage
+ _quotedMessage = BoxedAny(quotedMessage)
self.mentionedUsers = mentionedUsers
self.attachments = attachments
self.currentUser = currentUser
@@ -96,7 +96,7 @@ public struct DraftMessage {
threadId = message.parentMessageId
showReplyInChannel = message.showReplyInChannel
extraData = message.extraData
- _quotedMessage = { message.quotedMessage }
+ _quotedMessage = BoxedAny(message.quotedMessage)
mentionedUsers = message.mentionedUsers
attachments = message.allAttachments
currentUser = message.author
@@ -137,7 +137,7 @@ extension ChatMessage {
showReplyInChannel = draft.showReplyInChannel
replyCount = 0
extraData = draft.extraData
- _quotedMessage = { draft.quotedMessage }
+ _quotedMessage = BoxedAny(draft.quotedMessage)
isBounced = false
isSilent = false
isShadowed = false
diff --git a/Sources/StreamChat/Models/Member.swift b/Sources/StreamChat/Models/Member.swift
index 7f3c559deef..0ce06714a8d 100644
--- a/Sources/StreamChat/Models/Member.swift
+++ b/Sources/StreamChat/Models/Member.swift
@@ -5,7 +5,7 @@
import Foundation
/// A type representing a chat channel member. `ChatChannelMember` is an immutable snapshot of a channel entity at the given time.
-public class ChatChannelMember: ChatUser {
+public class ChatChannelMember: ChatUser, @unchecked Sendable {
/// The role of the user within the channel.
public let memberRole: MemberRole
@@ -164,7 +164,7 @@ public class ChatChannelMember: ChatUser {
/// A `struct` describing roles of a member in a channel.
/// There are some predefined types but any type can be introduced and sent by the backend.
-public struct MemberRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral {
+public struct MemberRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable {
public let rawValue: String
public init(rawValue: String) {
diff --git a/Sources/StreamChat/Models/MessageModerationDetails.swift b/Sources/StreamChat/Models/MessageModerationDetails.swift
index e14d6a74335..c0287cd2094 100644
--- a/Sources/StreamChat/Models/MessageModerationDetails.swift
+++ b/Sources/StreamChat/Models/MessageModerationDetails.swift
@@ -5,7 +5,7 @@
import Foundation
/// Describes the details of a message which was moderated.
-public struct MessageModerationDetails {
+public struct MessageModerationDetails: Sendable {
/// The original message text.
public let originalText: String
/// The type of moderation performed to a message.
@@ -26,7 +26,7 @@ public struct MessageModerationDetails {
}
/// The type of moderation performed to a message.
-public struct MessageModerationAction: RawRepresentable, Equatable {
+public struct MessageModerationAction: RawRepresentable, Equatable, Sendable {
public let rawValue: String
public init(rawValue: String) {
diff --git a/Sources/StreamChat/Models/MessagePinning.swift b/Sources/StreamChat/Models/MessagePinning.swift
index dfe15f5cd14..9c10d21fc93 100644
--- a/Sources/StreamChat/Models/MessagePinning.swift
+++ b/Sources/StreamChat/Models/MessagePinning.swift
@@ -5,7 +5,7 @@
import Foundation
/// Describes the pinning expiration
-public struct MessagePinning: Equatable {
+public struct MessagePinning: Equatable, Sendable {
/// The expiration date of the pinning. Infinite expiration in case it is `nil`.
public let expirationDate: Date?
diff --git a/Sources/StreamChat/Models/MessageReaction.swift b/Sources/StreamChat/Models/MessageReaction.swift
index fe81ae467b1..efa433ccaa3 100644
--- a/Sources/StreamChat/Models/MessageReaction.swift
+++ b/Sources/StreamChat/Models/MessageReaction.swift
@@ -6,7 +6,7 @@ import Foundation
/// A type representing a message reaction. `ChatMessageReaction` is an immutable snapshot
/// of a message reaction entity at the given time.
-public struct ChatMessageReaction: Hashable {
+public struct ChatMessageReaction: Hashable, Sendable {
/// The id of the reaction.
let id: String
diff --git a/Sources/StreamChat/Models/MessageReactionGroup.swift b/Sources/StreamChat/Models/MessageReactionGroup.swift
index 4dbfe5392dd..6644ac7efe6 100644
--- a/Sources/StreamChat/Models/MessageReactionGroup.swift
+++ b/Sources/StreamChat/Models/MessageReactionGroup.swift
@@ -5,7 +5,7 @@
import Foundation
/// All the reactions information about a specific type of reaction.
-public struct ChatMessageReactionGroup: Equatable {
+public struct ChatMessageReactionGroup: Equatable, Sendable {
/// The type of reaction.
public let type: MessageReactionType
/// The sum of all reaction scores for this type of reaction.
diff --git a/Sources/StreamChat/Models/MessageReactionType.swift b/Sources/StreamChat/Models/MessageReactionType.swift
index 8d7dc6019a3..07179e69d9e 100644
--- a/Sources/StreamChat/Models/MessageReactionType.swift
+++ b/Sources/StreamChat/Models/MessageReactionType.swift
@@ -10,7 +10,7 @@ import Foundation
/// will be displayed in the application.
///
/// Common examples are: "like", "love", "smile", etc.
-public struct MessageReactionType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral {
+public struct MessageReactionType: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable {
// MARK: - RawRepresentable
public let rawValue: String
diff --git a/Sources/StreamChat/Models/MuteDetails.swift b/Sources/StreamChat/Models/MuteDetails.swift
index 06ae3f8f183..1c535c3c235 100644
--- a/Sources/StreamChat/Models/MuteDetails.swift
+++ b/Sources/StreamChat/Models/MuteDetails.swift
@@ -5,7 +5,7 @@
import Foundation
/// Describes user/channel mute details.
-public struct MuteDetails: Equatable {
+public struct MuteDetails: Equatable, Sendable {
/// The time when the mute action was taken.
public let createdAt: Date
/// The time when the mute was updated.
diff --git a/Sources/StreamChat/Models/Poll.swift b/Sources/StreamChat/Models/Poll.swift
index 6ecb5328329..970f9cd13bc 100644
--- a/Sources/StreamChat/Models/Poll.swift
+++ b/Sources/StreamChat/Models/Poll.swift
@@ -5,7 +5,7 @@
import Foundation
/// The model for a Poll.
-public struct Poll: Equatable {
+public struct Poll: Equatable, Sendable {
/// A boolean indicating whether the poll allows answers/comments.
public let allowAnswers: Bool
diff --git a/Sources/StreamChat/Models/PollOption.swift b/Sources/StreamChat/Models/PollOption.swift
index 2e7d721db7f..70d3d23fb16 100644
--- a/Sources/StreamChat/Models/PollOption.swift
+++ b/Sources/StreamChat/Models/PollOption.swift
@@ -5,7 +5,7 @@
import Foundation
/// The model for an option in a poll.
-public struct PollOption: Hashable, Equatable {
+public struct PollOption: Hashable, Equatable, Sendable {
/// The unique identifier of the poll option.
public let id: String
diff --git a/Sources/StreamChat/Models/PollVote.swift b/Sources/StreamChat/Models/PollVote.swift
index 233fe2c6eda..6cdee431e8d 100644
--- a/Sources/StreamChat/Models/PollVote.swift
+++ b/Sources/StreamChat/Models/PollVote.swift
@@ -5,7 +5,7 @@
import Foundation
/// A structure representing a vote in a poll.
-public struct PollVote: Hashable, Equatable {
+public struct PollVote: Hashable, Equatable, Sendable {
/// The unique identifier of the poll vote.
public let id: String
diff --git a/Sources/StreamChat/Models/PushProvider.swift b/Sources/StreamChat/Models/PushProvider.swift
index 8804d6ee8d6..07ce65b63b5 100644
--- a/Sources/StreamChat/Models/PushProvider.swift
+++ b/Sources/StreamChat/Models/PushProvider.swift
@@ -5,7 +5,7 @@
import Foundation
/// A type that represents the supported push providers.
-public struct PushProvider: RawRepresentable, Hashable, ExpressibleByStringLiteral {
+public struct PushProvider: RawRepresentable, Hashable, ExpressibleByStringLiteral, Sendable {
public static let firebase: Self = "firebase"
public static let apn: Self = "apn"
diff --git a/Sources/StreamChat/Models/Thread.swift b/Sources/StreamChat/Models/Thread.swift
index 55a4d4ef507..1058100a952 100644
--- a/Sources/StreamChat/Models/Thread.swift
+++ b/Sources/StreamChat/Models/Thread.swift
@@ -5,7 +5,7 @@
import Foundation
/// A type representing a thread.
-public struct ChatThread {
+public struct ChatThread: Sendable {
/// The id of the message which created the thread. It is also the id of the thread.
public let parentMessageId: MessageId
/// The parent message which is the root of this thread.
diff --git a/Sources/StreamChat/Models/ThreadParticipant.swift b/Sources/StreamChat/Models/ThreadParticipant.swift
index 6cc2f9f0cff..4516edaf11e 100644
--- a/Sources/StreamChat/Models/ThreadParticipant.swift
+++ b/Sources/StreamChat/Models/ThreadParticipant.swift
@@ -5,7 +5,7 @@
import Foundation
/// The details of a participant in a thread.
-public struct ThreadParticipant: Equatable {
+public struct ThreadParticipant: Equatable, Sendable {
/// The user information of the participant.
public let user: ChatUser
/// The id of the thread, which is also the id of the parent message.
diff --git a/Sources/StreamChat/Models/ThreadRead.swift b/Sources/StreamChat/Models/ThreadRead.swift
index 1995ea8daf8..43e5bb6eeae 100644
--- a/Sources/StreamChat/Models/ThreadRead.swift
+++ b/Sources/StreamChat/Models/ThreadRead.swift
@@ -5,7 +5,7 @@
import Foundation
/// The information about a thread read.
-public struct ThreadRead: Equatable {
+public struct ThreadRead: Equatable, Sendable {
/// The user which the read belongs to.
public let user: ChatUser
/// The date when the user last read the thread.
diff --git a/Sources/StreamChat/Models/UnreadCount.swift b/Sources/StreamChat/Models/UnreadCount.swift
index d0e03eeb68b..121ff8f4cc2 100644
--- a/Sources/StreamChat/Models/UnreadCount.swift
+++ b/Sources/StreamChat/Models/UnreadCount.swift
@@ -5,7 +5,7 @@
import Foundation
/// A struct containing information about unread counts of channels and messages.
-public struct UnreadCount: Decodable, Equatable {
+public struct UnreadCount: Decodable, Equatable, Sendable {
/// The default value representing no unread channels, messages and threads.
public static let noUnread = UnreadCount(channels: 0, messages: 0, threads: 0)
diff --git a/Sources/StreamChat/Models/User.swift b/Sources/StreamChat/Models/User.swift
index a71a3dd92e4..cfc8adfa403 100644
--- a/Sources/StreamChat/Models/User.swift
+++ b/Sources/StreamChat/Models/User.swift
@@ -12,7 +12,7 @@ public typealias TeamId = String
/// A type representing a chat user. `ChatUser` is an immutable snapshot of a chat user entity at the given time.
///
-public class ChatUser {
+public class ChatUser: @unchecked Sendable {
/// The unique identifier of the user.
public let id: UserId
@@ -128,7 +128,7 @@ extension ChatUser: Equatable {
}
}
-public struct UserRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral {
+public struct UserRole: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable {
public let rawValue: String
public init(rawValue: String) {
diff --git a/Sources/StreamChat/Models/UserInfo.swift b/Sources/StreamChat/Models/UserInfo.swift
index a108ef0327a..000c9da7631 100644
--- a/Sources/StreamChat/Models/UserInfo.swift
+++ b/Sources/StreamChat/Models/UserInfo.swift
@@ -5,7 +5,7 @@
import Foundation
/// A model containing user info that's used to connect to chat's backend
-public struct UserInfo {
+public struct UserInfo: Sendable {
/// The id of the user.
public let id: UserId
/// The name of the user.
@@ -45,7 +45,7 @@ public struct UserInfo {
}
/// The privacy settings of the user.
-public struct UserPrivacySettings {
+public struct UserPrivacySettings: Sendable {
/// The settings for typing indicator events.
public var typingIndicators: TypingIndicatorPrivacySettings?
/// The settings for the read receipt events.
@@ -61,7 +61,7 @@ public struct UserPrivacySettings {
}
/// The settings for typing indicator events.
-public struct TypingIndicatorPrivacySettings {
+public struct TypingIndicatorPrivacySettings: Sendable {
public var enabled: Bool
public init(enabled: Bool = true) {
@@ -70,7 +70,7 @@ public struct TypingIndicatorPrivacySettings {
}
/// The settings for the read receipt events.
-public struct ReadReceiptsPrivacySettings {
+public struct ReadReceiptsPrivacySettings: Sendable {
public var enabled: Bool
public init(enabled: Bool = true) {
diff --git a/Sources/StreamChat/Query/ChannelListQuery.swift b/Sources/StreamChat/Query/ChannelListQuery.swift
index ff4f35cf76b..4436b5a509f 100644
--- a/Sources/StreamChat/Query/ChannelListQuery.swift
+++ b/Sources/StreamChat/Query/ChannelListQuery.swift
@@ -233,7 +233,7 @@ internal extension FilterKey where Scope: AnyChannelListFilterScope {
/// A query is used for querying specific channels from backend.
/// You can specify filter, sorting, pagination, limit for fetched messages in channel and other options.
-public struct ChannelListQuery: Encodable {
+public struct ChannelListQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case filter = "filter_conditions"
case sort
diff --git a/Sources/StreamChat/Query/ChannelMemberListQuery.swift b/Sources/StreamChat/Query/ChannelMemberListQuery.swift
index 7cb79b2c534..ab8b896ab20 100644
--- a/Sources/StreamChat/Query/ChannelMemberListQuery.swift
+++ b/Sources/StreamChat/Query/ChannelMemberListQuery.swift
@@ -59,7 +59,7 @@ public extension FilterKey where Scope: AnyMemberListFilterScope {
}
/// A query type used for fetching channel members from the backend.
-public struct ChannelMemberListQuery: Encodable {
+public struct ChannelMemberListQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case filter = "filter_conditions"
case sort
@@ -121,6 +121,14 @@ extension ChannelMemberListQuery {
}
}
+extension ChannelMemberListQuery {
+ func withPagination(_ pagination: Pagination) -> Self {
+ var query = self
+ query.pagination = pagination
+ return query
+ }
+}
+
extension ChannelMemberListQuery {
/// Builds `ChannelMemberListQuery` for a single member in a specific channel
/// - Parameters:
diff --git a/Sources/StreamChat/Query/ChannelQuery.swift b/Sources/StreamChat/Query/ChannelQuery.swift
index 579086d0c43..ea6e6df15b5 100644
--- a/Sources/StreamChat/Query/ChannelQuery.swift
+++ b/Sources/StreamChat/Query/ChannelQuery.swift
@@ -5,7 +5,7 @@
import Foundation
/// A channel query.
-public struct ChannelQuery: Encodable {
+public struct ChannelQuery: Encodable, Sendable {
enum CodingKeys: String, CodingKey {
case data
case messages
diff --git a/Sources/StreamChat/Query/ChannelWatcherListQuery.swift b/Sources/StreamChat/Query/ChannelWatcherListQuery.swift
index 260ccb871a7..6ceed4e3e23 100644
--- a/Sources/StreamChat/Query/ChannelWatcherListQuery.swift
+++ b/Sources/StreamChat/Query/ChannelWatcherListQuery.swift
@@ -8,7 +8,7 @@ import Foundation
///
/// Learn more about watchers in our documentation [here](https://getstream.io/chat/docs/ios/watch_channel/?language=swift)
///
-public struct ChannelWatcherListQuery: Encodable {
+public struct ChannelWatcherListQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case watchers
}
diff --git a/Sources/StreamChat/Query/DraftListQuery.swift b/Sources/StreamChat/Query/DraftListQuery.swift
index 1c2367f219f..5a4bf9a0a4a 100644
--- a/Sources/StreamChat/Query/DraftListQuery.swift
+++ b/Sources/StreamChat/Query/DraftListQuery.swift
@@ -5,7 +5,7 @@
import Foundation
/// A query used to fetch the drafts of the current user.
-public struct DraftListQuery: Encodable {
+public struct DraftListQuery: Encodable, Sendable {
/// The pagination information to query the votes.
public var pagination: Pagination
/// The sorting parameter. By default drafts are sorted by newest first.
diff --git a/Sources/StreamChat/Query/Filter.swift b/Sources/StreamChat/Query/Filter.swift
index aa3a633a661..067195ee0bb 100644
--- a/Sources/StreamChat/Query/Filter.swift
+++ b/Sources/StreamChat/Query/Filter.swift
@@ -63,7 +63,7 @@ public protocol FilterScope {}
///
/// Only types representing text, numbers, booleans, dates, and other filters can be on the "right-hand" side of `Filter`.
///
-public protocol FilterValue: Encodable {}
+public protocol FilterValue: Encodable, Sendable {}
// Built-in `FilterValue` conformances for supported types
@@ -89,7 +89,7 @@ extension Optional: FilterValue where Wrapped == TeamId {}
///
/// Learn more about how to create simple, advanced, and custom filters in our [cheat sheet](https://github.com/GetStream/stream-chat-swift/wiki/StreamChat-SDK-Cheat-Sheet#query-filters).
///
-public struct Filter {
+public struct Filter: Sendable {
/// An operator used for the filter.
public let `operator`: String
@@ -102,7 +102,7 @@ public struct Filter {
/// The mapper that will transform the input value to a value that
/// can be compared with the DB value
- typealias ValueMapper = (Any) -> FilterValue?
+ typealias ValueMapper = @Sendable(Any) -> FilterValue?
let valueMapper: ValueMapper?
/// The keypath of the DB object that will be compared with the input value during
@@ -114,7 +114,7 @@ public struct Filter {
/// The mapper that will override the DB Predicate. This might be needed
/// for cases where our DB value is completely different from the server value.
- typealias PredicateMapper = (FilterOperator, Any) -> NSPredicate?
+ typealias PredicateMapper = @Sendable(FilterOperator, Any) -> NSPredicate?
let predicateMapper: PredicateMapper?
init(
@@ -224,7 +224,7 @@ public extension Filter {
///
/// Learn more about how to create filter keys for your custom extra data in our [cheat sheet](https://github.com/GetStream/stream-chat-swift/wiki/StreamChat-SDK-Cheat-Sheet#query-filters).
///
-public struct FilterKey: ExpressibleByStringLiteral, RawRepresentable {
+public struct FilterKey: ExpressibleByStringLiteral, RawRepresentable, Sendable {
/// The raw value of the key. This value should match the "encodable" key for the given object.
public let rawValue: String
@@ -234,14 +234,14 @@ public struct FilterKey: ExpressibleBySt
/// The mapper that will transform the input value to a value that
/// can be compared with the DB value
- typealias ValueMapper = (Any) -> FilterValue?
- typealias TypedValueMapper = (Value) -> FilterValue?
+ typealias ValueMapper = @Sendable(Any) -> FilterValue?
+ typealias TypedValueMapper = @Sendable(Value) -> FilterValue?
let valueMapper: ValueMapper?
/// The mapper that will override the DB Predicate. This might be needed
/// for cases where our DB value is completely different from the server value.
- typealias PredicateMapper = (FilterOperator, Any) -> NSPredicate?
- typealias TypedPredicateMapper = (FilterOperator, Value) -> NSPredicate?
+ typealias PredicateMapper = @Sendable(FilterOperator, Any) -> NSPredicate?
+ typealias TypedPredicateMapper = @Sendable(FilterOperator, Value) -> NSPredicate?
let predicateMapper: PredicateMapper?
let isCollectionFilter: Bool
diff --git a/Sources/StreamChat/Query/MessageSearchQuery.swift b/Sources/StreamChat/Query/MessageSearchQuery.swift
index 2d2b6d1cf7a..ff4ec4ceb50 100644
--- a/Sources/StreamChat/Query/MessageSearchQuery.swift
+++ b/Sources/StreamChat/Query/MessageSearchQuery.swift
@@ -43,9 +43,9 @@ public enum MessageSearchSortingKey: String, SortingKey {
}
/// Default sort descriptor for Message search. Corresponds to `created_at`
- static let defaultSortDescriptor: NSSortDescriptor = {
+ static var defaultSortDescriptor: NSSortDescriptor {
NSSortDescriptor(keyPath: \MessageDTO.defaultSortingKey, ascending: true)
- }()
+ }
func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? {
canUseAsSortDescriptor ? .init(key: rawValue, ascending: isAscending) : nil
@@ -83,7 +83,7 @@ public extension Filter where Scope: AnyMessageSearchFilterScope {
}
}
-public struct MessageSearchQuery: Encodable {
+public struct MessageSearchQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case query
case channelFilter = "filter_conditions"
diff --git a/Sources/StreamChat/Query/Pagination.swift b/Sources/StreamChat/Query/Pagination.swift
index d36ee1e1052..12e6c3a65ec 100644
--- a/Sources/StreamChat/Query/Pagination.swift
+++ b/Sources/StreamChat/Query/Pagination.swift
@@ -19,7 +19,7 @@ public extension Int {
/// Basic pagination with `pageSize` and `offset`.
/// Used everywhere except `ChannelQuery`. (See `MessagesPagination`)
-public struct Pagination: Encodable, Equatable {
+public struct Pagination: Encodable, Equatable, Sendable {
/// A page size.
public let pageSize: Int
/// An offset.
@@ -56,7 +56,7 @@ public struct Pagination: Encodable, Equatable {
}
}
-public struct MessagesPagination: Encodable, Equatable {
+public struct MessagesPagination: Encodable, Equatable, Sendable {
/// A page size
public let pageSize: Int
/// Parameter for pagination.
@@ -79,7 +79,7 @@ public struct MessagesPagination: Encodable, Equatable {
}
/// Pagination parameters
-public enum PaginationParameter: Encodable, Hashable {
+public enum PaginationParameter: Encodable, Hashable, Sendable {
enum CodingKeys: String, CodingKey {
case greaterThan = "id_gt"
case greaterThanOrEqual = "id_gte"
diff --git a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift
index 005fbe5c6ee..5e221ca6533 100644
--- a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift
+++ b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesPagination.swift
@@ -5,7 +5,7 @@
import Foundation
/// Pagination options available when paginating pinned messages.
-public enum PinnedMessagesPagination: Hashable {
+public enum PinnedMessagesPagination: Hashable, Sendable {
/// When used, the backend returns messages around the message with the given id.
case aroundMessage(_ messageId: MessageId)
diff --git a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift
index 14b1e610031..98c4e8624ba 100644
--- a/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift
+++ b/Sources/StreamChat/Query/PinnedMessages/PinnedMessagesQuery.swift
@@ -5,7 +5,7 @@
import Foundation
/// The query used to paginate pinned messages.
-struct PinnedMessagesQuery: Hashable {
+struct PinnedMessagesQuery: Hashable, Sendable {
/// The page size.
let pageSize: Int
diff --git a/Sources/StreamChat/Query/PollVoteListQuery.swift b/Sources/StreamChat/Query/PollVoteListQuery.swift
index 7f7d75c0962..61d280a1b97 100644
--- a/Sources/StreamChat/Query/PollVoteListQuery.swift
+++ b/Sources/StreamChat/Query/PollVoteListQuery.swift
@@ -5,7 +5,7 @@
import Foundation
/// A query used for querying specific votes from a poll.
-public struct PollVoteListQuery: Encodable {
+public struct PollVoteListQuery: Encodable, Sendable {
/// The pollId which the votes belong to.
public var pollId: String
/// The optionId which the votes belong to in case the query relates to only one poll option.
diff --git a/Sources/StreamChat/Query/QueryOptions.swift b/Sources/StreamChat/Query/QueryOptions.swift
index bdf6c756220..2d6b6405fee 100644
--- a/Sources/StreamChat/Query/QueryOptions.swift
+++ b/Sources/StreamChat/Query/QueryOptions.swift
@@ -5,7 +5,7 @@
import Foundation
/// Query options.
-public struct QueryOptions: OptionSet, Encodable {
+public struct QueryOptions: OptionSet, Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case state
case watch
diff --git a/Sources/StreamChat/Query/ReactionListQuery.swift b/Sources/StreamChat/Query/ReactionListQuery.swift
index 609114d0735..71472574a1a 100644
--- a/Sources/StreamChat/Query/ReactionListQuery.swift
+++ b/Sources/StreamChat/Query/ReactionListQuery.swift
@@ -5,7 +5,7 @@
import Foundation
/// A query used for querying specific reactions from a message.
-public struct ReactionListQuery: Encodable {
+public struct ReactionListQuery: Encodable, Sendable {
/// The message id that the reactions belong to.
public var messageId: MessageId
/// The pagination information to query the reactions.
diff --git a/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift b/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
index 5b47b11f066..696c1678247 100644
--- a/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
+++ b/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
@@ -104,10 +104,10 @@ extension ChannelListSortingKey: CustomDebugStringConvertible {
}
extension ChannelListSortingKey {
- static let defaultSortDescriptor: NSSortDescriptor = {
+ static var defaultSortDescriptor: NSSortDescriptor {
let dateKeyPath: KeyPath = \ChannelDTO.defaultSortingAt
return .init(keyPath: dateKeyPath, ascending: false)
- }()
+ }
func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? {
guard let localKey = self.localKey else {
@@ -142,3 +142,11 @@ extension ChatChannel {
lastMessageAt ?? createdAt
}
}
+
+// Remove when InferSendableFromCaptures is enabled
+// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0418-inferring-sendable-for-methods.md
+#if compiler(>=6.0)
+extension PartialKeyPath: @retroactive @unchecked Sendable where Root == ChatChannel {}
+#else
+extension PartialKeyPath: @unchecked Sendable where Root == ChatChannel {}
+#endif
diff --git a/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift b/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
index f580bf43330..1699305cf83 100644
--- a/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
+++ b/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
@@ -36,10 +36,10 @@ public enum ChannelMemberListSortingKey: String, SortingKey {
}
extension ChannelMemberListSortingKey {
- static let defaultSortDescriptor: NSSortDescriptor = {
+ static var defaultSortDescriptor: NSSortDescriptor {
let dateKeyPath: KeyPath = \MemberDTO.memberCreatedAt
return .init(keyPath: dateKeyPath, ascending: false)
- }()
+ }
func sortDescriptor(isAscending: Bool) -> NSSortDescriptor {
.init(key: rawValue, ascending: isAscending)
diff --git a/Sources/StreamChat/Query/Sorting/Sorting.swift b/Sources/StreamChat/Query/Sorting/Sorting.swift
index 1682986d0f0..c9cc2eef291 100644
--- a/Sources/StreamChat/Query/Sorting/Sorting.swift
+++ b/Sources/StreamChat/Query/Sorting/Sorting.swift
@@ -5,7 +5,7 @@
import Foundation
/// A sorting key protocol.
-public protocol SortingKey: Encodable {}
+public protocol SortingKey: Encodable, Sendable {}
/// Sorting options.
///
@@ -14,7 +14,7 @@ public protocol SortingKey: Encodable {}
/// // Sort channels by the last message date:
/// let sorting = Sorting("lastMessageDate")
/// ```
-public struct Sorting: Encodable, CustomStringConvertible {
+public struct Sorting: Encodable, CustomStringConvertible, Sendable {
/// A sorting field name.
public let key: Key
/// A sorting direction.
diff --git a/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift b/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
index de41f50441a..f88e01065d0 100644
--- a/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
+++ b/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
@@ -34,10 +34,10 @@ public enum UserListSortingKey: String, SortingKey {
}
extension UserListSortingKey {
- static let defaultSortDescriptor: NSSortDescriptor = {
+ static var defaultSortDescriptor: NSSortDescriptor {
let stringKeyPath: KeyPath = \UserDTO.id
return .init(keyPath: stringKeyPath, ascending: false)
- }()
+ }
func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? {
.init(key: rawValue, ascending: isAscending)
diff --git a/Sources/StreamChat/Query/ThreadListQuery.swift b/Sources/StreamChat/Query/ThreadListQuery.swift
index 5563469cea5..56e800d0354 100644
--- a/Sources/StreamChat/Query/ThreadListQuery.swift
+++ b/Sources/StreamChat/Query/ThreadListQuery.swift
@@ -5,7 +5,7 @@
import Foundation
/// A query to fetch the list of threads the current belongs to.
-public struct ThreadListQuery: Encodable {
+public struct ThreadListQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case watch
case replyLimit = "reply_limit"
diff --git a/Sources/StreamChat/Query/ThreadQuery.swift b/Sources/StreamChat/Query/ThreadQuery.swift
index 4ecaa08ca38..f6f08d1d003 100644
--- a/Sources/StreamChat/Query/ThreadQuery.swift
+++ b/Sources/StreamChat/Query/ThreadQuery.swift
@@ -6,7 +6,7 @@ import Foundation
/// A query to fetch information about a thread.
/// To fetch all the replies from a thread and paginate the replies, the `ChatMessageController` should be used instead.
-public struct ThreadQuery: Encodable {
+public struct ThreadQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case messageId = "message_id"
case watch
diff --git a/Sources/StreamChat/Query/UserListQuery.swift b/Sources/StreamChat/Query/UserListQuery.swift
index 210aa1481bd..71e1877036a 100644
--- a/Sources/StreamChat/Query/UserListQuery.swift
+++ b/Sources/StreamChat/Query/UserListQuery.swift
@@ -58,7 +58,7 @@ public extension FilterKey where Scope: AnyUserListFilterScope {
/// A query is used for querying specific users from backend.
/// You can specify filter, sorting and pagination.
-public struct UserListQuery: Encodable {
+public struct UserListQuery: Encodable, Sendable {
private enum CodingKeys: String, CodingKey {
case filter = "filter_conditions"
case sort
diff --git a/Sources/StreamChat/Repositories/AuthenticationRepository.swift b/Sources/StreamChat/Repositories/AuthenticationRepository.swift
index c4d323adcce..a200ddae7bd 100644
--- a/Sources/StreamChat/Repositories/AuthenticationRepository.swift
+++ b/Sources/StreamChat/Repositories/AuthenticationRepository.swift
@@ -4,7 +4,7 @@
import Foundation
-public typealias TokenProvider = (@escaping (Result) -> Void) -> Void
+public typealias TokenProvider = @Sendable(@escaping @Sendable(Result) -> Void) -> Void
enum EnvironmentState {
case firstConnection
@@ -27,7 +27,7 @@ protocol AuthenticationRepositoryDelegate: AnyObject {
func logOutUser(completion: @escaping () -> Void)
}
-class AuthenticationRepository {
+class AuthenticationRepository: @unchecked Sendable {
private enum Constants {
/// Maximum amount of consecutive token refresh attempts before failing
static let maximumTokenRefreshAttempts = 10
@@ -46,8 +46,8 @@ class AuthenticationRepository {
private var _currentToken: Token?
private var _tokenExpirationRetryStrategy: RetryStrategy
private var _tokenProvider: TokenProvider?
- private var _tokenRequestCompletions: [(Error?) -> Void] = []
- private var _tokenWaiters: [String: (Result) -> Void] = [:]
+ private var _tokenRequestCompletions: [@Sendable(Error?) -> Void] = []
+ private var _tokenWaiters: [String: @Sendable(Result) -> Void] = [:]
private var _tokenProviderTimer: TimerControl?
private var _connectionProviderTimer: TimerControl?
@@ -142,7 +142,7 @@ class AuthenticationRepository {
/// - Parameters:
/// - userInfo: The user information that will be created OR updated if it exists.
/// - tokenProvider: The block to be used to get a token.
- func connectUser(userInfo: UserInfo, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
+ func connectUser(userInfo: UserInfo, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) {
var logOutFirst: Bool {
if let currentUserId = currentUserId, currentUserId.isGuest {
return true
@@ -158,7 +158,7 @@ class AuthenticationRepository {
/// Establishes a connection for a guest user.
/// - Parameters:
/// - userInfo: The user information that will be created OR updated if it exists.
- func connectGuestUser(userInfo: UserInfo, completion: @escaping (Error?) -> Void) {
+ func connectGuestUser(userInfo: UserInfo, completion: @escaping @Sendable(Error?) -> Void) {
let tokenProvider: TokenProvider = { [weak self] completion in
self?.fetchGuestToken(userInfo: userInfo, completion: completion)
}
@@ -166,12 +166,12 @@ class AuthenticationRepository {
}
/// Establishes a connection for an anonymous user.
- func connectAnonymousUser(completion: @escaping (Error?) -> Void) {
+ func connectAnonymousUser(completion: @escaping @Sendable(Error?) -> Void) {
let tokenProvider: TokenProvider = { $0(.success(.anonymous)) }
executeTokenFetch(logOutFirst: true, userInfo: nil, tokenProvider: tokenProvider, completion: completion)
}
- private func executeTokenFetch(logOutFirst: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
+ private func executeTokenFetch(logOutFirst: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) {
log.assert(delegate != nil, "Delegate should not be nil at this point")
let handleTokenFetch = { [weak self] in
@@ -211,7 +211,7 @@ class AuthenticationRepository {
currentUserId = nil
}
- func refreshToken(completion: @escaping (Error?) -> Void) {
+ func refreshToken(completion: @escaping @Sendable(Error?) -> Void) {
guard let tokenProvider = tokenProvider else {
let error = ClientError.MissingTokenProvider()
log.assertionFailure(error.localizedDescription)
@@ -251,7 +251,7 @@ class AuthenticationRepository {
}
}
- func provideToken(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) {
+ func provideToken(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) {
if let token = currentToken {
completion(.success(token))
return
@@ -284,7 +284,7 @@ class AuthenticationRepository {
}
func completeTokenCompletions(error: Error?) {
- let completionBlocks: [(Error?) -> Void]? = tokenQueue.sync(flags: .barrier) {
+ let completionBlocks: [@Sendable(Error?) -> Void]? = tokenQueue.sync(flags: .barrier) {
self._isGettingToken = false
let completions = self._tokenRequestCompletions
return completions
@@ -297,7 +297,7 @@ class AuthenticationRepository {
}
private func updateToken(token: Token?, notifyTokenWaiters: Bool) {
- let waiters: [String: (Result) -> Void] = tokenQueue.sync(flags: .barrier) {
+ let waiters: [String: @Sendable(Result) -> Void] = tokenQueue.sync(flags: .barrier) {
_currentToken = token
_currentUserId = token?.userId
guard notifyTokenWaiters else { return [:] }
@@ -315,7 +315,7 @@ class AuthenticationRepository {
}
}
- private func scheduleTokenFetch(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
+ private func scheduleTokenFetch(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) {
guard !isGettingToken || isRetry else {
tokenQueue.async(flags: .barrier) {
self._tokenRequestCompletions.append(completion)
@@ -335,7 +335,7 @@ class AuthenticationRepository {
}
}
- private func getToken(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping (Error?) -> Void) {
+ private func getToken(isRetry: Bool, userInfo: UserInfo?, tokenProvider: @escaping TokenProvider, completion: @escaping @Sendable(Error?) -> Void) {
tokenQueue.async(flags: .barrier) {
self._tokenRequestCompletions.append(completion)
}
@@ -346,7 +346,7 @@ class AuthenticationRepository {
isGettingToken = true
- let onCompletion: (Error?) -> Void = { [weak self] error in
+ let onCompletion: @Sendable(Error?) -> Void = { [weak self] error in
if let error = error {
log.error("Error when getting token: \(error)", subsystems: .authentication)
} else {
@@ -360,7 +360,7 @@ class AuthenticationRepository {
return
}
- let onTokenReceived: (Token) -> Void = { [weak self, weak connectionRepository] token in
+ let onTokenReceived: @Sendable(Token) -> Void = { [weak self, weak connectionRepository] token in
self?.isGettingToken = false
self?.prepareEnvironment(userInfo: userInfo, newToken: token)
// We manually change the `connectionStatus` for passive client
@@ -370,7 +370,7 @@ class AuthenticationRepository {
connectionRepository?.connect(completion: onCompletion)
}
- let retryFetchIfPossible: (Error?) -> Void = { [weak self] error in
+ let retryFetchIfPossible: @Sendable(Error?) -> Void = { [weak self] error in
guard let self = self else { return }
self.tokenQueue.async(flags: .barrier) {
self._consecutiveRefreshFailures += 1
@@ -401,7 +401,7 @@ class AuthenticationRepository {
private func fetchGuestToken(
userInfo: UserInfo,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
let endpoint: Endpoint = .guestUserToken(
userId: userInfo.id,
@@ -427,7 +427,7 @@ class AuthenticationRepository {
}
extension ClientError {
- public final class TooManyFailedTokenRefreshAttempts: ClientError {
+ public final class TooManyFailedTokenRefreshAttempts: ClientError, @unchecked Sendable {
override public var localizedDescription: String {
"""
Token fetch has failed more than 10 times.
diff --git a/Sources/StreamChat/Repositories/ChannelRepository.swift b/Sources/StreamChat/Repositories/ChannelRepository.swift
index b46561c8ab4..14fe7f16fe2 100644
--- a/Sources/StreamChat/Repositories/ChannelRepository.swift
+++ b/Sources/StreamChat/Repositories/ChannelRepository.swift
@@ -5,7 +5,7 @@
import CoreData
import Foundation
-class ChannelRepository {
+class ChannelRepository: @unchecked Sendable {
let database: DatabaseContainer
let apiClient: APIClient
@@ -14,7 +14,7 @@ class ChannelRepository {
self.apiClient = apiClient
}
- func getChannel(for query: ChannelQuery, store: Bool, completion: @escaping (Result) -> Void) {
+ func getChannel(for query: ChannelQuery, store: Bool, completion: @escaping @Sendable(Result) -> Void) {
let endpoint: Endpoint = query.options == .state ? .channelState(query: query) : .updateChannel(query: query)
apiClient.request(endpoint: endpoint) { [database] result in
switch result {
@@ -42,7 +42,7 @@ class ChannelRepository {
func markRead(
cid: ChannelId,
userId: UserId,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
apiClient.request(endpoint: .markRead(cid: cid)) { [weak self] result in
if let error = result.error {
@@ -71,7 +71,7 @@ class ChannelRepository {
userId: UserId,
from messageId: MessageId,
lastReadMessageId: MessageId?,
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
apiClient.request(
endpoint: .markUnread(cid: cid, messageId: messageId, userId: userId)
@@ -81,8 +81,7 @@ class ChannelRepository {
return
}
- var channel: ChatChannel?
- self?.database.write({ session in
+ self?.database.write(converting: { session in
session.markChannelAsUnread(
for: cid,
userId: userId,
@@ -91,13 +90,12 @@ class ChannelRepository {
lastReadAt: nil,
unreadMessagesCount: nil
)
- channel = try session.channel(cid: cid)?.asModel()
- }, completion: { error in
- if let channel = channel, error == nil {
- completion?(.success(channel))
- } else {
- completion?(.failure(error ?? ClientError.ChannelNotCreatedYet()))
+ guard let channelDTO = session.channel(cid: cid) else {
+ throw ClientError.ChannelDoesNotExist(cid: cid)
}
+ return try channelDTO.asModel()
+ }, completion: {
+ completion?($0)
})
}
}
diff --git a/Sources/StreamChat/Repositories/ConnectionRepository.swift b/Sources/StreamChat/Repositories/ConnectionRepository.swift
index f675e0285cb..3a4ea5a9c7a 100644
--- a/Sources/StreamChat/Repositories/ConnectionRepository.swift
+++ b/Sources/StreamChat/Repositories/ConnectionRepository.swift
@@ -4,9 +4,9 @@
import Foundation
-class ConnectionRepository {
+class ConnectionRepository: @unchecked Sendable {
private let connectionQueue: DispatchQueue = DispatchQueue(label: "io.getstream.connection-repository", attributes: .concurrent)
- private var _connectionIdWaiters: [String: (Result) -> Void] = [:]
+ private var _connectionIdWaiters: [String: @Sendable(Result) -> Void] = [:]
private var _connectionId: ConnectionId?
private var _connectionStatus: ConnectionStatus = .initialized
@@ -53,7 +53,7 @@ class ConnectionRepository {
/// - Parameters:
/// - completion: Called when the connection is established. If the connection fails, the completion is called with an error.
///
- func connect(completion: ((Error?) -> Void)? = nil) {
+ func connect(completion: (@Sendable(Error?) -> Void)? = nil) {
// Connecting is not possible in connectionless mode (duh)
guard isClientInActiveMode else {
completion?(ClientError.ClientIsNotInActiveMode())
@@ -87,7 +87,7 @@ class ConnectionRepository {
/// are received.
func disconnect(
source: WebSocketConnectionState.DisconnectionSource,
- completion: @escaping () -> Void
+ completion: @escaping @Sendable() -> Void
) {
apiClient.flushRequestsQueue()
syncRepository.cancelRecoveryFlow()
@@ -157,7 +157,7 @@ class ConnectionRepository {
)
}
- func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping (Result) -> Void) {
+ func provideConnectionId(timeout: TimeInterval = 10, completion: @escaping @Sendable(Result) -> Void) {
if let connectionId = connectionId {
completion(.success(connectionId))
return
@@ -210,7 +210,7 @@ class ConnectionRepository {
connectionId: String?,
shouldNotifyWaiters: Bool
) {
- let waiters: [String: (Result) -> Void] = connectionQueue.sync(flags: .barrier) {
+ let waiters: [String: @Sendable(Result) -> Void] = connectionQueue.sync(flags: .barrier) {
_connectionId = connectionId
guard shouldNotifyWaiters else { return [:] }
let waiters = _connectionIdWaiters
diff --git a/Sources/StreamChat/Repositories/DraftMessagesRepository.swift b/Sources/StreamChat/Repositories/DraftMessagesRepository.swift
index a54bdf08f79..dd20f787f32 100644
--- a/Sources/StreamChat/Repositories/DraftMessagesRepository.swift
+++ b/Sources/StreamChat/Repositories/DraftMessagesRepository.swift
@@ -4,12 +4,12 @@
import CoreData
-struct DraftListResponse {
+struct DraftListResponse: Sendable {
var drafts: [DraftMessage]
var next: String?
}
-class DraftMessagesRepository {
+class DraftMessagesRepository: @unchecked Sendable {
private let database: DatabaseContainer
private let apiClient: APIClient
@@ -20,14 +20,13 @@ class DraftMessagesRepository {
func loadDrafts(
query: DraftListQuery,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
apiClient.request(endpoint: .drafts(query: query)) { [weak self] result in
switch result {
case .success(let response):
- var drafts: [DraftMessage] = []
- self?.database.write({ session in
- drafts = try response.drafts.compactMap {
+ self?.database.write(converting: { session in
+ let drafts: [DraftMessage] = try response.drafts.compactMap {
guard let channelId = $0.channelPayload?.cid else {
return nil
}
@@ -35,13 +34,8 @@ class DraftMessagesRepository {
.saveDraftMessage(payload: $0, for: channelId, cache: nil)
.asModel())
}
- }, completion: { error in
- if let error {
- completion(.failure(error))
- return
- }
- completion(.success(DraftListResponse(drafts: drafts, next: response.next)))
- })
+ return DraftListResponse(drafts: drafts, next: response.next)
+ }, completion: completion)
case .failure(let error):
completion(.failure(error))
}
@@ -60,10 +54,9 @@ class DraftMessagesRepository {
mentionedUserIds: [UserId],
quotedMessageId: MessageId?,
extraData: [String: RawJSON],
- completion: ((Result) -> Void)?
+ completion: (@Sendable(Result) -> Void)?
) {
- var draftRequestBody: DraftMessageRequestBody?
- database.write({ (session) in
+ database.write(converting: { (session) in
let newMessageDTO = try session.createNewDraftMessage(
in: cid,
text: text,
@@ -77,36 +70,32 @@ class DraftMessagesRepository {
quotedMessageId: quotedMessageId,
extraData: extraData
)
- draftRequestBody = newMessageDTO.asDraftRequestBody()
- }) { error in
- guard let requestBody = draftRequestBody, error == nil else {
- completion?(.failure(error ?? ClientError.Unknown()))
- return
- }
-
- self.apiClient.request(
- endpoint: .updateDraftMessage(channelId: cid, requestBody: requestBody)
- ) { [weak self] result in
- switch result {
- case .success(let response):
- var draft: ChatMessage?
- self?.database.write({ session in
- let draftPayload = response.draft
- let messageDTO = try session.saveDraftMessage(
- payload: draftPayload,
- for: cid,
- cache: nil
- )
- draft = try messageDTO.asModel()
- }, completion: { error in
- if let draft {
- completion?(.success(DraftMessage(draft)))
- } else if let error {
- completion?(.failure(error))
- }
- })
- case .failure(let error):
- completion?(.failure(error))
+ return newMessageDTO.asDraftRequestBody()
+ }) { writeResult in
+ switch writeResult {
+ case .failure(let error):
+ completion?(.failure(error))
+ case .success(let requestBody):
+ self.apiClient.request(
+ endpoint: .updateDraftMessage(channelId: cid, requestBody: requestBody)
+ ) { [weak self] result in
+ switch result {
+ case .success(let response):
+ self?.database.write(converting: { session in
+ let draftPayload = response.draft
+ let messageDTO = try session.saveDraftMessage(
+ payload: draftPayload,
+ for: cid,
+ cache: nil
+ )
+ let message = try messageDTO.asModel()
+ return DraftMessage(message)
+ }, completion: {
+ completion?($0)
+ })
+ case .failure(let error):
+ completion?(.failure(error))
+ }
}
}
}
@@ -115,28 +104,24 @@ class DraftMessagesRepository {
func getDraft(
for cid: ChannelId,
threadId: MessageId?,
- completion: ((Result) -> Void)?
+ completion: (@Sendable(Result) -> Void)?
) {
apiClient.request(
endpoint: .getDraftMessage(channelId: cid, threadId: threadId)
) { [weak self] result in
switch result {
case .success(let response):
- var draft: ChatMessage?
- self?.database.write({ session in
+ self?.database.write(converting: { session in
let messageDTO = try session.saveDraftMessage(
payload: response.draft,
for: cid,
cache: nil
)
- draft = try messageDTO.asModel()
- }) { error in
- if let draft {
- completion?(.success(DraftMessage(draft)))
- } else if let error {
- completion?(.failure(error))
- }
- }
+ let message = try messageDTO.asModel()
+ return DraftMessage(message)
+ }, completion: {
+ completion?($0)
+ })
case .failure(let error):
completion?(.failure(error))
}
@@ -146,7 +131,7 @@ class DraftMessagesRepository {
func deleteDraft(
for cid: ChannelId,
threadId: MessageId?,
- completion: @escaping (Error?) -> Void
+ completion: @escaping @Sendable(Error?) -> Void
) {
database.write { session in
session.deleteDraftMessage(in: cid, threadId: threadId)
diff --git a/Sources/StreamChat/Repositories/MessageRepository.swift b/Sources/StreamChat/Repositories/MessageRepository.swift
index 392dad7b482..1ace2001ea6 100644
--- a/Sources/StreamChat/Repositories/MessageRepository.swift
+++ b/Sources/StreamChat/Repositories/MessageRepository.swift
@@ -12,7 +12,7 @@ enum MessageRepositoryError: LocalizedError {
case failedToSendMessage(Error)
}
-class MessageRepository {
+class MessageRepository: @unchecked Sendable {
let database: DatabaseContainer
let apiClient: APIClient
@@ -23,7 +23,7 @@ class MessageRepository {
func sendMessage(
with messageId: MessageId,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
// Check the message with the given id is still in the DB.
database.backgroundReadOnlyContext.perform { [weak self] in
@@ -54,7 +54,7 @@ class MessageRepository {
self?.database.write({
let messageDTO = $0.message(id: messageId)
messageDTO?.localMessageState = .sending
- }, completion: { error in
+ }, completion: { [weak self] error in
if let error = error {
log.error("Error changing localMessageState message with id \(messageId) to `sending`: \(error)")
self?.markMessageAsFailedToSend(id: messageId) {
@@ -69,7 +69,7 @@ class MessageRepository {
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl
)
- self?.apiClient.request(endpoint: endpoint) {
+ self?.apiClient.request(endpoint: endpoint) { [weak self] in
switch $0 {
case let .success(payload):
self?.saveSuccessfullySentMessage(cid: cid, message: payload.message) { result in
@@ -90,9 +90,9 @@ class MessageRepository {
}
/// Marks the message's local status to failed and adds it to the offline retry which sends the message when connection comes back.
- func scheduleOfflineRetry(for messageId: MessageId, completion: @escaping (Result) -> Void) {
- var dataEndpoint: DataEndpoint!
- var messageModel: ChatMessage!
+ func scheduleOfflineRetry(for messageId: MessageId, completion: @escaping @Sendable(Result) -> Void) {
+ nonisolated(unsafe) var dataEndpoint: DataEndpoint!
+ nonisolated(unsafe) var messageModel: ChatMessage!
database.write { session in
guard let dto = session.message(id: messageId) else {
throw MessageRepositoryError.messageDoesNotExist
@@ -133,9 +133,9 @@ class MessageRepository {
func saveSuccessfullySentMessage(
cid: ChannelId,
message: MessagePayload,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
- var messageModel: ChatMessage!
+ nonisolated(unsafe) var messageModel: ChatMessage!
database.write({
let messageDTO = try $0.saveMessage(
payload: message,
@@ -161,7 +161,7 @@ class MessageRepository {
private func handleSendingMessageError(
_ error: Error,
messageId: MessageId,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
log.error("Sending the message with id \(messageId) failed with error: \(error)")
@@ -188,7 +188,7 @@ class MessageRepository {
}
}
- private func markMessageAsFailedToSend(id: MessageId, completion: @escaping () -> Void) {
+ private func markMessageAsFailedToSend(id: MessageId, completion: @escaping @Sendable() -> Void) {
database.write({
let dto = $0.message(id: id)
if dto?.localMessageState == .sending {
@@ -205,11 +205,11 @@ class MessageRepository {
})
}
- func saveSuccessfullyEditedMessage(for id: MessageId, completion: @escaping () -> Void) {
+ func saveSuccessfullyEditedMessage(for id: MessageId, completion: @escaping @Sendable() -> Void) {
updateMessage(withID: id, localState: nil, completion: { _ in completion() })
}
- func saveSuccessfullyDeletedMessage(message: MessagePayload, completion: ((Error?) -> Void)? = nil) {
+ func saveSuccessfullyDeletedMessage(message: MessagePayload, completion: (@Sendable(Error?) -> Void)? = nil) {
database.write({ session in
guard let messageDTO = session.message(id: message.id), let cid = messageDTO.channel?.cid else { return }
let deletedMessage = try session.saveMessage(
@@ -238,34 +238,28 @@ class MessageRepository {
/// - messageId: The message identifier.
/// - store: A boolean indicating if the message should be stored to database or should only be retrieved
/// - completion: The completion. Will be called with an error if something goes wrong, otherwise - will be called with `nil`.
- func getMessage(cid: ChannelId, messageId: MessageId, store: Bool, completion: ((Result) -> Void)? = nil) {
+ func getMessage(cid: ChannelId, messageId: MessageId, store: Bool, completion: (@Sendable(Result) -> Void)? = nil) {
let endpoint: Endpoint = .getMessage(messageId: messageId)
apiClient.request(endpoint: endpoint) {
switch $0 {
case let .success(boxed):
- var message: ChatMessage?
- self.database.write({ session in
- message = try session.saveMessage(
+ self.database.write(converting: { session in
+ let message = try session.saveMessage(
payload: boxed.message,
for: cid,
syncOwnReactions: true,
skipDraftUpdate: false,
cache: nil
- ).asModel()
+ )
+ .asModel()
if !store {
// Force load attachments before discarding changes
- _ = message?.attachmentCounts
+ _ = message.attachmentCounts
self.database.writableContext.rollback()
}
- }, completion: { error in
- if let error = error {
- completion?(.failure(error))
- } else if let message = message {
- completion?(.success(message))
- } else {
- let error = ClientError.MessagePayloadSavingFailure("Missing message or error")
- completion?(.failure(error))
- }
+ return message
+ }, completion: { result in
+ completion?(result)
})
case let .failure(error):
completion?(.failure(error))
@@ -299,25 +293,18 @@ class MessageRepository {
}
}
- func updateMessage(withID id: MessageId, localState: LocalMessageState?, completion: @escaping (Result) -> Void) {
- var message: ChatMessage?
- database.write({
- let dto = $0.message(id: id)
- dto?.localMessageState = localState
- message = try dto?.asModel()
- }, completion: { error in
- if let error = error {
- completion(.failure(error))
- } else {
- completion(.success(message!))
- }
- })
+ func updateMessage(withID id: MessageId, localState: LocalMessageState?, completion: @escaping @Sendable(Result) -> Void) {
+ database.write(converting: {
+ guard let dto = $0.message(id: id) else { throw ClientError.MessageDoesNotExist(messageId: id) }
+ dto.localMessageState = localState
+ return try dto.asModel()
+ }, completion: completion)
}
func undoReactionAddition(
on messageId: MessageId,
type: MessageReactionType,
- completion: (() -> Void)? = nil
+ completion: (@Sendable() -> Void)? = nil
) {
database.write {
let reaction = try $0.removeReaction(from: messageId, type: type, on: nil)
@@ -334,7 +321,7 @@ class MessageRepository {
on messageId: MessageId,
type: MessageReactionType,
score: Int,
- completion: (() -> Void)? = nil
+ completion: (@Sendable() -> Void)? = nil
) {
database.write {
_ = try $0.addReaction(to: messageId, type: type, score: score, enforceUnique: false, extraData: [:], localState: .deletingFailed)
diff --git a/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift b/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift
index b1ee5236992..12d82c04f51 100644
--- a/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift
+++ b/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift
@@ -23,7 +23,7 @@ extension Endpoint {
/// OfflineRequestsRepository handles both the enqueuing and the execution of offline requests when needed.
/// When running the queued requests, it basically passes the requests on to the APIClient, and waits for its result.
-class OfflineRequestsRepository {
+class OfflineRequestsRepository: @unchecked Sendable {
enum Constants {
static let secondsInHour: Double = 3600
}
@@ -51,7 +51,7 @@ class OfflineRequestsRepository {
/// - If the requests succeeds -> The request is removed from the pending ones
/// - If the request fails with a connection error -> The request is kept to be executed once the connection is back (we are not putting it back at the queue to make sure we respect the order)
/// - If the request fails with any other error -> We are dismissing the request, and removing it from the queue
- func runQueuedRequests(completion: @escaping () -> Void) {
+ func runQueuedRequests(completion: @escaping @Sendable() -> Void) {
database.read { session in
let dtos = session.allQueuedRequests()
var requests = [Request]()
@@ -113,8 +113,8 @@ class OfflineRequestsRepository {
} completion: { [weak self] result in
switch result {
case .success(let pair):
- self?.deleteRequests(with: pair.deleteIds, completion: {
- self?.retryQueue.async {
+ self?.deleteRequests(with: pair.deleteIds, completion: { [weak self] in
+ self?.retryQueue.async { [weak self] in
self?.executeRequests(pair.requests, completion: completion)
}
})
@@ -125,7 +125,7 @@ class OfflineRequestsRepository {
}
}
- private func deleteRequests(with ids: Set, completion: @escaping () -> Void) {
+ private func deleteRequests(with ids: Set, completion: @escaping @Sendable() -> Void) {
guard !ids.isEmpty else {
completion()
return
@@ -139,7 +139,7 @@ class OfflineRequestsRepository {
}
}
- private func executeRequests(_ requests: [Request], completion: @escaping () -> Void) {
+ private func executeRequests(_ requests: [Request], completion: @escaping @Sendable() -> Void) {
let database = self.database
let group = DispatchGroup()
for request in requests {
@@ -147,13 +147,10 @@ class OfflineRequestsRepository {
let endpoint = request.endpoint
group.enter()
- let leave = {
- group.leave()
- }
- let deleteQueuedRequestAndComplete = {
+ let deleteQueuedRequestAndComplete: @Sendable() -> Void = {
database.write({ session in
session.deleteQueuedRequest(id: id)
- }, completion: { _ in leave() })
+ }, completion: { _ in group.leave() })
}
log.info("Executing queued offline request for /\(endpoint.path)", subsystems: .offlineSupport)
@@ -172,7 +169,7 @@ class OfflineRequestsRepository {
"Keeping offline request /\(endpoint.path) as there is no connection",
subsystems: .offlineSupport
)
- leave()
+ group.leave()
case let .failure(error):
log.info(
"Request for /\(endpoint.path) failed: \(error)",
@@ -192,7 +189,7 @@ class OfflineRequestsRepository {
private func performDatabaseRecoveryActionsUponSuccess(
for endpoint: DataEndpoint,
data: Data,
- completion: @escaping () -> Void
+ completion: @escaping @Sendable() -> Void
) {
func decodeTo(_ type: T.Type) -> T? {
try? JSONDecoder.stream.decode(T.self, from: data)
@@ -222,7 +219,7 @@ class OfflineRequestsRepository {
}
}
- func queueOfflineRequest(endpoint: DataEndpoint, completion: (() -> Void)? = nil) {
+ func queueOfflineRequest(endpoint: DataEndpoint, completion: (@Sendable() -> Void)? = nil) {
guard endpoint.shouldBeQueuedOffline else {
completion?()
return
diff --git a/Sources/StreamChat/Repositories/PollsRepository.swift b/Sources/StreamChat/Repositories/PollsRepository.swift
index c39ec8fd1fe..ffdd0ba8f92 100644
--- a/Sources/StreamChat/Repositories/PollsRepository.swift
+++ b/Sources/StreamChat/Repositories/PollsRepository.swift
@@ -4,7 +4,7 @@
import Foundation
-class PollsRepository {
+class PollsRepository: @unchecked Sendable {
let database: DatabaseContainer
let apiClient: APIClient
@@ -23,7 +23,7 @@ class PollsRepository {
votingVisibility: String?,
options: [PollOption]?,
custom: [String: RawJSON]?,
- completion: @escaping (Result) -> Void
+ completion: @escaping @Sendable(Result) -> Void
) {
let request = CreatePollRequestBody(
name: name,
@@ -54,7 +54,7 @@ class PollsRepository {
currentUserId: String?,
query: PollVoteListQuery?,
deleteExistingVotes: [PollVote],
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
guard let optionId, !optionId.isEmpty else {
// No optimistic updates for answers.
@@ -78,12 +78,11 @@ class PollsRepository {
return
}
- var pollVote: PollVote?
- database.write { session in
+ database.write(converting: { session in
let voteId = PollVoteDTO.localVoteId(optionId: optionId, pollId: pollId, userId: currentUserId)
let existing = try? session.pollVote(id: voteId, pollId: pollId)
if existing == nil {
- pollVote = try session.savePollVote(
+ let pollVote = try session.savePollVote(
voteId: nil,
pollId: pollId,
optionId: optionId,
@@ -92,14 +91,15 @@ class PollsRepository {
query: query
)
.asModel()
+ for toDelete in deleteExistingVotes {
+ _ = try? session.removePollVote(with: toDelete.id, pollId: toDelete.pollId)
+ }
+ return pollVote
} else {
throw ClientError.PollVoteAlreadyExists()
}
- for toDelete in deleteExistingVotes {
- _ = try? session.removePollVote(with: toDelete.id, pollId: toDelete.pollId)
- }
- } completion: { [weak self] error in
- if let error {
+ }, completion: { [weak self] result in
+ if let error = result.error {
completion?(error)
return
}
@@ -117,8 +117,8 @@ class PollsRepository {
pollId: pollId,
vote: request
)
- ) {
- if $0.isError, $0.error?.isBackendErrorWith400StatusCode == false, let pollVote {
+ ) { [weak self] in
+ if $0.isError, $0.error?.isBackendErrorWith400StatusCode == false, let pollVote = result.value {
self?.database.write { session in
_ = try? session.removePollVote(with: pollVote.id, pollId: pollVote.pollId)
for vote in deleteExistingVotes {
@@ -135,60 +135,64 @@ class PollsRepository {
}
completion?($0.error)
}
- }
+ })
}
func removePollVote(
messageId: MessageId,
pollId: String,
voteId: String,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
- var exists = false
- var answerText: String?
- var optionId: String?
- var userId: UserId?
- var filterHash: String?
- database.write { session in
+ struct WriteResponse {
+ var exists = false
+ var answerText: String?
+ var optionId: String?
+ var userId: UserId?
+ var filterHash: String?
+ }
+ database.write(converting: { session in
let voteDto = try session.removePollVote(with: voteId, pollId: pollId)
- exists = voteDto != nil
- filterHash = voteDto?.queries?.first?.filterHash
- answerText = voteDto?.answerText
- optionId = voteDto?.optionId
- userId = voteDto?.user?.id
- } completion: { [weak self] error in
- if error == nil {
+ var writeResponse = WriteResponse()
+ writeResponse.exists = voteDto != nil
+ writeResponse.filterHash = voteDto?.queries?.first?.filterHash
+ writeResponse.answerText = voteDto?.answerText
+ writeResponse.optionId = voteDto?.optionId
+ writeResponse.userId = voteDto?.user?.id
+ return writeResponse
+ }, completion: { [weak self] result in
+ if let writeResponse = result.value {
self?.apiClient.request(
endpoint: .removePollVote(
messageId: messageId,
pollId: pollId,
voteId: voteId
)
- ) {
- if $0.error != nil, $0.error?.isBackendNotFound404StatusCode == false, exists {
+ ) { [weak self] in
+ if $0.error != nil, $0.error?.isBackendNotFound404StatusCode == false, writeResponse.exists {
self?.database.write { session in
_ = try session.savePollVote(
voteId: voteId,
pollId: pollId,
- optionId: optionId,
- answerText: answerText,
- userId: userId,
+ optionId: writeResponse.optionId,
+ answerText: writeResponse.answerText,
+ userId: writeResponse.userId,
query: nil
)
- try? session.linkVote(with: voteId, in: pollId, to: filterHash)
+ try? session.linkVote(with: voteId, in: pollId, to: writeResponse.filterHash)
}
}
completion?($0.error)
}
} else {
- completion?(error)
+ completion?(result.error)
}
- }
+ })
}
func closePoll(
pollId: String,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
let request = UpdatePollPartialRequestBody(
pollId: pollId,
@@ -203,7 +207,7 @@ class PollsRepository {
func deletePoll(
pollId: String,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
apiClient.request(endpoint: .deletePoll(pollId: pollId)) { [weak self] in
if $0.error == nil {
@@ -220,7 +224,7 @@ class PollsRepository {
text: String,
position: Int? = nil,
custom: [String: RawJSON]? = nil,
- completion: ((Error?) -> Void)? = nil
+ completion: (@Sendable(Error?) -> Void)? = nil
) {
let request = CreatePollOptionRequestBody(
pollId: pollId,
@@ -238,21 +242,21 @@ class PollsRepository {
func queryPollVotes(
query: PollVoteListQuery,
- completion: ((Result) -> Void)? = nil
+ completion: (@Sendable(Result) -> Void)? = nil
) {
apiClient.request(
endpoint: .queryPollVotes(pollId: query.pollId, query: query)
) { [weak self] (result: Result