Leave Room (#699)

* created the row in the view and the alert, and added the new function to the RoomProxy

* fixed an issue with the alert function

* handling the navigation

* fixed a bug with the detail coordinators being dismissed incorrectly when inside a stack

* implementation completed

* replaced UI screenshots

* added a test for the fixed bug of the coordinators

* trying to increase the wait time for the expectation

* improved the test

* improved the buttons UI

* uploading artifacts for unit tests

* added result bundle true

* improved the tests

* added a new test

* pr suggestions

* updating mock

* PR suggestions

* improved tests

* fixed UI tests

* pr should be ready now

* removed testing code

* reduced complexity

* fixed test

* added a an assert to the new test case

* more tests and messages cases

* pr comments addressed

* completed
This commit is contained in:
Mauro
2023-03-17 14:57:08 +01:00
committed by GitHub
parent 7544619a55
commit 61d42a24ba
35 changed files with 719 additions and 155 deletions
@@ -76,7 +76,7 @@ struct ReportContentScreen: View {
// MARK: - Previews
struct ReportContent_Previews: PreviewProvider {
static let viewModel = ReportContentViewModel(itemID: "", roomProxy: MockRoomProxy(displayName: nil))
static let viewModel = ReportContentViewModel(itemID: "", roomProxy: RoomProxyMock(with: .init(displayName: nil)))
static var previews: some View {
ReportContentScreen(context: viewModel.context)
@@ -24,6 +24,7 @@ struct RoomDetailsCoordinatorParameters {
enum RoomDetailsCoordinatorAction {
case cancel
case leftRoom
}
final class RoomDetailsCoordinator: CoordinatorProtocol {
@@ -51,6 +52,8 @@ final class RoomDetailsCoordinator: CoordinatorProtocol {
self.presentRoomMemberDetails(members)
case .cancel:
self.callback?(.cancel)
case .leftRoom:
self.callback?(.leftRoom)
}
}
}
@@ -23,6 +23,7 @@ import UIKit
enum RoomDetailsViewModelAction {
case requestMemberDetailsPresentation([RoomMemberProxy])
case leftRoom
case cancel
}
@@ -49,15 +50,34 @@ struct RoomDetailsViewState: BindableState {
struct RoomDetailsViewStateBindings {
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomDetailsErrorType>?
var leaveRoomAlertItem: LeaveRoomAlertItem?
}
enum RoomDetailsErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
struct LeaveRoomAlertItem: AlertItem {
enum RoomState {
case empty
case `public`
case `private`
}
let state: RoomState
let title = ElementL10n.roomProfileSectionMoreLeave
let confirmationTitle = ElementL10n.actionLeave
let cancelTitle = ElementL10n.actionCancel
var subtitle: String {
switch state {
case .empty: return ElementL10n.roomDetailsLeaveEmptyRoomAlertSubtitle
case .private: return ElementL10n.roomDetailsLeavePrivateRoomAlertSubtitle
case .public: return ElementL10n.roomDetailsLeaveRoomAlertSubtitle
}
}
}
enum RoomDetailsViewAction {
case processTapPeople
case processTapLeave
case confirmLeave
case copyRoomLink
}
@@ -72,3 +92,10 @@ struct RoomDetailsMember: Identifiable, Equatable {
avatarURL = proxy.avatarURL
}
}
enum RoomDetailsErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
/// Leaving room has failed..
case unknown
}
@@ -19,6 +19,7 @@ import SwiftUI
typealias RoomDetailsViewModelType = StateStoreViewModel<RoomDetailsViewState, RoomDetailsViewAction>
class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtocol {
private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxy] = [] {
didSet {
state.members = members.map { RoomDetailsMember(withProxy: $0) }
@@ -29,6 +30,7 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
init(roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol) {
self.roomProxy = roomProxy
super.init(initialViewState: .init(roomId: roomProxy.id,
canonicalAlias: roomProxy.canonicalAlias,
isEncrypted: roomProxy.isEncrypted,
@@ -60,10 +62,20 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
callback?(.requestMemberDetailsPresentation(members))
case .copyRoomLink:
copyRoomLink()
case .processTapLeave:
guard members.count > 1 else {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: .empty)
return
}
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: roomProxy.isPublic ? .public : .private)
case .confirmLeave:
await leaveRoom()
}
}
// MARK: - Private
private static let leaveRoomLoadingID = "LeaveRoomLoading"
private func copyRoomLink() {
if let roomLink = state.permalink {
@@ -73,4 +85,16 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: ElementL10n.unknownError))
}
}
private func leaveRoom() async {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: ElementL10n.loading, persistent: true))
let result = await roomProxy.leaveRoom()
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
switch result {
case .failure:
state.bindings.alertInfo = AlertInfo(id: .unknown)
case .success:
callback?(.leftRoom)
}
}
}
@@ -30,10 +30,15 @@ struct RoomDetailsScreen: View {
if context.viewState.isEncrypted {
securitySection
}
leaveRoomSection
}
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
.alert(item: $context.alertInfo) { $0.alert }
.alert(item: $context.leaveRoomAlertItem,
actions: leaveRoomAlertActions,
message: leaveRoomAlertMessage)
}
// MARK: - Private
@@ -147,6 +152,30 @@ struct RoomDetailsScreen: View {
}
.formSectionStyle()
}
private var leaveRoomSection: some View {
Section {
Button(role: .destructive) {
context.send(viewAction: .processTapLeave)
} label: {
Label(ElementL10n.roomProfileSectionMoreLeave, systemImage: "door.right.hand.open")
}
.buttonStyle(FormButtonStyle(accessory: nil))
}
.formSectionStyle()
}
@ViewBuilder
private func leaveRoomAlertActions(_ item: LeaveRoomAlertItem) -> some View {
Button(item.cancelTitle, role: .cancel) { }
Button(item.confirmationTitle, role: .destructive) {
context.send(viewAction: .confirmLeave)
}
}
private func leaveRoomAlertMessage(_ item: LeaveRoomAlertItem) -> some View {
Text(item.subtitle)
}
}
// MARK: - Previews
@@ -158,12 +187,12 @@ struct RoomDetails_Previews: PreviewProvider {
.mockBob,
.mockCharlie
]
let roomProxy = MockRoomProxy(displayName: "Room A",
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
isDirect: false,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
members: members)
let roomProxy = RoomProxyMock(with: .init(displayName: "Room A",
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
isDirect: false,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
members: members))
return RoomDetailsViewModel(roomProxy: roomProxy,
mediaProvider: MockMediaProvider())
@@ -24,6 +24,10 @@ struct RoomScreenCoordinatorParameters {
let emojiProvider: EmojiProviderProtocol
}
enum RoomScreenCoordinatorAction {
case leftRoom
}
final class RoomScreenCoordinator: CoordinatorProtocol {
private var parameters: RoomScreenCoordinatorParameters
@@ -31,6 +35,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
private var navigationStackCoordinator: NavigationStackCoordinator {
parameters.navigationStackCoordinator
}
var callback: ((RoomScreenCoordinatorAction) -> Void)?
init(parameters: RoomScreenCoordinatorParameters) {
self.parameters = parameters
@@ -111,8 +117,13 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider)
let coordinator = RoomDetailsCoordinator(parameters: params)
coordinator.callback = { [weak self] _ in
self?.navigationStackCoordinator.pop()
coordinator.callback = { [weak self] action in
switch action {
case .cancel:
self?.navigationStackCoordinator.pop()
case .leftRoom:
self?.callback?(.leftRoom)
}
}
navigationStackCoordinator.push(coordinator)