Invite people in an existing room (#915)

* Handle power levels

* Add “invite” button in RoomMembersListScreen

* Refactor DI in RoomDetailsScreenCoordinator

* Add navigation to “select people” coordinator

* Add InviteUsersScreenRoomContext

* Add invite user by id api

* Updare InviteUsersScreenRoomContext

* Support disabled state on FormButtonStyles

* Handle membership in UserProfileCell

* Fix conflicts

* Refactor RoomContext -> RoomType

* Refactor UserProfileCell

* Add invite action

* Setup navigation to user flow coordinator

* Add invite logic

* Fix test build errors

* Address comments

* Fix localisations

* Update required state

* Handle error

* Move invite logic in RoomDetailsScreenCoordinator

* Add navigation from room’s members list

* Fix UTs

* Fix missing display name in RoomMemberDetails

* Add user indicator

* Add feature flag

* Add RoomDetailsViewModel UTs

* Add room details ui tests

* Add InviteUsersViewModelTests UTs

* Add UI tests

* Update UI tests

* Empty commit
This commit is contained in:
Alfonso Grillo
2023-05-22 16:03:07 +02:00
committed by GitHub
parent 8ed8cf6f0c
commit d9e0f36aeb
58 changed files with 708 additions and 172 deletions
@@ -14,12 +14,14 @@
// limitations under the License.
//
import Combine
import SwiftUI
struct RoomDetailsScreenCoordinatorParameters {
let navigationStackCoordinator: NavigationStackCoordinator
let roomProxy: RoomProxyProtocol
let mediaProvider: MediaProviderProtocol
let userDiscoveryService: UserDiscoveryServiceProtocol
}
enum RoomDetailsScreenCoordinatorAction {
@@ -30,7 +32,11 @@ enum RoomDetailsScreenCoordinatorAction {
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomDetailsScreenCoordinatorParameters
private var viewModel: RoomDetailsScreenViewModelProtocol
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
private var cancellables: Set<AnyCancellable> = .init()
private let selectedUsers: CurrentValueSubject<[UserProfile], Never> = .init([])
private var navigationStackCoordinator: NavigationStackCoordinator {
parameters.navigationStackCoordinator
}
var callback: ((RoomDetailsScreenCoordinatorAction) -> Void)?
@@ -50,6 +56,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
switch action {
case .requestMemberDetailsPresentation(let members):
self.presentRoomMembersList(members)
case .requestInvitePeoplePresentation(let members):
self.presentInviteUsersScreen(members: members)
case .cancel:
self.callback?(.cancel)
case .leftRoom:
@@ -62,12 +70,96 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
AnyView(RoomDetailsScreen(context: viewModel.context))
}
// MARK: - Private
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) {
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
mediaProvider: parameters.mediaProvider,
members: members)
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
coordinator.callback = { [weak self] action in
switch action {
case .invite:
self?.presentInviteUsersScreen(members: members)
}
}
navigationStackCoordinator.push(coordinator)
}
private func presentInviteUsersScreen(members: [RoomMemberProxyProtocol]) {
let navigationStackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator)
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers),
roomType: .room(members: members, userIndicatorController: userIndicatorController),
mediaProvider: parameters.mediaProvider,
userDiscoveryService: parameters.userDiscoveryService)
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
coordinator.actions.sink { [weak self] result in
guard let self else { return }
switch result {
case .proceed:
break
case .invite(let users):
self.inviteUsers(users, in: parameters.roomProxy)
case .toggleUser(let user):
self.toggleUser(user)
}
}
.store(in: &cancellables)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
}
private func toggleUser(_ user: UserProfile) {
var selectedUsers = selectedUsers.value
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
selectedUsers.remove(at: index)
} else {
selectedUsers.append(user)
}
self.selectedUsers.send(selectedUsers)
}
private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) {
navigationStackCoordinator.setSheetCoordinator(nil)
Task {
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
for user in users {
group.addTask {
await room.invite(userID: user)
}
}
return await group.first { inviteResult in
inviteResult.isFailure
} ?? .success(())
}
guard case .failure = result else {
return
}
ServiceLocator.shared.userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.commonUnableToInviteTitle,
message: L10n.commonUnableToInviteMessage)
}
}
}
private extension Result {
var isFailure: Bool {
switch self {
case .success:
return false
case .failure:
return true
}
}
}
@@ -23,6 +23,7 @@ import UIKit
enum RoomDetailsScreenViewModelAction {
case requestMemberDetailsPresentation([RoomMemberProxyProtocol])
case requestInvitePeoplePresentation([RoomMemberProxyProtocol])
case leftRoom
case cancel
}
@@ -41,6 +42,7 @@ struct RoomDetailsScreenViewState: BindableState {
var members: [RoomMemberDetails] = []
var joinedMembersCount = 0
var isProcessingIgnoreRequest = false
var canInviteUsers = false
var isLoadingMembers: Bool {
members.isEmpty
@@ -124,6 +126,7 @@ struct LeaveRoomAlertItem: AlertItem {
enum RoomDetailsScreenViewAction {
case processTapPeople
case processTapInvite
case processTapLeave
case processTapIgnore
case processTapUnignore
@@ -56,6 +56,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
switch viewAction {
case .processTapPeople:
callback?(.requestMemberDetailsPresentation(members))
case .processTapInvite:
callback?(.requestInvitePeoplePresentation(members))
case .processTapLeave:
let joinedMembers = members.filter { $0.membership == .join }
guard joinedMembers.count > 1 else {
@@ -84,27 +86,29 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
guard let self else { return }
buildMembersDetailsTask = Task {
let (membersDetails, joinedMembersCount) = await self.buildMembersDetails(members: members)
let roomMembersDetails = await self.buildMembersDetails(members: members)
guard !Task.isCancelled else { return }
if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 {
self.dmRecipient = members.first(where: { !$0.isAccountOwner })
}
self.state.members = membersDetails
self.state.joinedMembersCount = joinedMembersCount
self.state.members = roomMembersDetails.members
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
self.members = members
}
}
.store(in: &cancellables)
}
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> (memberDetails: [RoomMemberDetails], joinedMembersCount: Int) {
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
await Task.detached {
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
var roomMembersDetails: [RoomMemberDetails] = []
var accountOwner: RoomMemberProxyProtocol?
var joinedMembersCount = 0
roomMembersDetails.reserveCapacity(members.count)
@@ -114,9 +118,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
if member.membership == .join {
joinedMembersCount += 1
}
if accountOwner == nil, member.isAccountOwner {
accountOwner = member
}
}
return (roomMembersDetails, joinedMembersCount)
return .init(members: roomMembersDetails, joinedMembersCount: joinedMembersCount, accountOwner: accountOwner)
}
.value
}
@@ -124,7 +132,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private static let leaveRoomLoadingID = "LeaveRoomLoading"
private func leaveRoom() async {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLoading, persistent: true))
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLeavingRoom, persistent: true))
let result = await roomProxy.leaveRoom()
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(Self.leaveRoomLoadingID)
switch result {
@@ -159,3 +167,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}
}
private struct RoomMembersDetails {
let members: [RoomMemberDetails]
let joinedMembersCount: Int
let accountOwner: RoomMemberProxyProtocol?
}
@@ -128,12 +128,22 @@ struct RoomDetailsScreen: View {
Label(L10n.commonPeople, systemImage: "person")
}
}
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
.foregroundColor(.element.primaryContent)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
.disabled(context.viewState.isLoadingMembers)
if context.viewState.canInviteUsers, ServiceLocator.shared.settings.inviteMorePeopleFlowEnabled {
Button {
context.send(viewAction: .processTapInvite)
} label: {
Label(L10n.screenRoomDetailsInvitePeopleTitle, systemImage: "person.badge.plus")
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
}
}
.formSectionStyle()
.listRowSeparatorTint(.element.quinaryContent)
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
.foregroundColor(.element.primaryContent)
.disabled(context.viewState.isLoadingMembers)
}
@ViewBuilder