Final changes for Room Moderation (#2576)

* Add confirmation that the user would like to discard unsaved changes.

* Update string on permissions screen.

* Fix UI tests and update snapshots.

* Fix integration tests.

* Run periphery for Moderation feature.
This commit is contained in:
Doug 2024-03-15 13:41:23 +00:00 committed by GitHub
parent 668c5188fb
commit 3ba34c0704
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 84 additions and 47 deletions

View File

@ -499,7 +499,7 @@
"screen_room_attachment_text_formatting" = "Text Formatting"; "screen_room_attachment_text_formatting" = "Text Formatting";
"screen_room_change_permissions_administrators" = "Admins only"; "screen_room_change_permissions_administrators" = "Admins only";
"screen_room_change_permissions_ban_people" = "Ban people"; "screen_room_change_permissions_ban_people" = "Ban people";
"screen_room_change_permissions_delete_messages" = "Delete messages"; "screen_room_change_permissions_delete_messages" = "Remove messages";
"screen_room_change_permissions_invite_people" = "Invite people"; "screen_room_change_permissions_invite_people" = "Invite people";
"screen_room_change_permissions_moderators" = "Admins and moderators"; "screen_room_change_permissions_moderators" = "Admins and moderators";
"screen_room_change_permissions_remove_people" = "Remove people"; "screen_room_change_permissions_remove_people" = "Remove people";

View File

@ -50,6 +50,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private let analytics: AnalyticsService private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
// periphery:ignore - used to avoid deallocation
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator? private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
private let stateMachine: StateMachine<State, Event> = .init(state: .initial) private let stateMachine: StateMachine<State, Event> = .init(state: .initial)

View File

@ -1215,7 +1215,7 @@ internal enum L10n {
internal static var screenRoomChangePermissionsAdministrators: String { return L10n.tr("Localizable", "screen_room_change_permissions_administrators") } internal static var screenRoomChangePermissionsAdministrators: String { return L10n.tr("Localizable", "screen_room_change_permissions_administrators") }
/// Ban people /// Ban people
internal static var screenRoomChangePermissionsBanPeople: String { return L10n.tr("Localizable", "screen_room_change_permissions_ban_people") } internal static var screenRoomChangePermissionsBanPeople: String { return L10n.tr("Localizable", "screen_room_change_permissions_ban_people") }
/// Delete messages /// Remove messages
internal static var screenRoomChangePermissionsDeleteMessages: String { return L10n.tr("Localizable", "screen_room_change_permissions_delete_messages") } internal static var screenRoomChangePermissionsDeleteMessages: String { return L10n.tr("Localizable", "screen_room_change_permissions_delete_messages") }
/// Everyone /// Everyone
internal static var screenRoomChangePermissionsEveryone: String { return L10n.tr("Localizable", "screen_room_change_permissions_everyone") } internal static var screenRoomChangePermissionsEveryone: String { return L10n.tr("Localizable", "screen_room_change_permissions_everyone") }

View File

@ -43,6 +43,8 @@ struct RoomChangePermissionsScreenViewStateBindings: BindableState {
} }
enum RoomChangePermissionsScreenAlertType { enum RoomChangePermissionsScreenAlertType {
/// A confirmation that the user would like to discard any unsaved changes.
case discardChanges
/// The generic error message. /// The generic error message.
case generic case generic
} }
@ -50,6 +52,8 @@ enum RoomChangePermissionsScreenAlertType {
enum RoomChangePermissionsScreenViewAction { enum RoomChangePermissionsScreenViewAction {
/// Save the permissions. /// Save the permissions.
case save case save
/// Discard any changes and hide the screen.
case cancel
} }
extension RoomChangePermissionsScreenViewState { extension RoomChangePermissionsScreenViewState {

View File

@ -49,6 +49,8 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel
switch viewAction { switch viewAction {
case .save: case .save:
Task { await save() } Task { await save() }
case .cancel:
confirmDiscardChanges()
} }
} }
@ -83,6 +85,14 @@ class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModel
} }
} }
private func confirmDiscardChanges() {
state.bindings.alertInfo = AlertInfo(id: .discardChanges,
title: L10n.screenRoomChangeRoleUnsavedChangesTitle,
message: L10n.screenRoomChangeRoleUnsavedChangesDescription,
primaryButton: .init(title: L10n.actionSave) { Task { await self.save() } },
secondaryButton: .init(title: L10n.actionDiscard, role: .cancel) { self.actionsSubject.send(.complete) })
}
// MARK: Loading indicator // MARK: Loading indicator
private static let indicatorID = "SavingRoomPermissions" private static let indicatorID = "SavingRoomPermissions"

View File

@ -35,6 +35,7 @@ struct RoomChangePermissionsScreen: View {
.compoundList() .compoundList()
.navigationTitle(context.viewState.title) .navigationTitle(context.viewState.title)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(context.viewState.hasChanges)
.toolbar { toolbar } .toolbar { toolbar }
.alert(item: $context.alertInfo) .alert(item: $context.alertInfo)
} }
@ -47,6 +48,14 @@ struct RoomChangePermissionsScreen: View {
} }
.disabled(!context.viewState.hasChanges) .disabled(!context.viewState.hasChanges)
} }
if context.viewState.hasChanges {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
} }
} }

View File

@ -89,6 +89,8 @@ struct RoomChangeRolesScreenViewStateBindings {
enum RoomChangeRolesScreenAlertType { enum RoomChangeRolesScreenAlertType {
/// A warning that a particular promotion can't be undone. /// A warning that a particular promotion can't be undone.
case promotionWarning case promotionWarning
/// A confirmation that the user would like to discard any unsaved changes.
case discardChanges
/// The generic error message. /// The generic error message.
case error case error
} }
@ -100,4 +102,6 @@ enum RoomChangeRolesScreenViewAction {
case demoteMember(RoomMemberDetails) case demoteMember(RoomMemberDetails)
/// Save all the changes that the user has made. /// Save all the changes that the user has made.
case save case save
/// Discard any changes and hide the screen.
case cancel
} }

View File

@ -74,6 +74,8 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
} else { } else {
Task { await save() } Task { await save() }
} }
case .cancel:
confirmDiscardChanges()
} }
} }
@ -148,6 +150,14 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
} }
} }
private func confirmDiscardChanges() {
state.bindings.alertInfo = AlertInfo(id: .discardChanges,
title: L10n.screenRoomChangeRoleUnsavedChangesTitle,
message: L10n.screenRoomChangeRoleUnsavedChangesDescription,
primaryButton: .init(title: L10n.actionSave) { Task { await self.save() } },
secondaryButton: .init(title: L10n.actionDiscard, role: .cancel) { self.actionsSubject.send(.complete) })
}
// MARK: Loading indicator // MARK: Loading indicator
private static let indicatorID = "SavingRoomRoles" private static let indicatorID = "SavingRoomRoles"

View File

@ -61,15 +61,6 @@ struct RoomChangeRolesScreen: View {
} }
} }
private var noResultsContent: some View {
Text(L10n.commonNoResults)
.font(.compound.bodyLG)
.foregroundColor(.compound.textSecondary)
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
.accessibilityIdentifier(A11yIdentifiers.startChatScreen.searchNoResults)
}
@ViewBuilder @ViewBuilder
private var membersSection: some View { private var membersSection: some View {
if !context.viewState.visibleMembers.isEmpty { if !context.viewState.visibleMembers.isEmpty {
@ -123,6 +114,14 @@ struct RoomChangeRolesScreen: View {
} }
.disabled(!context.viewState.hasChanges) .disabled(!context.viewState.hasChanges)
} }
if context.viewState.hasChanges {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
} }
} }

View File

@ -66,7 +66,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
case .editRoles(let role): case .editRoles(let role):
actionsSubject.send(.editRoles(role)) actionsSubject.send(.editRoles(role))
case .editOwnUserRole: case .editOwnUserRole:
state.bindings.alertInfo = AlertInfo(id: .resetConfirmation, state.bindings.alertInfo = AlertInfo(id: .editOwnRole,
title: L10n.screenRoomRolesAndPermissionsChangeMyRole, title: L10n.screenRoomRolesAndPermissionsChangeMyRole,
message: L10n.screenRoomChangeRoleConfirmDemoteSelfDescription, message: L10n.screenRoomChangeRoleConfirmDemoteSelfDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },

View File

@ -18,7 +18,7 @@ import XCTest
extension XCUIApplication { extension XCUIApplication {
func login(currentTestCase: XCTestCase) { func login(currentTestCase: XCTestCase) {
let getStartedButton = buttons[A11yIdentifiers.onboardingScreen.signIn] let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn]
XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0))
getStartedButton.tap() getStartedButton.tap()
@ -128,7 +128,7 @@ extension XCUIApplication {
alertLogoutButton.tap() alertLogoutButton.tap()
// Check that we're back on the login screen // Check that we're back on the login screen
let getStartedButton = buttons[A11yIdentifiers.onboardingScreen.signIn] let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn]
XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0))
} }
} }

View File

@ -23,7 +23,7 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
let app = Application.launch(.authenticationFlow) let app = Application.launch(.authenticationFlow)
// Splash Screen: Tap get started button // Splash Screen: Tap get started button
app.buttons[A11yIdentifiers.onboardingScreen.signIn].tap() app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
// Server Confirmation: Tap continue button // Server Confirmation: Tap continue button
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
@ -45,7 +45,7 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
let app = Application.launch(.authenticationFlow) let app = Application.launch(.authenticationFlow)
// Splash Screen: Tap get started button // Splash Screen: Tap get started button
app.buttons[A11yIdentifiers.onboardingScreen.signIn].tap() app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
// Server Confirmation: Tap continue button // Server Confirmation: Tap continue button
app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap()
@ -69,7 +69,7 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase {
let app = Application.launch(.authenticationFlow) let app = Application.launch(.authenticationFlow)
// Splash Screen: Tap get started button // Splash Screen: Tap get started button
app.buttons[A11yIdentifiers.onboardingScreen.signIn].tap() app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap()
// Server Confirmation: Tap change server button // Server Confirmation: Tap change server button
app.buttons[A11yIdentifiers.serverConfirmationScreen.changeServer].tap() app.buttons[A11yIdentifiers.serverConfirmationScreen.changeServer].tap()

View File

@ -19,7 +19,7 @@ import XCTest
@MainActor @MainActor
class AuthenticationStartScreenUITests: XCTestCase { class AuthenticationStartScreenUITests: XCTestCase {
func testInitialStateComponents() async throws { func testInitialStateComponents() async throws {
let app = Application.launch(.onboarding) let app = Application.launch(.authenticationStartScreen)
try await app.assertScreenshot(.onboarding) try await app.assertScreenshot(.authenticationStartScreen)
} }
} }

Binary file not shown.