mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add alerts with reason for kick and ban (#3967)
* feat: added alerts with reason for kick and ban in the room members list screen * fix tests
This commit is contained in:
@@ -208,6 +208,7 @@
|
||||
"common_privacy_policy" = "Privacy policy";
|
||||
"common_reaction" = "Reaction";
|
||||
"common_reactions" = "Reactions";
|
||||
"common_reason" = "Reason";
|
||||
"common_recovery_key" = "Recovery key";
|
||||
"common_refreshing" = "Refreshing…";
|
||||
"common_replying_to" = "Replying to %1$@";
|
||||
@@ -875,6 +876,9 @@
|
||||
"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
|
||||
"screen_room_member_list_banned_empty" = "There are no banned users in this room.";
|
||||
"screen_room_member_list_banning_user" = "Banning %1$@";
|
||||
"screen_room_member_list_kick_member_confirmation_action" = "Remove";
|
||||
"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
|
||||
"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
|
||||
"screen_room_member_list_manage_member_ban" = "Remove and ban member";
|
||||
"screen_room_member_list_manage_member_remove" = "Remove from room";
|
||||
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"common_privacy_policy" = "Privacy policy";
|
||||
"common_reaction" = "Reaction";
|
||||
"common_reactions" = "Reactions";
|
||||
"common_reason" = "Reason";
|
||||
"common_recovery_key" = "Recovery key";
|
||||
"common_refreshing" = "Refreshing…";
|
||||
"common_replying_to" = "Replying to %1$@";
|
||||
@@ -875,6 +876,9 @@
|
||||
"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
|
||||
"screen_room_member_list_banned_empty" = "There are no banned users in this room.";
|
||||
"screen_room_member_list_banning_user" = "Banning %1$@";
|
||||
"screen_room_member_list_kick_member_confirmation_action" = "Remove";
|
||||
"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
|
||||
"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
|
||||
"screen_room_member_list_manage_member_ban" = "Remove and ban member";
|
||||
"screen_room_member_list_manage_member_remove" = "Remove from room";
|
||||
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
|
||||
|
||||
@@ -478,6 +478,8 @@ internal enum L10n {
|
||||
internal static var commonReaction: String { return L10n.tr("Localizable", "common_reaction") }
|
||||
/// Reactions
|
||||
internal static var commonReactions: String { return L10n.tr("Localizable", "common_reactions") }
|
||||
/// Reason
|
||||
internal static var commonReason: String { return L10n.tr("Localizable", "common_reason") }
|
||||
/// Recovery key
|
||||
internal static var commonRecoveryKey: String { return L10n.tr("Localizable", "common_recovery_key") }
|
||||
/// Refreshing…
|
||||
@@ -2050,6 +2052,12 @@ internal enum L10n {
|
||||
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
|
||||
}
|
||||
/// Remove
|
||||
internal static var screenRoomMemberListKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_action") }
|
||||
/// They will be able to join this room again if invited.
|
||||
internal static var screenRoomMemberListKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_description") }
|
||||
/// Are you sure you want to remove this member?
|
||||
internal static var screenRoomMemberListKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_title") }
|
||||
/// Remove and ban member
|
||||
internal static var screenRoomMemberListManageMemberBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_ban") }
|
||||
/// Remove from room
|
||||
|
||||
@@ -9077,15 +9077,15 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
//MARK: - kickUser
|
||||
|
||||
var kickUserUnderlyingCallsCount = 0
|
||||
var kickUserCallsCount: Int {
|
||||
var kickUserReasonUnderlyingCallsCount = 0
|
||||
var kickUserReasonCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return kickUserUnderlyingCallsCount
|
||||
return kickUserReasonUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = kickUserUnderlyingCallsCount
|
||||
returnValue = kickUserReasonUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -9093,29 +9093,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
kickUserUnderlyingCallsCount = newValue
|
||||
kickUserReasonUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
kickUserUnderlyingCallsCount = newValue
|
||||
kickUserReasonUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var kickUserCalled: Bool {
|
||||
return kickUserCallsCount > 0
|
||||
var kickUserReasonCalled: Bool {
|
||||
return kickUserReasonCallsCount > 0
|
||||
}
|
||||
var kickUserReceivedUserID: String?
|
||||
var kickUserReceivedInvocations: [String] = []
|
||||
var kickUserReasonReceivedArguments: (userID: String, reason: String?)?
|
||||
var kickUserReasonReceivedInvocations: [(userID: String, reason: String?)] = []
|
||||
|
||||
var kickUserUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var kickUserReturnValue: Result<Void, RoomProxyError>! {
|
||||
var kickUserReasonUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var kickUserReasonReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return kickUserUnderlyingReturnValue
|
||||
return kickUserReasonUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = kickUserUnderlyingReturnValue
|
||||
returnValue = kickUserReasonUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -9123,39 +9123,39 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
kickUserUnderlyingReturnValue = newValue
|
||||
kickUserReasonUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
kickUserUnderlyingReturnValue = newValue
|
||||
kickUserReasonUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var kickUserClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
var kickUserReasonClosure: ((String, String?) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func kickUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
kickUserCallsCount += 1
|
||||
kickUserReceivedUserID = userID
|
||||
func kickUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
||||
kickUserReasonCallsCount += 1
|
||||
kickUserReasonReceivedArguments = (userID: userID, reason: reason)
|
||||
DispatchQueue.main.async {
|
||||
self.kickUserReceivedInvocations.append(userID)
|
||||
self.kickUserReasonReceivedInvocations.append((userID: userID, reason: reason))
|
||||
}
|
||||
if let kickUserClosure = kickUserClosure {
|
||||
return await kickUserClosure(userID)
|
||||
if let kickUserReasonClosure = kickUserReasonClosure {
|
||||
return await kickUserReasonClosure(userID, reason)
|
||||
} else {
|
||||
return kickUserReturnValue
|
||||
return kickUserReasonReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - banUser
|
||||
|
||||
var banUserUnderlyingCallsCount = 0
|
||||
var banUserCallsCount: Int {
|
||||
var banUserReasonUnderlyingCallsCount = 0
|
||||
var banUserReasonCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return banUserUnderlyingCallsCount
|
||||
return banUserReasonUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = banUserUnderlyingCallsCount
|
||||
returnValue = banUserReasonUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -9163,29 +9163,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
banUserUnderlyingCallsCount = newValue
|
||||
banUserReasonUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
banUserUnderlyingCallsCount = newValue
|
||||
banUserReasonUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var banUserCalled: Bool {
|
||||
return banUserCallsCount > 0
|
||||
var banUserReasonCalled: Bool {
|
||||
return banUserReasonCallsCount > 0
|
||||
}
|
||||
var banUserReceivedUserID: String?
|
||||
var banUserReceivedInvocations: [String] = []
|
||||
var banUserReasonReceivedArguments: (userID: String, reason: String?)?
|
||||
var banUserReasonReceivedInvocations: [(userID: String, reason: String?)] = []
|
||||
|
||||
var banUserUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var banUserReturnValue: Result<Void, RoomProxyError>! {
|
||||
var banUserReasonUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||
var banUserReasonReturnValue: Result<Void, RoomProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return banUserUnderlyingReturnValue
|
||||
return banUserReasonUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = banUserUnderlyingReturnValue
|
||||
returnValue = banUserReasonUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -9193,26 +9193,26 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
banUserUnderlyingReturnValue = newValue
|
||||
banUserReasonUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
banUserUnderlyingReturnValue = newValue
|
||||
banUserReasonUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var banUserClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
var banUserReasonClosure: ((String, String?) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func banUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
banUserCallsCount += 1
|
||||
banUserReceivedUserID = userID
|
||||
func banUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
||||
banUserReasonCallsCount += 1
|
||||
banUserReasonReceivedArguments = (userID: userID, reason: reason)
|
||||
DispatchQueue.main.async {
|
||||
self.banUserReceivedInvocations.append(userID)
|
||||
self.banUserReasonReceivedInvocations.append((userID: userID, reason: reason))
|
||||
}
|
||||
if let banUserClosure = banUserClosure {
|
||||
return await banUserClosure(userID)
|
||||
if let banUserReasonClosure = banUserReasonClosure {
|
||||
return await banUserReasonClosure(userID, reason)
|
||||
} else {
|
||||
return banUserReturnValue
|
||||
return banUserReasonReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - unbanUser
|
||||
|
||||
@@ -107,8 +107,8 @@ extension JoinedRoomProxyMock {
|
||||
canUserJoinCallUserIDReturnValue = .success(configuration.canUserJoinCall)
|
||||
canUserPinOrUnpinUserIDReturnValue = .success(configuration.canUserPin)
|
||||
|
||||
kickUserReturnValue = .success(())
|
||||
banUserReturnValue = .success(())
|
||||
kickUserReasonReturnValue = .success(())
|
||||
banUserReasonReturnValue = .success(())
|
||||
unbanUserReturnValue = .success(())
|
||||
|
||||
let widgetDriver = ElementCallWidgetDriverMock()
|
||||
|
||||
@@ -111,4 +111,6 @@ enum RoomMembersListScreenViewAction {
|
||||
|
||||
enum RoomMembersListScreenAlertType: Hashable {
|
||||
case unbanConfirmation(RoomMemberDetails)
|
||||
case kickConfirmation
|
||||
case banConfirmation
|
||||
}
|
||||
|
||||
@@ -51,9 +51,31 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
case .showMemberDetails(let member):
|
||||
showMemberDetails(member)
|
||||
case .kickMember(let member):
|
||||
Task { await kickMember(member) }
|
||||
var reason: String?
|
||||
let binding: Binding<String> = .init(get: { reason ?? "" },
|
||||
set: { reason = $0.isEmpty ? nil : $0 })
|
||||
state.bindings.alertInfo = .init(id: .kickConfirmation,
|
||||
title: L10n.screenRoomMemberListKickMemberConfirmationTitle,
|
||||
message: L10n.screenRoomMemberListKickMemberConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
|
||||
secondaryButton: .init(title: L10n.screenRoomMemberListKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
text: binding,
|
||||
autoCapitalization: .sentences,
|
||||
autoCorrectionDisabled: false)])
|
||||
case .banMember(let member):
|
||||
Task { await banMember(member) }
|
||||
var reason: String?
|
||||
let binding: Binding<String> = .init(get: { reason ?? "" },
|
||||
set: { reason = $0.isEmpty ? nil : $0 })
|
||||
state.bindings.alertInfo = .init(id: .banConfirmation,
|
||||
title: L10n.screenRoomMemberListBanMemberConfirmationTitle,
|
||||
message: L10n.screenRoomMemberListBanMemberConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
|
||||
secondaryButton: .init(title: L10n.screenRoomMemberListBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
text: binding,
|
||||
autoCapitalization: .sentences,
|
||||
autoCorrectionDisabled: false)])
|
||||
case .unbanMember(let member):
|
||||
Task { await unbanMember(member) }
|
||||
case .invite:
|
||||
@@ -183,11 +205,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
|
||||
// MARK: - Member Management
|
||||
|
||||
private func kickMember(_ member: RoomMemberDetails) async {
|
||||
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListRemovingUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.kickUser(member.id) {
|
||||
switch await roomProxy.kickUser(member.id, reason: reason) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
@@ -197,11 +219,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
}
|
||||
}
|
||||
|
||||
private func banMember(_ member: RoomMemberDetails) async {
|
||||
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListBanningUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.banUser(member.id) {
|
||||
switch await roomProxy.banUser(member.id, reason: reason) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
+1
-11
@@ -14,8 +14,6 @@ struct RoomMembersListManageMemberSheet: View {
|
||||
|
||||
@ObservedObject var context: RoomMembersListScreenViewModel.Context
|
||||
|
||||
@State private var isPresentingBanConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
AvatarHeaderView(member: member,
|
||||
@@ -44,7 +42,7 @@ struct RoomMembersListManageMemberSheet: View {
|
||||
icon: \.block,
|
||||
role: .destructive),
|
||||
kind: .button {
|
||||
isPresentingBanConfirmation = true
|
||||
context.send(viewAction: .banMember(member))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -53,14 +51,6 @@ struct RoomMembersListManageMemberSheet: View {
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.large, .fraction(0.54)]) // Maybe find a way to use the ideal height somehow?
|
||||
.alert(L10n.screenRoomMemberListBanMemberConfirmationTitle, isPresented: $isPresentingBanConfirmation) {
|
||||
Button(L10n.actionCancel, role: .cancel) { }
|
||||
Button(L10n.screenRoomMemberListBanMemberConfirmationAction) {
|
||||
context.send(viewAction: .banMember(member))
|
||||
}
|
||||
} message: {
|
||||
Text(L10n.screenRoomMemberListBanMemberConfirmationDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -623,9 +623,9 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
|
||||
// MARK: - Moderation
|
||||
|
||||
func kickUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
func kickUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.kickUser(userId: userID, reason: nil)
|
||||
try await room.kickUser(userId: userID, reason: reason)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed kicking \(userID) with error: \(error)")
|
||||
@@ -633,9 +633,9 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func banUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
func banUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.banUser(userId: userID, reason: nil)
|
||||
try await room.banUser(userId: userID, reason: reason)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed banning \(userID) with error: \(error)")
|
||||
|
||||
@@ -159,8 +159,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||
|
||||
// MARK: - Moderation
|
||||
|
||||
func kickUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
func banUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
func kickUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError>
|
||||
func banUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError>
|
||||
func unbanUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
// MARK: - Element Call
|
||||
|
||||
@@ -249,28 +249,42 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
func testKickMember() async throws {
|
||||
setup(with: .allMembersAsAdmin)
|
||||
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Ban member")
|
||||
roomProxy.kickUserReasonClosure = { _, _ in
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0].member))
|
||||
|
||||
// Calling the mock won't actually change any view state, so sleep instead.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(roomProxy.kickUserCalled)
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testBanMember() async throws {
|
||||
setup(with: .allMembersAsAdmin)
|
||||
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Ban member")
|
||||
roomProxy.banUserReasonClosure = { _, _ in
|
||||
defer { expectation.fulfill() }
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0].member))
|
||||
|
||||
// Calling the mock won't actually change any view state, so sleep instead.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(roomProxy.banUserCalled)
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testUnbanMember() async throws {
|
||||
|
||||
Reference in New Issue
Block a user