diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db0605bc6f..9dfc5baeb45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming -### 🔄 Changed +## StreamChat +### ✅ Added +- Add support for user blocking [#3223](https://github.com/GetStream/stream-chat-swift/pull/3223) # [4.58.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.58.0) _June 26, 2024_ diff --git a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift index 8934310a401..66ef0b73ab7 100644 --- a/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift +++ b/DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift @@ -183,6 +183,7 @@ class AppConfigViewController: UITableViewController { case isUnreadMessageSeparatorEnabled case isJumpToUnreadEnabled case mentionAllAppUsers + case isBlockingUsersEnabled } enum ChatClientConfigOption: String, CaseIterable { @@ -459,6 +460,10 @@ class AppConfigViewController: UITableViewController { cell.accessoryView = makeSwitchButton(Components.default.mentionAllAppUsers) { newValue in Components.default.mentionAllAppUsers = newValue } + case .isBlockingUsersEnabled: + cell.accessoryView = makeSwitchButton(Components.default.isBlockingUsersEnabled) { newValue in + Components.default.isBlockingUsersEnabled = newValue + } } } diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift index 9f23007ac41..2c827e44aa9 100644 --- a/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift +++ b/DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift @@ -447,6 +447,21 @@ final class DemoChatChannelListRouter: ChatChannelListRouter { ) ), animated: true) }), + .init(title: "Show Blocked Users", handler: { [unowned self] _ in + guard let cid = channelController.channel?.cid else { return } + 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)) + ) + ) + ), animated: true) + } + }), .init(title: "Truncate channel w/o message", isEnabled: canUpdateChannel, handler: { _ in channelController.truncateChannel { [unowned self] error in if let error = error { diff --git a/Sources/StreamChat/APIClient/Endpoints/BlockedUserPayload.swift b/Sources/StreamChat/APIClient/Endpoints/BlockedUserPayload.swift new file mode 100644 index 00000000000..b092835eaf0 --- /dev/null +++ b/Sources/StreamChat/APIClient/Endpoints/BlockedUserPayload.swift @@ -0,0 +1,70 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// An object describing the incoming blocking user JSON payload. +struct BlockingUserPayload: Decodable, Equatable { + private enum CodingKeys: String, CodingKey { + case blockedUserId = "blocked_user_id" + case blockedByUserId = "blocked_by_user_id" + case createdAt = "created_at" + } + + let blockedUserId: String + let blockedByUserId: String + let createdAt: Date +} + +/// An object describing the incoming blocked users JSON payload. +struct BlocksPayload: Decodable { + private enum CodingKeys: String, CodingKey { + case blockedUsers = "blocks" + } + + /// The blocked users. + let blockedUsers: [BlockPayload] +} + +/// An object describing the incoming block JSON payload. +struct BlockPayload: Decodable { + private enum CodingKeys: String, CodingKey { + case blockedUserId = "blocked_user_id" + case userId = "user_id" + case createdAt = "created_at" + case blockedUser = "blocked_user" + } + + let blockedUserId: String + let userId: String + let createdAt: Date + let blockedUser: BlockedUserPayload +} + +extension BlockPayload: Equatable { + static func == (lhs: BlockPayload, rhs: BlockPayload) -> Bool { + lhs.blockedUserId == rhs.blockedUserId + && lhs.userId == rhs.userId + && lhs.createdAt == rhs.createdAt + } +} + +/// An object describing the incoming blocked-user JSON payload. +struct BlockedUserPayload: Decodable { + private enum CodingKeys: String, CodingKey { + case id + case anon + case name + case role + case teams + case username + } + + let id: String + let anon: Bool? + let name: String? + let role: UserRole + let teams: [TeamId] + let username: String? +} diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift index 5698952b327..c49abaf1e5f 100644 --- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift +++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift @@ -11,10 +11,10 @@ extension EndpointPath { return true case .createChannel, .connect, .sync, .users, .guest, .members, .search, .devices, .channels, .updateChannel, .deleteChannel, .channelUpdate, .muteChannel, .showChannel, .truncateChannel, .markChannelRead, .markChannelUnread, - .markAllChannelsRead, .channelEvent, .stopWatchingChannel, .pinnedMessages, .uploadAttachment, .message, - .replies, .reactions, .messageAction, .banMember, .flagUser, .flagMessage, .muteUser, .translateMessage, - .callToken, .createCall, .deleteFile, .deleteImage, .og, .appSettings, .threads, .thread, .markThreadRead, .markThreadUnread, .polls, .pollsQuery, - .poll, .pollOption, .pollOptions, .pollVotes, .pollVoteInMessage, .pollVote: + .markAllChannelsRead, .channelEvent, .stopWatchingChannel, .pinnedMessages, .uploadAttachment, .message, .replies, + .reactions, .messageAction, .banMember, .flagUser, .flagMessage, .muteUser, .blockUser, .unblockUser, .callToken, + .createCall, .translateMessage, .deleteFile, .deleteImage, .og, .appSettings, .threads, .thread, .markThreadRead, + .markThreadUnread, .polls, .pollsQuery, .poll, .pollOption, .pollOptions, .pollVotes, .pollVoteInMessage, .pollVote: return false } } diff --git a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift index 2805ccdacf1..f758d8a9465 100644 --- a/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift +++ b/Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift @@ -52,6 +52,8 @@ enum EndpointPath: Codable { case flagUser(Bool) case flagMessage(Bool) case muteUser(Bool) + case blockUser + case unblockUser case callToken(String) case createCall(String) @@ -123,6 +125,8 @@ enum EndpointPath: Codable { case let .flagUser(flag): return "moderation/\(flag ? "flag" : "unflag")" case let .flagMessage(flag): return "moderation/\(flag ? "flag" : "unflag")" case let .muteUser(mute): return "moderation/\(mute ? "mute" : "unmute")" + case .blockUser: return "users/block" + case .unblockUser: return "users/unblock" case let .callToken(callId): return "calls/\(callId)" case let .createCall(queryString): return "channels/\(queryString)/call" case let .deleteFile(channelId): return "channels/\(channelId)/file" diff --git a/Sources/StreamChat/APIClient/Endpoints/ModerationEndpoints.swift b/Sources/StreamChat/APIClient/Endpoints/ModerationEndpoints.swift index a64b46ff08c..98450ed4d83 100644 --- a/Sources/StreamChat/APIClient/Endpoints/ModerationEndpoints.swift +++ b/Sources/StreamChat/APIClient/Endpoints/ModerationEndpoints.swift @@ -16,6 +16,39 @@ extension Endpoint { } } +// MARK: - User blocking + +extension Endpoint { + static func blockUser(_ userId: UserId) -> Endpoint { + .init( + path: .blockUser, + method: .post, + queryItems: nil, + requiresConnectionId: false, + body: ["blocked_user_id": userId] + ) + } + + static func unblockUser(_ userId: UserId) -> Endpoint { + .init( + path: .unblockUser, + method: .post, + queryItems: nil, + requiresConnectionId: false, + body: ["blocked_user_id": userId] + ) + } + + static func loadBlockedUsers() -> Endpoint { + .init( + path: .blockUser, + method: .get, + queryItems: nil, + requiresConnectionId: false + ) + } +} + // MARK: - User banning extension Endpoint { diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift index e635eb268de..bae12ffcdab 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelCodingKeys.swift @@ -46,4 +46,6 @@ public enum ChannelCodingKeys: String, CodingKey, CaseIterable { /// Cooldown duration for the channel, if it's in slow mode. /// This value will be 0 if the channel is not in slow mode. case cooldownDuration = "cooldown" + /// Blocked flag. + case blocked } diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift index a08e503aca5..2f5764651f0 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift @@ -116,6 +116,8 @@ struct ChannelDetailPayload { let ownCapabilities: [String]? /// Checks if the channel is frozen. let isFrozen: Bool + /// Checks if the channel is blocked. + let isBlocked: Bool? /// Checks if the channel is hidden. /// Backend only sends this field for `QueryChannel` and `QueryChannels` API calls, @@ -168,6 +170,7 @@ extension ChannelDetailPayload: Decodable { config: try container.decode(ChannelConfig.self, forKey: .config), ownCapabilities: try container.decodeIfPresent([String].self, forKey: .ownCapabilities), isFrozen: try container.decode(Bool.self, forKey: .frozen), + isBlocked: try container.decodeIfPresent(Bool.self, forKey: .blocked), // For `hidden`, we don't fallback to `false` // since this field is not sent for all API calls and for events // We can't assume anything regarding this flag when it's absent diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift index df4f651d31c..f89c21ded29 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/CurrentUserPayloads.swift @@ -16,6 +16,8 @@ class CurrentUserPayload: UserPayload { let unreadCount: UnreadCount? /// The current privacy settings of the user. let privacySettings: UserPrivacySettingsPayload? + /// Blocked user ids. + let blockedUserIds: Set init( id: String, @@ -36,13 +38,15 @@ class CurrentUserPayload: UserPayload { mutedUsers: [MutedUserPayload] = [], mutedChannels: [MutedChannelPayload] = [], unreadCount: UnreadCount? = nil, - privacySettings: UserPrivacySettingsPayload? = nil + privacySettings: UserPrivacySettingsPayload? = nil, + blockedUserIds: Set = [] ) { self.devices = devices self.mutedUsers = mutedUsers self.mutedChannels = mutedChannels self.unreadCount = unreadCount self.privacySettings = privacySettings + self.blockedUserIds = blockedUserIds super.init( id: id, @@ -69,6 +73,7 @@ class CurrentUserPayload: UserPayload { mutedChannels = try container.decodeIfPresent([MutedChannelPayload].self, forKey: .mutedChannels) ?? [] unreadCount = try? UnreadCount(from: decoder) privacySettings = try container.decodeIfPresent(UserPrivacySettingsPayload.self, forKey: .privacySettings) + blockedUserIds = try container.decodeIfPresent(Set.self, forKey: .blockedUserIds) ?? [] try super.init(from: decoder) } diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift index 72a7772b13c..12fe13c8210 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UserPayloads.swift @@ -27,6 +27,7 @@ enum UserPayloadsCodingKeys: String, CodingKey, CaseIterable { case unreadCount = "unread_count" case language case privacySettings = "privacy_settings" + case blockedUserIds = "blocked_user_ids" } // MARK: - GET users diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift index 3e17858f086..0ba7ad89ed0 100644 --- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift +++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift @@ -257,6 +257,18 @@ public extension CurrentChatUserController { } } } + + /// Get all blocked users. + /// + /// - 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) { + currentUserUpdater.loadBlockedUsers { result in + self.callback { + completion(result) + } + } + } } // MARK: - Environment diff --git a/Sources/StreamChat/Controllers/UserController/UserController.swift b/Sources/StreamChat/Controllers/UserController/UserController.swift index f686c78d0b8..72006f9dad0 100644 --- a/Sources/StreamChat/Controllers/UserController/UserController.swift +++ b/Sources/StreamChat/Controllers/UserController/UserController.swift @@ -160,6 +160,28 @@ 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) { + userUpdater.blockUser(userId) { error in + self.callback { + completion?(error) + } + } + } + + /// 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) { + userUpdater.unblockUser(userId) { error in + self.callback { + completion?(error) + } + } + } /// Flags the user this controller manages. /// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished. diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift index 522a7838692..fb40989e083 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift @@ -41,6 +41,8 @@ class ChannelDTO: NSManagedObject { @NSManaged var isFrozen: Bool @NSManaged var cooldownDuration: Int @NSManaged var team: String? + + @NSManaged var isBlocked: Bool // MARK: - Queries @@ -224,6 +226,14 @@ extension NSManagedObjectContext { } dto.isFrozen = payload.isFrozen + + // Backend only returns a boolean + // for blocked 1:1 channels on channel list query + if let isBlocked = payload.isBlocked { + dto.isBlocked = isBlocked + } else { + dto.isBlocked = false + } // Backend only returns a boolean for hidden state // on channel query and channel list query @@ -373,6 +383,18 @@ extension ChannelDTO { request.fetchBatchSize = query.pagination.pageSize return request } + + static func directMessageChannel(participantId: UserId, context: NSManagedObjectContext) -> ChannelDTO? { + let request = NSFetchRequest(entityName: ChannelDTO.entityName) + request.sortDescriptors = [NSSortDescriptor(keyPath: \ChannelDTO.updatedAt, ascending: false)] + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(format: "cid CONTAINS ':!members'"), + NSPredicate(format: "members.@count == 2"), + NSPredicate(format: "ANY members.user.id == %@", participantId) + ]) + request.fetchLimit = 1 + return try? context.fetch(request).first + } } extension ChannelDTO { @@ -514,6 +536,7 @@ extension ChatChannel { config: dto.config.asModel(), ownCapabilities: Set(dto.ownCapabilities.compactMap(ChannelCapability.init(rawValue:))), isFrozen: dto.isFrozen, + isBlocked: dto.isBlocked, lastActiveMembers: { fetchMembers() }, membership: dto.membership.map { try $0.asModel() }, currentlyTypingUsers: { Set(dto.currentlyTypingUsers.compactMap { try? $0.asModel() }) }, diff --git a/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift b/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift index e1ab1e06657..7291e9ed8c4 100644 --- a/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift +++ b/Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift @@ -14,7 +14,8 @@ class CurrentUserDTO: NSManagedObject { /// Contains the timestamp when last sync process was finished. /// The date later serves as reference date for the last event synced using `/sync` endpoint @NSManaged var lastSynchedEventDate: DBDate? - + + @NSManaged var blockedUserIds: Set @NSManaged var flaggedUsers: Set @NSManaged var flaggedMessages: Set @NSManaged var mutedUsers: Set @@ -91,6 +92,8 @@ extension NSManagedObjectContext: CurrentUserDatabaseSession { let mutedUsers = try payload.mutedUsers.map { try saveUser(payload: $0.mutedUser) } dto.mutedUsers = Set(mutedUsers) + + dto.blockedUserIds = payload.blockedUserIds let channelMutes = Set( try payload.mutedChannels.map { try saveChannelMute(payload: $0) } @@ -205,7 +208,7 @@ extension CurrentChatUser { ) extraData = [:] } - + let mutedUsers: [ChatUser] = try dto.mutedUsers.map { try $0.asModel() } let flaggedUsers: [ChatUser] = try dto.flaggedUsers.map { try $0.asModel() } let flaggedMessagesIDs: [MessageId] = dto.flaggedMessages.map(\.id) @@ -233,6 +236,7 @@ extension CurrentChatUser { extraData: extraData, devices: dto.devices.map { try $0.asModel() }, currentDevice: dto.currentDevice?.asModel(), + blockedUserIds: dto.blockedUserIds, mutedUsers: Set(mutedUsers), flaggedUsers: Set(flaggedUsers), flaggedMessageIDs: Set(flaggedMessagesIDs), diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents index 6742ae6e6c8..ae708dab66c 100644 --- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents +++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -41,6 +41,7 @@ + @@ -133,6 +134,7 @@ + diff --git a/Sources/StreamChat/Models/BlockedUserDetails.swift b/Sources/StreamChat/Models/BlockedUserDetails.swift new file mode 100644 index 00000000000..5cc077c0a78 --- /dev/null +++ b/Sources/StreamChat/Models/BlockedUserDetails.swift @@ -0,0 +1,28 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A type representing a blocked user. +public struct BlockedUserDetails { + /// The unique identifier of the blocked user. + public let userId: UserId + + /// The date the user was blocked. + public let blockedAt: Date? + + init( + userId: UserId, + blockedAt: Date? + ) { + self.userId = userId + self.blockedAt = blockedAt + } +} + +extension BlockedUserDetails: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(userId) + } +} diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift index 2eb5dc770f2..accd91c393b 100644 --- a/Sources/StreamChat/Models/Channel.swift +++ b/Sources/StreamChat/Models/Channel.swift @@ -49,6 +49,12 @@ public struct ChatChannel { /// It's not possible to send new messages to a frozen channel. /// public let isFrozen: Bool + + /// Returns `true` if the channel is blocked. + /// + /// It's not possible to send new messages to a blocked channel. + /// + public let isBlocked: Bool /// The total number of members in the channel. public let memberCount: Int @@ -190,6 +196,7 @@ public struct ChatChannel { config: ChannelConfig = .init(), ownCapabilities: Set = [], isFrozen: Bool = false, + isBlocked: Bool = false, lastActiveMembers: @escaping (() -> [ChatChannelMember]) = { [] }, membership: ChatChannelMember? = nil, currentlyTypingUsers: @escaping () -> Set = { [] }, @@ -220,6 +227,7 @@ public struct ChatChannel { self.config = config self.ownCapabilities = ownCapabilities self.isFrozen = isFrozen + self.isBlocked = isBlocked self.membership = membership self.team = team self.watcherCount = watcherCount diff --git a/Sources/StreamChat/Models/CurrentUser.swift b/Sources/StreamChat/Models/CurrentUser.swift index f157164d4ea..1abab8c6223 100644 --- a/Sources/StreamChat/Models/CurrentUser.swift +++ b/Sources/StreamChat/Models/CurrentUser.swift @@ -31,6 +31,9 @@ public class CurrentChatUser: ChatUser { /// A set of users muted by the user. public let mutedUsers: Set + + /// A list of blocked user ids. + public let blockedUserIds: Set /// A set of users flagged by the user. /// @@ -76,6 +79,7 @@ public class CurrentChatUser: ChatUser { extraData: [String: RawJSON], devices: [Device], currentDevice: Device?, + blockedUserIds: Set, mutedUsers: Set, flaggedUsers: Set, flaggedMessageIDs: Set, @@ -86,6 +90,7 @@ public class CurrentChatUser: ChatUser { ) { self.devices = devices self.currentDevice = currentDevice + self.blockedUserIds = blockedUserIds self.mutedUsers = mutedUsers self.flaggedUsers = flaggedUsers self.flaggedMessageIDs = flaggedMessageIDs diff --git a/Sources/StreamChat/Models/UserInfo.swift b/Sources/StreamChat/Models/UserInfo.swift index aeb30128b94..c2db8e27e05 100644 --- a/Sources/StreamChat/Models/UserInfo.swift +++ b/Sources/StreamChat/Models/UserInfo.swift @@ -18,6 +18,8 @@ public struct UserInfo { public let language: TranslationLanguage? /// The privacy settings of the user. Example: If the user does not want to expose typing events or read events. public let privacySettings: UserPrivacySettings? + /// A list of blocked user ids. + public let blockedUserIds: [UserId]? /// Custom extra data of the user. public let extraData: [String: RawJSON] @@ -28,6 +30,7 @@ public struct UserInfo { isInvisible: Bool? = nil, language: TranslationLanguage? = nil, privacySettings: UserPrivacySettings? = nil, + blockedUserIds: [UserId]? = nil, extraData: [String: RawJSON] = [:] ) { self.id = id @@ -36,6 +39,7 @@ public struct UserInfo { self.isInvisible = isInvisible self.language = language self.privacySettings = privacySettings + self.blockedUserIds = blockedUserIds self.extraData = extraData } } diff --git a/Sources/StreamChat/Query/ChannelListQuery.swift b/Sources/StreamChat/Query/ChannelListQuery.swift index 49a8ffed042..f77560b8422 100644 --- a/Sources/StreamChat/Query/ChannelListQuery.swift +++ b/Sources/StreamChat/Query/ChannelListQuery.swift @@ -103,6 +103,10 @@ public extension FilterKey where Scope: AnyChannelListFilterScope { /// A filter key for matching the `frozen` value. /// Supported operators: `equal` static var frozen: FilterKey { .init(rawValue: "frozen", keyPathString: #keyPath(ChannelDTO.isFrozen)) } + + /// A filter key for matching the `blocked` value. + /// Supported operators: `equal` + static var blocked: FilterKey { .init(rawValue: "blocked", keyPathString: #keyPath(ChannelDTO.isBlocked)) } /// A filter key for matching the `memberCount` value. /// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `notEqual` diff --git a/Sources/StreamChat/StateLayer/ConnectedUser.swift b/Sources/StreamChat/StateLayer/ConnectedUser.swift index 6c33ab2d554..4a7dbbb91fc 100644 --- a/Sources/StreamChat/StateLayer/ConnectedUser.swift +++ b/Sources/StreamChat/StateLayer/ConnectedUser.swift @@ -133,6 +133,32 @@ public final class ConnectedUser { try await userUpdater.unmuteUser(userId) } + /// Blocks the user in all the channels. + /// + /// - Parameter userId: The id of the user to mute. + /// + /// - Throws: An error while communicating with the Stream API. + public func blockUser(_ userId: UserId) async throws { + try await userUpdater.blockUser(userId) + } + + /// Unblocks the user in all the channels. + /// + /// - Parameter userId: The id of the user to unmute. + /// + /// - Throws: An error while communicating with the Stream API. + public func unblockUser(_ userId: UserId) async throws { + try await userUpdater.unblockUser(userId) + } + + /// Get all blocked users. + /// + /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. + /// + func loadBlockedUsers() async throws -> [BlockedUserDetails] { + try await currentUserUpdater.loadBlockedUsers() + } + /// Flags the specified user. /// /// - Parameter userId: The id of the user to flag. diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelVisibilityEventMiddleware.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelVisibilityEventMiddleware.swift index 5d90a3174e8..22e58018340 100644 --- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelVisibilityEventMiddleware.swift +++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/ChannelVisibilityEventMiddleware.swift @@ -33,8 +33,8 @@ struct ChannelVisibilityEventMiddleware: EventMiddleware { guard let channelDTO = session.channel(cid: event.cid) else { throw ClientError.ChannelDoesNotExist(cid: event.cid) } - - if !event.message.isShadowed { + + if !event.message.isShadowed && !channelDTO.isBlocked { channelDTO.isHidden = false } @@ -45,7 +45,7 @@ struct ChannelVisibilityEventMiddleware: EventMiddleware { throw ClientError.ChannelDoesNotExist(cid: event.channel.cid) } - if !event.message.isShadowed { + if !event.message.isShadowed && !channelDTO.isBlocked { channelDTO.isHidden = false } diff --git a/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift b/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift index 6ad87f73a71..8e3b03a119e 100644 --- a/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift +++ b/Sources/StreamChat/WebSocketClient/WebSocketConnectPayload.swift @@ -30,6 +30,7 @@ struct UserWebSocketPayload: Encodable { case imageURL = "image" case language case privacySettings = "privacy_settings" + case blockedUserIds = "blocked_user_ids" } let id: String @@ -38,6 +39,7 @@ struct UserWebSocketPayload: Encodable { let isInvisible: Bool? let language: String? let privacySettings: UserPrivacySettingsPayload? + let blockedUserIds: [UserId]? let extraData: [String: RawJSON] init(userInfo: UserInfo) { @@ -47,6 +49,7 @@ struct UserWebSocketPayload: Encodable { isInvisible = userInfo.isInvisible language = userInfo.language?.languageCode privacySettings = userInfo.privacySettings.map { .init(settings: $0) } + blockedUserIds = userInfo.blockedUserIds extraData = userInfo.extraData } @@ -58,6 +61,7 @@ struct UserWebSocketPayload: Encodable { try container.encodeIfPresent(isInvisible, forKey: .isInvisible) try container.encodeIfPresent(language, forKey: .language) try container.encodeIfPresent(privacySettings, forKey: .privacySettings) + try container.encodeIfPresent(blockedUserIds, forKey: .blockedUserIds) try extraData.encode(to: encoder) } } diff --git a/Sources/StreamChat/Workers/CurrentUserUpdater.swift b/Sources/StreamChat/Workers/CurrentUserUpdater.swift index e3d0a06c2d7..81355f9e469 100644 --- a/Sources/StreamChat/Workers/CurrentUserUpdater.swift +++ b/Sources/StreamChat/Workers/CurrentUserUpdater.swift @@ -163,6 +163,31 @@ class CurrentUserUpdater: Worker { completion?($0.error) } } + + /// Get all blocked users. + /// + /// - 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) { + apiClient.request(endpoint: .loadBlockedUsers()) { + switch $0 { + case let .success(payload): + self.database.write({ session in + session.currentUser?.blockedUserIds = Set(payload.blockedUsers.map(\.blockedUserId)) + }, completion: { + if let error = $0 { + log.error("Failed to save blocked users to the database. Error: \(error)") + } + let blockedUsers = payload.blockedUsers.map { + BlockedUserDetails(userId: $0.blockedUserId, blockedAt: $0.createdAt) + } + completion(.success(blockedUsers)) + }) + case let .failure(error): + completion(.failure(error)) + } + } + } } @available(iOS 13.0, *) @@ -204,6 +229,14 @@ extension CurrentUserUpdater { } } + func loadBlockedUsers() async throws -> [BlockedUserDetails] { + try await withCheckedThrowingContinuation { continuation in + loadBlockedUsers { result in + continuation.resume(with: result) + } + } + } + func updateUserData( currentUserId: UserId, name: String?, diff --git a/Sources/StreamChat/Workers/UserUpdater.swift b/Sources/StreamChat/Workers/UserUpdater.swift index 86c532b4e09..273625bb0bb 100644 --- a/Sources/StreamChat/Workers/UserUpdater.swift +++ b/Sources/StreamChat/Workers/UserUpdater.swift @@ -2,6 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // +import CoreData import Foundation /// Makes user-related calls to the backend and updates the local storage with the results. @@ -27,6 +28,68 @@ class UserUpdater: Worker { completion?($0.error) } } + + /// Blocks the user with the provided `userId`. + /// - Parameters: + /// - userId: The user identifier. + /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. + /// + func blockUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + apiClient.request(endpoint: .blockUser(userId)) { + switch $0 { + case .success: + self.database.write({ session in + session.currentUser?.blockedUserIds.insert(userId) + let channel = ChannelDTO.directMessageChannel( + participantId: userId, + context: self.database.writableContext + ) + channel?.isHidden = true + }, completion: { + if let error = $0 { + log.error("Failed to save blocked user with id: <\(userId)> to the database. Error: \(error)") + } + completion?($0) + }) + case let .failure(error): + self.database.write { session in + session.currentUser?.blockedUserIds.remove(userId) + } + completion?(error) + } + } + } + + /// Unblocks the user with the provided `userId`. + /// - Parameters: + /// - userId: The user identifier. + /// - completion: Called when the API call is finished. Called with `Error` if the remote update fails. + /// + func unblockUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + apiClient.request(endpoint: .unblockUser(userId)) { + switch $0 { + case .success: + self.database.write({ session in + session.currentUser?.blockedUserIds.remove(userId) + let channel = ChannelDTO.directMessageChannel( + participantId: userId, + context: self.database.writableContext + ) + channel?.isHidden = false + }, completion: { + if let error = $0 { + log.error("Failed to remove blocked user with id: <\(userId)> from the database. Error: \(error)") + } + completion?($0) + }) + case let .failure(error): + self.database.write { session in + session.currentUser?.blockedUserIds.insert(userId) + } + completion?(error) + } + } + } /// Makes a single user query call to the backend and updates the local storage with the results. /// @@ -124,6 +187,22 @@ extension UserUpdater { } } + func blockUser(_ userId: UserId) async throws { + try await withCheckedThrowingContinuation { continuation in + blockUser(userId) { error in + continuation.resume(with: error) + } + } + } + + func unblockUser(_ userId: UserId) async throws { + try await withCheckedThrowingContinuation { continuation in + unblockUser(userId) { error in + continuation.resume(with: error) + } + } + } + func flag(_ userId: UserId) async throws { try await withCheckedThrowingContinuation { continuation in flagUser(true, with: userId) { error in diff --git a/Sources/StreamChatUI/Components.swift b/Sources/StreamChatUI/Components.swift index 411f7dcd1fa..ebf43f90036 100644 --- a/Sources/StreamChatUI/Components.swift +++ b/Sources/StreamChatUI/Components.swift @@ -527,6 +527,11 @@ public struct Components { /// The view that shows current user avatar. public var currentUserAvatarView: CurrentChatUserAvatarView.Type = CurrentChatUserAvatarView.self + + // MARK: - User blocking components + + /// Determines if users are able to block other users. Disabled by default. + public var isBlockingUsersEnabled = false // MARK: - Navigation diff --git a/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift b/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift index b4afd4fe05f..d7c9476f949 100644 --- a/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift +++ b/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift @@ -131,7 +131,7 @@ open class ChatMessageActionsVC: _ViewController, ThemeProvider { if canDeleteAnyMessage { actions.append(deleteActionItem()) - } else if canDeleteOwnMessage && message.isSentByCurrentUser { + } else if canDeleteOwnMessage && isSentByCurrentUser { actions.append(deleteActionItem()) } @@ -139,10 +139,15 @@ open class ChatMessageActionsVC: _ViewController, ThemeProvider { actions.append(flagActionItem()) } - if channelConfig.mutesEnabled && !message.isSentByCurrentUser { + if channelConfig.mutesEnabled && !isSentByCurrentUser { let isMuted = currentUser.mutedUsers.map(\.id).contains(message.author.id) actions.append(isMuted ? unmuteActionItem() : muteActionItem()) } + + if components.isBlockingUsersEnabled && !isSentByCurrentUser { + let isBlocked = currentUser.blockedUserIds.contains(message.author.id) + actions.append(isBlocked ? unblockActionItem() : blockActionItem()) + } return actions case .sendingFailed: @@ -236,6 +241,40 @@ open class ChatMessageActionsVC: _ViewController, ThemeProvider { appearance: appearance ) } + + /// Returns `ChatMessageActionItem` for block action. + open func blockActionItem() -> ChatMessageActionItem { + BlockUserActionItem( + action: { [weak self] _ in + guard + let self = self, + let author = self.message?.author + else { return } + + self.messageController.client + .userController(userId: author.id) + .block { _ in self.delegate?.chatMessageActionsVCDidFinish(self) } + }, + appearance: appearance + ) + } + + /// Returns `ChatMessageActionItem` for unblock action. + open func unblockActionItem() -> ChatMessageActionItem { + UnblockUserActionItem( + action: { [weak self] _ in + guard + let self = self, + let author = self.message?.author + else { return } + + self.messageController.client + .userController(userId: author.id) + .unblock { _ in self.delegate?.chatMessageActionsVCDidFinish(self) } + }, + appearance: appearance + ) + } /// Returns `ChatMessageActionItem` for inline reply action. open func inlineReplyActionItem() -> ChatMessageActionItem { diff --git a/Sources/StreamChatUI/Resources/Assets.xcassets/Message Actions/icn_block_user.imageset/icn_block_user.pdf b/Sources/StreamChatUI/Resources/Assets.xcassets/Message Actions/icn_block_user.imageset/icn_block_user.pdf index d00b6628df1..cd06bdf8472 100644 Binary files a/Sources/StreamChatUI/Resources/Assets.xcassets/Message Actions/icn_block_user.imageset/icn_block_user.pdf and b/Sources/StreamChatUI/Resources/Assets.xcassets/Message Actions/icn_block_user.imageset/icn_block_user.pdf differ diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index 60917a6d6ef..b9898df3974 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -623,6 +623,10 @@ 829CD5CC2848C8D6003C3877 /* BackendRobot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 829CD5CB2848C8D6003C3877 /* BackendRobot.swift */; }; 82A6F5C027E2031000F4A2F6 /* Reactions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82A6F5BF27E2031000F4A2F6 /* Reactions_Tests.swift */; }; 82BA52EF27E1EF7B00951B87 /* MessageList_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82BA52EE27E1EF7B00951B87 /* MessageList_Tests.swift */; }; + 82BE0ACD2C009A17008DA9DC /* BlockedUserDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82BE0ACC2C009A17008DA9DC /* BlockedUserDetails.swift */; }; + 82BE0ACE2C009A17008DA9DC /* BlockedUserDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82BE0ACC2C009A17008DA9DC /* BlockedUserDetails.swift */; }; + 82C18FDC2C10C8E600C5283C /* BlockedUserPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C18FDB2C10C8E600C5283C /* BlockedUserPayload.swift */; }; + 82C18FDD2C10C8E600C5283C /* BlockedUserPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C18FDB2C10C8E600C5283C /* BlockedUserPayload.swift */; }; 82CBE5682861BF300039C35C /* AttachmentResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82CBE5672861BF300039C35C /* AttachmentResponses.swift */; }; 82CED1C827DF492F006E967A /* ThreadPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82CED1C727DF492F006E967A /* ThreadPage.swift */; }; 82DCB3A92A4AE8FB00738933 /* StreamChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 799C941B247D2F80001F1104 /* StreamChat.framework */; }; @@ -3482,6 +3486,8 @@ 82B350FC27EA2A3900FEB6A0 /* MessageResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResponses.swift; sourceTree = ""; }; 82B3510027EA2B1400FEB6A0 /* EventResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventResponses.swift; sourceTree = ""; }; 82BA52EE27E1EF7B00951B87 /* MessageList_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageList_Tests.swift; sourceTree = ""; }; + 82BE0ACC2C009A17008DA9DC /* BlockedUserDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUserDetails.swift; sourceTree = ""; }; + 82C18FDB2C10C8E600C5283C /* BlockedUserPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUserPayload.swift; sourceTree = ""; }; 82CBE5672861BF300039C35C /* AttachmentResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentResponses.swift; sourceTree = ""; }; 82CED1C727DF492F006E967A /* ThreadPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPage.swift; sourceTree = ""; }; 82D2E81E27D10F4300169ADA /* StreamMockServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamMockServer.swift; sourceTree = ""; }; @@ -5661,6 +5667,7 @@ isa = PBXGroup; children = ( 88E26D7C2580F95300F55AB5 /* AttachmentEndpoints.swift */, + 82C18FDB2C10C8E600C5283C /* BlockedUserPayload.swift */, 43D3F0F8284106EE00B74921 /* CallEndpoints.swift */, 79877A132498E4EE00015F8B /* ChannelEndpoints.swift */, F6FF1DA924FD23D300151735 /* MessageEndpoints.swift */, @@ -5777,6 +5784,7 @@ 79877A012498E4BB00015F8B /* User.swift */, 7900452525374CA20096ECA1 /* User+SwiftUI.swift */, 64C8C86D26934C6100329F82 /* UserInfo.swift */, + 82BE0ACC2C009A17008DA9DC /* BlockedUserDetails.swift */, 841BAA532BD26136000C73E4 /* PollOption.swift */, 841BAA562BD29DA5000C73E4 /* PollVote.swift */, 841BAA592BD2B39A000C73E4 /* Poll.swift */, @@ -11089,6 +11097,7 @@ buildActionMask = 2147483647; files = ( 4F97F2672BA83146001C4D66 /* UserList.swift in Sources */, + 82BE0ACD2C009A17008DA9DC /* BlockedUserDetails.swift in Sources */, DA8407032524F7E6005A0F62 /* UserListUpdater.swift in Sources */, 8413D2F52BDDAAFF005ADA4E /* PollVoteListController+SwiftUI.swift in Sources */, 4F8E53062B7CD01D008C0F9F /* Chat.swift in Sources */, @@ -11318,6 +11327,7 @@ 882C5759252C794900E60C44 /* MemberEndpoints.swift in Sources */, DA640FC12535CFA100D32944 /* ChannelMemberListSortingKey.swift in Sources */, 7900452625374CA20096ECA1 /* User+SwiftUI.swift in Sources */, + 82C18FDC2C10C8E600C5283C /* BlockedUserPayload.swift in Sources */, 404296DA2A0112D00089126D /* AudioQueuePlayer.swift in Sources */, 4F51519C2BC66FBE001B7152 /* Task+Extensions.swift in Sources */, 792FCB4924A3BF38000290C7 /* OptionSet+Extensions.swift in Sources */, @@ -12005,6 +12015,7 @@ buildActionMask = 2147483647; files = ( 4F1BEE7D2BE3851200B6685C /* ReactionListState+Observer.swift in Sources */, + 82BE0ACE2C009A17008DA9DC /* BlockedUserDetails.swift in Sources */, 40789D1C29F6AC500018C2BB /* AudioPlaybackState.swift in Sources */, AD8C7C612BA3DF2800260715 /* AppSettings.json in Sources */, C121E804274544AC00023E4C /* ChatClient.swift in Sources */, @@ -12297,6 +12308,7 @@ 4F4562F72C240FD200675C7F /* DatabaseItemConverter.swift in Sources */, C121E8AE274544B000023E4C /* ChannelController+SwiftUI.swift in Sources */, C121E8AF274544B000023E4C /* ChannelController+Combine.swift in Sources */, + 82C18FDD2C10C8E600C5283C /* BlockedUserPayload.swift in Sources */, C121E8B0274544B000023E4C /* ChannelListController.swift in Sources */, C121E8B1274544B000023E4C /* ChannelListController+SwiftUI.swift in Sources */, C121E8B2274544B000023E4C /* ChannelListController+Combine.swift in Sources */, diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannelMember_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannelMember_Mock.swift index 51c1680a21d..67e4adcc291 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannelMember_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatChannelMember_Mock.swift @@ -21,6 +21,7 @@ public extension ChatChannelMember { lastActiveAt: Date? = nil, teams: Set = ["RED", "GREEN"], language: TranslationLanguage? = nil, + blockedUserIds: [UserId] = [], extraData: [String: RawJSON] = [:], memberRole: MemberRole = .member, memberCreatedAt: Date = .distantPast, diff --git a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/CurrentChatUser_Mock.swift b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/CurrentChatUser_Mock.swift index 7d55acd57c0..96214b9cfeb 100644 --- a/TestTools/StreamChatTestTools/Mocks/Models + Extensions/CurrentChatUser_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/Models + Extensions/CurrentChatUser_Mock.swift @@ -21,10 +21,12 @@ public extension CurrentChatUser { lastActiveAt: Date? = nil, teams: Set = [], language: TranslationLanguage? = nil, + blockedUserIds: Set = [], extraData: [String: RawJSON] = [:], devices: [Device] = [], currentDevice: Device? = nil, mutedUsers: Set = [], + blockedUsers: Set = [], flaggedUsers: Set = [], flaggedMessageIDs: Set = [], unreadCount: UnreadCount = .noUnread, @@ -48,6 +50,7 @@ public extension CurrentChatUser { extraData: extraData, devices: devices, currentDevice: currentDevice, + blockedUserIds: blockedUserIds, mutedUsers: mutedUsers, flaggedUsers: flaggedUsers, flaggedMessageIDs: flaggedMessageIDs, diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift index d46c10a7784..4cbb6e6aa88 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/UserUpdater_Mock.swift @@ -23,6 +23,14 @@ final class UserUpdater_Mock: UserUpdater { @Atomic var flagUser_userId: UserId? @Atomic var flagUser_completion: ((Error?) -> Void)? @Atomic var flagUser_completion_result: Result? + + @Atomic var blockUser_userId: UserId? + @Atomic var blockUser_completion: ((Error?) -> Void)? + @Atomic var blockUser_completion_result: Result? + + @Atomic var unblockUser_userId: UserId? + @Atomic var unblockUser_completion: ((Error?) -> Void)? + @Atomic var unblockUser_completion_result: Result? override func muteUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { muteUser_userId = userId @@ -48,6 +56,18 @@ final class UserUpdater_Mock: UserUpdater { flagUser_completion = completion flagUser_completion_result?.invoke(with: completion) } + + override func blockUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + blockUser_userId = userId + blockUser_completion = completion + blockUser_completion_result?.invoke(with: completion) + } + + override func unblockUser(_ userId: UserId, completion: ((Error?) -> Void)? = nil) { + unblockUser_userId = userId + unblockUser_completion = completion + unblockUser_completion_result?.invoke(with: completion) + } // Cleans up all recorded values func cleanUp() { @@ -67,5 +87,13 @@ final class UserUpdater_Mock: UserUpdater { flagUser_userId = nil flagUser_completion = nil flagUser_completion_result = nil + + blockUser_userId = nil + blockUser_completion = nil + blockUser_completion_result = nil + + unblockUser_userId = nil + unblockUser_completion = nil + unblockUser_completion_result = nil } } diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift b/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift index 010f829c1b2..da94b5d38d9 100644 --- a/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift +++ b/TestTools/StreamChatTestTools/TestData/DummyData/ChannelDetailPayload.swift @@ -22,6 +22,7 @@ extension ChannelDetailPayload { config: ChannelConfig = .mock(), ownCapabilities: [String] = [], isFrozen: Bool = false, + isBlocked: Bool = false, isHidden: Bool? = nil, members: [MemberPayload] = [], team: String? = nil, @@ -42,6 +43,7 @@ extension ChannelDetailPayload { config: config, ownCapabilities: ownCapabilities, isFrozen: isFrozen, + isBlocked: isBlocked, isHidden: isHidden, members: members, memberCount: members.count, diff --git a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift index 5025ba3a26e..5097e987414 100644 --- a/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift +++ b/TestTools/StreamChatTestTools/TestData/DummyData/XCTestCase+Dummy.swift @@ -183,6 +183,7 @@ extension XCTestCase { config: channelConfig, ownCapabilities: ownCapabilities, isFrozen: true, + isBlocked: false, isHidden: nil, members: members, memberCount: 100, @@ -295,6 +296,7 @@ extension XCTestCase { ), ownCapabilities: [], isFrozen: true, + isBlocked: false, isHidden: nil, members: nil, memberCount: 100, diff --git a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift index 243f4c549ee..2db7b267874 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/EndpointPath_Tests.swift @@ -110,6 +110,7 @@ final class EndpointPathTests: XCTestCase { assertResultEncodingAndDecoding(.flagUser(false)) assertResultEncodingAndDecoding(.flagMessage(false)) assertResultEncodingAndDecoding(.muteUser(false)) + assertResultEncodingAndDecoding(.blockUser) assertResultEncodingAndDecoding(.polls) assertResultEncodingAndDecoding(.pollsQuery) diff --git a/Tests/StreamChatTests/APIClient/Endpoints/ModerationEndpoints_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/ModerationEndpoints_Tests.swift index b1b2f1ce288..3b6531dbec1 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/ModerationEndpoints_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/ModerationEndpoints_Tests.swift @@ -157,4 +157,42 @@ final class ModerationEndpoints_Tests: XCTestCase { XCTAssertEqual(flag ? "moderation/flag" : "moderation/unflag", endpoint.path.value) } } + + func test_blockUser_buildsCorrectly() { + let userId: UserId = .unique + + let expectedEndpoint = Endpoint( + path: .blockUser, + method: .post, + queryItems: nil, + requiresConnectionId: false, + body: ["blocked_user_id": userId] + ) + + // Build endpoint + let endpoint: Endpoint = .blockUser(userId) + + // Assert endpoint is built correctly + XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint)) + XCTAssertEqual("users/block", endpoint.path.value) + } + + func test_unblockUser_buildsCorrectly() { + let userId: UserId = .unique + + let expectedEndpoint = Endpoint( + path: .unblockUser, + method: .post, + queryItems: nil, + requiresConnectionId: false, + body: ["blocked_user_id": userId] + ) + + // Build endpoint + let endpoint: Endpoint = .unblockUser(userId) + + // Assert endpoint is built correctly + XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint)) + XCTAssertEqual("users/unblock", endpoint.path.value) + } } diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift index bffe946577b..32b3e33d45a 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift @@ -144,6 +144,7 @@ final class ChannelListPayload_Tests: XCTestCase { "delete-channel" ], isFrozen: true, + isBlocked: false, isHidden: false, members: channelUsers.map { MemberPayload.dummy( diff --git a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift index 78a08a11631..079d1297018 100644 --- a/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift +++ b/Tests/StreamChatTests/APIClient/Endpoints/Payloads/IdentifiablePayload_Tests.swift @@ -306,6 +306,7 @@ final class IdentifiablePayload_Tests: XCTestCase { config: .mock(), ownCapabilities: [], isFrozen: true, + isBlocked: false, isHidden: false, members: users.map { MemberPayload.dummy(user: $0) }, memberCount: users.count, diff --git a/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift b/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift index d501701b1bb..f8e86cbf4b5 100644 --- a/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/SearchControllers/UserController/UserController_Tests.swift @@ -490,7 +490,7 @@ final class UserController_Tests: XCTestCase { // Assert controller is kept alive AssertAsync.staysTrue(weakController != nil) } - + // MARK: - Unlag user func test_unflagUser_propagatesError() { @@ -559,6 +559,140 @@ final class UserController_Tests: XCTestCase { // Assert controller is kept alive AssertAsync.staysTrue(weakController != nil) } + + // MARK: - Block user + + func test_blockUser_propagatesError() { + // Simulate `block` call and catch the completion. + var completionError: Error? + controller.block { [callbackQueueID] in + AssertTestQueue(withId: callbackQueueID) + completionError = $0 + } + + // Simulate network response with the error. + let networkError = TestError() + env.userUpdater!.blockUser_completion!(networkError) + + // Assert error is propogated. + AssertAsync.willBeEqual(completionError as? TestError, networkError) + } + + func test_blockUser_propagatesNilError() { + // Simulate `block` call and catch the completion. + var completionIsCalled = false + controller.block { [callbackQueueID] error in + // Assert callback queue is correct. + AssertTestQueue(withId: callbackQueueID) + // Assert there is no error. + XCTAssertNil(error) + completionIsCalled = true + } + + // Keep a weak ref so we can check if it's actually deallocated + weak var weakController = controller + + // (Try to) deallocate the controller + // by not keeping any references to it + controller = nil + + // Simulate successful network response. + env.userUpdater!.blockUser_completion!(nil) + // Release reference of completion so we can deallocate stuff + env.userUpdater!.blockUser_completion = nil + + // Assert completion is called. + AssertAsync.willBeTrue(completionIsCalled) + // `weakController` should be deallocated too + AssertAsync.canBeReleased(&weakController) + } + + func test_blockUser_callsUserUpdater_withCorrectValues() { + // Simulate `block` call. + controller.block() + + // Assert updater is called with correct `userId` + XCTAssertEqual(env.userUpdater!.blockUser_userId, controller.userId) + } + + func test_blockUser_keepsControllerAlive() { + // Simulate `block` call. + controller.block() + + // Create a weak ref and release a controller. + weak var weakController = controller + controller = nil + + // Assert controller is kept alive + AssertAsync.staysTrue(weakController != nil) + } + + // MARK: - Unblock user + + func test_unblockUser_propagatesError() { + // Simulate `unblock` call and catch the completion. + var completionError: Error? + controller.unblock { [callbackQueueID] in + AssertTestQueue(withId: callbackQueueID) + completionError = $0 + } + + // Simulate network response with the error. + let networkError = TestError() + env.userUpdater!.unblockUser_completion!(networkError) + + // Assert error is propogated. + AssertAsync.willBeEqual(completionError as? TestError, networkError) + } + + func test_unblockUser_propagatesNilError() { + // Simulate `unblock` call and catch the completion. + var completionIsCalled = false + controller.unblock { [callbackQueueID] error in + // Assert callback queue is correct. + AssertTestQueue(withId: callbackQueueID) + // Assert there is no error. + XCTAssertNil(error) + completionIsCalled = true + } + + // Keep a weak ref so we can check if it's actually deallocated + weak var weakController = controller + + // (Try to) deallocate the controller + // by not keeping any references to it + controller = nil + + // Simulate successful network response. + env.userUpdater!.unblockUser_completion!(nil) + // Release reference of completion so we can deallocate stuff + env.userUpdater!.unblockUser_completion = nil + + // Assert completion is called. + AssertAsync.willBeTrue(completionIsCalled) + // `weakController` should be deallocated too + AssertAsync.canBeReleased(&weakController) + } + + func test_unblockUser_callsUserUpdater_withCorrectValues() { + // Simulate `unblock` call. + controller.unblock() + + // Assert updater is called with correct `userId` + XCTAssertEqual(env.userUpdater!.unblockUser_userId, controller.userId) + } + + func test_unblockUser_keepsControllerAlive() { + // Simulate `unblock` call. + controller.unblock() + + // Create a weak ref and release a controller. + weak var weakController = controller + controller = nil + + // Assert controller is kept alive + AssertAsync.staysTrue(weakController != nil) + } } private class TestEnvironment { diff --git a/Tests/StreamChatTests/Query/ChannelListFilterScope_Tests.swift b/Tests/StreamChatTests/Query/ChannelListFilterScope_Tests.swift index e2fbca8c33e..0207f2366f3 100644 --- a/Tests/StreamChatTests/Query/ChannelListFilterScope_Tests.swift +++ b/Tests/StreamChatTests/Query/ChannelListFilterScope_Tests.swift @@ -44,6 +44,7 @@ final class ChannelListFilterScope_Tests: XCTestCase { XCTAssertEqual(Key.createdBy.keyPathString, "createdBy.id") XCTAssertEqual(Key.updatedAt.keyPathString, "updatedAt") XCTAssertEqual(Key.deletedAt.keyPathString, "deletedAt") + XCTAssertEqual(Key.blocked.keyPathString, "isBlocked") XCTAssertEqual(Key.hidden.keyPathString, "isHidden") XCTAssertEqual(Key.frozen.keyPathString, "isFrozen") XCTAssertEqual(Key.memberCount.keyPathString, "memberCount") diff --git a/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift b/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift index 590667e6cd5..8b703c92da1 100644 --- a/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/ConnectedUser_Tests.swift @@ -112,7 +112,7 @@ final class ConnectedUser_Tests: XCTestCase { XCTAssertEqual(id, env.userUpdaterMock.muteUser_userId) } - func test_unmuteUser_whenUpdatedSucceeds_thenMuteUserSucceeds() async throws { + func test_unmuteUser_whenUpdatedSucceeds_thenUnmuteUserSucceeds() async throws { try await setUpConnectedUser(usesMockedUpdaters: true) env.userUpdaterMock.unmuteUser_completion_result = .success(()) @@ -121,7 +121,7 @@ final class ConnectedUser_Tests: XCTestCase { XCTAssertEqual(id, env.userUpdaterMock.unmuteUser_userId) } - func test_flagUser_whenUpdatedSucceeds_thenMuteUserSucceeds() async throws { + func test_flagUser_whenUpdatedSucceeds_thenFlagUserSucceeds() async throws { try await setUpConnectedUser(usesMockedUpdaters: true) env.userUpdaterMock.flagUser_completion_result = .success(()) @@ -131,7 +131,7 @@ final class ConnectedUser_Tests: XCTestCase { XCTAssertEqual(id, env.userUpdaterMock.flagUser_userId) } - func test_unflagUser_whenUpdatedSucceeds_thenMuteUserSucceeds() async throws { + func test_unflagUser_whenUpdatedSucceeds_thenUnflagUserSucceeds() async throws { try await setUpConnectedUser(usesMockedUpdaters: true) env.userUpdaterMock.flagUser_completion_result = .success(()) @@ -141,6 +141,24 @@ final class ConnectedUser_Tests: XCTestCase { XCTAssertEqual(id, env.userUpdaterMock.flagUser_userId) } + func test_blockUser_whenUpdatedSucceeds_thenBlockUserSucceeds() async throws { + try await setUpConnectedUser(usesMockedUpdaters: true) + + env.userUpdaterMock.blockUser_completion_result = .success(()) + let id = UserId.unique + try await connectedUser.blockUser(id) + XCTAssertEqual(id, env.userUpdaterMock.blockUser_userId) + } + + func test_unblockUser_whenUpdatedSucceeds_thenUnblockUserSucceeds() async throws { + try await setUpConnectedUser(usesMockedUpdaters: true) + + env.userUpdaterMock.unblockUser_completion_result = .success(()) + let id = UserId.unique + try await connectedUser.unblockUser(id) + XCTAssertEqual(id, env.userUpdaterMock.unblockUser_userId) + } + // MARK: - Test Data @MainActor private func setUpConnectedUser(usesMockedUpdaters: Bool, loadState: Bool = true, initialDeviceCount: Int = 0) async throws { diff --git a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift index 2116c7ed33f..2fc7bcfc6c0 100644 --- a/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift +++ b/Tests/StreamChatTests/WebSocketClient/EventMiddlewares/ChannelReadUpdaterMiddleware_Tests.swift @@ -937,6 +937,7 @@ final class ChannelReadUpdaterMiddleware_Tests: XCTestCase { config: .init(), ownCapabilities: [], isFrozen: false, + isBlocked: false, isHidden: nil, members: nil, memberCount: 0, diff --git a/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift b/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift index a733cf18e49..b8261e8d125 100644 --- a/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/UserUpdater_Tests.swift @@ -352,4 +352,95 @@ final class UserUpdater_Tests: XCTestCase { // Assert database error is propogated. AssertAsync.willBeEqual(completionCalledError as? TestError, databaseError) } + + // MARK: - Block user + + func test_blockUser_makesCorrectAPICall() { + let userId: UserId = .unique + + // Simulate `blockUser` call + userUpdater.blockUser(userId) + + // Assert correct endpoint is called + XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(.blockUser(userId))) + } + + func test_blockUser_propagatesSuccessfulResponse() { + // Simulate `blockUser` call + var completionCalled = false + userUpdater.blockUser(.unique) { error in + XCTAssertNil(error) + completionCalled = true + } + + // Assert completion is not called yet + XCTAssertFalse(completionCalled) + + // Simulate API response with success + let payload: BlockingUserPayload = .init(blockedUserId: .unique, blockedByUserId: .unique, createdAt: .unique) + apiClient.test_simulateResponse(Result.success(payload)) + + // Assert completion is called + AssertAsync.willBeTrue(completionCalled) + } + + func test_blockUser_propagatesError() { + // Simulate `blockUser` call + var completionCalledError: Error? + userUpdater.blockUser(.unique) { + completionCalledError = $0 + } + + // Simulate API response with failure + let error = TestError() + apiClient.test_simulateResponse(Result.failure(error)) + + // Assert the completion is called with the error + AssertAsync.willBeEqual(completionCalledError as? TestError, error) + } + + // MARK: - Unblock user + + func test_unblockUser_makesCorrectAPICall() { + let userId: UserId = .unique + + // Simulate `unblockUser` call + userUpdater.unblockUser(userId) + + // Assert correct endpoint is called + XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(.unblockUser(userId))) + } + + func test_unblockUser_propagatesSuccessfulResponse() { + // Simulate `blockUser` call + var completionCalled = false + userUpdater.unblockUser(.unique) { error in + XCTAssertNil(error) + completionCalled = true + } + + // Assert completion is not called yet + XCTAssertFalse(completionCalled) + + // Simulate API response with success + apiClient.test_simulateResponse(Result.success(.init())) + + // Assert completion is called + AssertAsync.willBeTrue(completionCalled) + } + + func test_unblockUser_propagatesError() { + // Simulate `blockUser` call + var completionCalledError: Error? + userUpdater.unblockUser(.unique) { + completionCalledError = $0 + } + + // Simulate API response with failure + let error = TestError() + apiClient.test_simulateResponse(Result.failure(error)) + + // Assert the completion is called with the error + AssertAsync.willBeEqual(completionCalledError as? TestError, error) + } } diff --git a/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift index 6ec859ecf51..a00e176bcb3 100644 --- a/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift @@ -110,6 +110,19 @@ final class ChatMessageActionsVC_Tests: XCTestCase { XCTAssertTrue(vc.messageActions.contains(where: { $0 is UnmuteUserActionItem })) } + + func test_messageActions_whenBlockingEnabled_containsBlockAction() throws { + chatMessageController.simulateInitial( + message: ChatMessage.mock(isSentByCurrentUser: false), + replies: [], + state: .remoteDataFetched + ) + + vc.channel = .mock(cid: .unique, ownCapabilities: []) + vc.components.isBlockingUsersEnabled = true + + XCTAssertTrue(vc.messageActions.contains(where: { $0 is BlockUserActionItem })) + } func test_messageActions_whenQuotesEnabled_containsQuoteAction() { vc.channel = .mock(cid: .unique, ownCapabilities: [.quoteMessage]) diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components-properties.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components-properties.md index 819c5d08880..4ca3082937d 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components-properties.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components-properties.md @@ -703,6 +703,13 @@ When true the suggestionsVC will search users from the entire application instea public var mentionAllAppUsers: Bool = false ``` +### `isBlockingUsersEnabled` + +Determines if users are able to block other users. Disabled by default. +``` swift +public var isBlockingUsersEnabled = false +``` + ### `suggestionsCollectionView` The collection view of the suggestions view controller. diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components.md index f0788246044..96d61c75162 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat-ui/components.md @@ -722,6 +722,13 @@ When true the suggestionsVC will search users from the entire application instea public var mentionAllAppUsers: Bool = false ``` +### `isBlockingUsersEnabled` + +Determines if users are able to block other users. Disabled by default. +``` swift +public var isBlockingUsersEnabled = false +``` + ### `suggestionsCollectionView` The collection view of the suggestions view controller. diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-coding-keys.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-coding-keys.md index 2378f607a74..3e618e6ebd6 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-coding-keys.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-coding-keys.md @@ -148,3 +148,11 @@ This value will be 0 if the channel is not in slow mode. ``` swift case cooldownDuration = "cooldown" ``` + +### `blocked` + +A blocked flag. + +``` swift +case blocked +``` diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config-properties.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config-properties.md index 0e142ab96c4..f2274f11f5e 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config-properties.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config-properties.md @@ -124,4 +124,5 @@ public let updatedAt: Date Determines if users are able to flag messages. Enabled by default. ``` swift -public var flagsEnabled: Bool +public var flagsEnabled: Bool +``` diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config.md index 06d73617635..9b4c6916be2 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/api-client/endpoints/payloads/channel-config.md @@ -139,5 +139,5 @@ public let updatedAt: Date Determines if users are able to flag messages. Enabled by default. ``` swift -public var flagsEnabled: Bool +public var flagsEnabled: Bool ``` diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller-properties.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller-properties.md index d0b58c053ae..0570bfbae6d 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller-properties.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller-properties.md @@ -98,6 +98,30 @@ Un-mutes the user this controller manages. func unmute(completion: ((Error?) -> Void)? = nil) ``` +#### Parameters + + - `completion`: The completion. Will be called on a **`callbackQueue`** when the network request is finished. + +### `block(completion:)` + +Blocks the user this controller manages. + +``` swift +func block(completion: ((Error?) -> Void)? = nil) +``` + +#### 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. + +### `unblock(completion:)` + +Un-blocks the user this controller manages. + +``` swift +func unblock(completion: ((Error?) -> Void)? = nil) +``` + #### Parameters - `completion`: The completion. Will be called on a **`callbackQueue`** when the network request is finished. diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller.md index 82cc9b97c65..15f26f2ccca 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/controllers/user-controller/chat-user-controller.md @@ -116,6 +116,30 @@ Un-mutes the user this controller manages. func unmute(completion: ((Error?) -> Void)? = nil) ``` +#### Parameters + + - `completion`: The completion. Will be called on a **`callbackQueue`** when the network request is finished. + +### `block(completion:)` + +Blocks the user this controller manages. + +``` swift +func block(completion: ((Error?) -> Void)? = nil) +``` + +#### 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. + +### `unblock(completion:)` + +Un-blocks the user this controller manages. + +``` swift +func unblock(completion: ((Error?) -> Void)? = nil) +``` + #### Parameters - `completion`: The completion. Will be called on a **`callbackQueue`** when the network request is finished. diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user-properties.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user-properties.md index 4f49f92d5a3..9bae8ad2263 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user-properties.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user-properties.md @@ -23,6 +23,14 @@ A set of users muted by the user. public let mutedUsers: Set ``` +### `blockedUsers` + +A set of users blocked by the user. + +``` swift +public let blockedUsers: Set +``` + ### `flaggedUsers` A set of users flagged by the user. diff --git a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user.md b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user.md index 383383b479f..b1a23944cc0 100644 --- a/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user.md +++ b/docusaurus/docs/iOS/common-content/reference-docs/stream-chat/models/current-chat-user.md @@ -39,6 +39,14 @@ A set of users muted by the user. public let mutedUsers: Set ``` +### `blockedUsers` + +A set of users blocked by the user. + +``` swift +public let blockedUsers: Set +``` + ### `flaggedUsers` A set of users flagged by the user.