Room's details edit screen (#956)

* Add RoomDetailsEditScreen template

* Add navigation to the edit screen

* Delete template code

* Start RoomDetailsEditScreen UI

* Manage power levels in RoomDetailsScreen

* Start RoomDetailsEditScreenViewModel logic

* Inject initial room state

* Add cancel action

* Expose set name/topic APIs in RoomProxy

* Refine RoomDetailsEditScreen UI

* Add save logic

* Add localisations

* Fix avatar image

* Update localisations

* Add “Add topic” button

* Add feature flag

* Add dismiss on save logic

* Reduce throttling

* Improve form logic

* Fix UT build errors

* Add media sheet

* Add media preprocessing

* Add LoadableEditableAvatarImage

* Add condition on delete image

* Add avatar save button logic

* Add remove avatar logic

* Cleanup

* Fix edit bug in DM

* Add upload avatar

* Add focus

* Add RoomDetailsViewModel UTs

* Fix button style

* Add UTs

* Add empty topic ui test

* Fix iPad sheet presentation

* Revert topic appearance in room’s details

* Address PR comments

* Add UI tests
This commit is contained in:
Alfonso Grillo
2023-05-26 15:47:12 +02:00
committed by GitHub
parent 524997438f
commit f2b7faa183
47 changed files with 1281 additions and 54 deletions
@@ -62,6 +62,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
self.callback?(.cancel)
case .leftRoom:
self.callback?(.leftRoom)
case .requestEditDetailsPresentation(let accountOwner):
self.presentRoomDetailsEditScreen(accountOwner: accountOwner)
}
}
}
@@ -116,6 +118,30 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
}
private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) {
let navigationStackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator)
let roomDetailsEditParameters = RoomDetailsEditScreenCoordinatorParameters(accountOwner: accountOwner,
mediaProvider: parameters.mediaProvider,
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: parameters.roomProxy,
userIndicatorController: userIndicatorController)
let roomDetailsEditCoordinator = RoomDetailsEditScreenCoordinator(parameters: roomDetailsEditParameters)
roomDetailsEditCoordinator.actions.sink { [weak self] action in
switch action {
case .dismiss:
self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
}
private func toggleUser(_ user: UserProfile) {
var selectedUsers = selectedUsers.value
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
@@ -26,6 +26,7 @@ enum RoomDetailsScreenViewModelAction {
case requestInvitePeoplePresentation([RoomMemberProxyProtocol])
case leftRoom
case cancel
case requestEditDetailsPresentation(RoomMemberProxyProtocol)
}
// MARK: View
@@ -43,10 +44,21 @@ struct RoomDetailsScreenViewState: BindableState {
var joinedMembersCount = 0
var isProcessingIgnoreRequest = false
var canInviteUsers = false
var canEditRoomName = false
var canEditRoomTopic = false
var canEditRoomAvatar = false
var isLoadingMembers: Bool {
members.isEmpty
}
var canEdit: Bool {
!isDirect && ServiceLocator.shared.settings.editRoomDetailsFlowEnabled && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
}
var hasTopicSection: Bool {
topic != nil || (canEdit && canEditRoomTopic)
}
var bindings: RoomDetailsScreenViewStateBindings
@@ -130,6 +142,8 @@ enum RoomDetailsScreenViewAction {
case processTapLeave
case processTapIgnore
case processTapUnignore
case processTapEdit
case processTapAddTopic
case confirmLeave
case ignoreConfirmed
case unignoreConfirmed
@@ -23,6 +23,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = []
private var dmRecipient: RoomMemberProxyProtocol?
private var accountOwner: RoomMemberProxyProtocol?
@CancellableTask
private var buildMembersDetailsTask: Task<Void, Never>?
@@ -52,6 +53,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// MARK: - Public
// swiftlint:disable:next cyclomatic_complexity
override func process(viewAction: RoomDetailsScreenViewAction) {
switch viewAction {
case .processTapPeople:
@@ -71,6 +73,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.bindings.ignoreUserRoomAlertItem = .init(action: .ignore)
case .processTapUnignore:
state.bindings.ignoreUserRoomAlertItem = .init(action: .unignore)
case .processTapEdit, .processTapAddTopic:
guard let accountOwner else {
MXLog.error("Missing account owner when presenting the room's edit details screen")
return
}
callback?(.requestEditDetailsPresentation(accountOwner))
case .ignoreConfirmed:
Task { await ignore() }
case .unignoreConfirmed:
@@ -105,13 +113,17 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
self.state.canEditRoomName = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomName) ?? false
self.state.canEditRoomTopic = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomTopic) ?? false
self.state.canEditRoomAvatar = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
self.members = members
self.accountOwner = roomMembersDetails.accountOwner
}
}
.store(in: &cancellables)
roomProxy.updatesPublisher
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
guard let self else { return }
self.state.title = self.roomProxy.roomTitle
@@ -50,6 +50,15 @@ struct RoomDetailsScreen: View {
.alert(item: $context.ignoreUserRoomAlertItem,
actions: blockUserAlertActions,
message: blockUserAlertMessage)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if context.viewState.canEdit {
Button(L10n.actionEdit) {
context.send(viewAction: .processTapEdit)
}
}
}
}
}
// MARK: - Private
@@ -98,11 +107,23 @@ struct RoomDetailsScreen: View {
@ViewBuilder
private var topicSection: some View {
if let topic = context.viewState.topic {
if context.viewState.hasTopicSection {
Section {
Text(topic)
.foregroundColor(.element.secondaryContent)
.font(.compound.bodySM)
if let topic = context.viewState.topic, !topic.isEmpty {
Text(topic)
.foregroundColor(.element.secondaryContent)
.font(.compound.bodySM)
.lineLimit(3)
} else {
Button {
context.send(viewAction: .processTapAddTopic)
} label: {
Text(L10n.screenRoomDetailsAddTopicTitle)
.foregroundColor(.element.primaryContent)
.font(.compound.bodyLG)
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.addTopic)
}
} header: {
Text(L10n.commonTopic)
.formSectionHeader()
@@ -142,7 +163,6 @@ struct RoomDetailsScreen: View {
.listRowSeparatorTint(.element.quinaryContent)
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
.foregroundColor(.element.primaryContent)
.disabled(context.viewState.isLoadingMembers)
}