Flescio/create room action (#925)

* fix invites users screen

* add create room action

* add a task runner with timeout, add fetch room summary after creation

* rename Runner, add Unit Test, separate UserProfile form client proxy

* change to all room summary publisher
This commit is contained in:
Flescio
2023-05-22 17:27:39 +02:00
committed by GitHub
parent 16728ca438
commit beb61d109f
25 changed files with 324 additions and 68 deletions
@@ -19,12 +19,13 @@ import SwiftUI
struct CreateRoomCoordinatorParameters {
let userSession: UserSessionProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol?
let createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>
let selectedUsers: CurrentValuePublisher<[UserProfile], Never>
}
enum CreateRoomCoordinatorAction {
case createRoom
case openRoom(withIdentifier: String)
case deselectUser(UserProfile)
case updateDetails(CreateRoomFlowParameters)
}
@@ -42,6 +43,7 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
init(parameters: CreateRoomCoordinatorParameters) {
self.parameters = parameters
viewModel = CreateRoomViewModel(userSession: parameters.userSession,
userIndicatorController: parameters.userIndicatorController,
createRoomParameters: parameters.createRoomParameters,
selectedUsers: parameters.selectedUsers)
}
@@ -52,8 +54,8 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
switch action {
case .deselectUser(let user):
self.actionsSubject.send(.deselectUser(user))
case .createRoom:
self.actionsSubject.send(.createRoom)
case .openRoom(let identifier):
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
case .updateDetails(let details):
self.actionsSubject.send(.updateDetails(details))
}
@@ -16,8 +16,13 @@
import Foundation
enum CreateRoomScreenErrorType: Error {
case failedCreatingRoom
case unknown
}
enum CreateRoomViewModelAction {
case createRoom
case openRoom(withIdentifier: String)
case deselectUser(UserProfile)
case updateDetails(CreateRoomFlowParameters)
}
@@ -35,6 +40,9 @@ struct CreateRoomViewStateBindings {
var roomName: String
var roomTopic: String
var isRoomPrivate: Bool
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<CreateRoomScreenErrorType>?
}
enum CreateRoomViewAction {
@@ -20,17 +20,22 @@ import SwiftUI
typealias CreateRoomViewModelType = StateStoreViewModel<CreateRoomViewState, CreateRoomViewAction>
class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol {
private let userSession: UserSessionProtocol
private var actionsSubject: PassthroughSubject<CreateRoomViewModelAction, Never> = .init()
private var createRoomParameters: CreateRoomFlowParameters
private weak var userIndicatorController: UserIndicatorControllerProtocol?
var actions: AnyPublisher<CreateRoomViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(userSession: UserSessionProtocol,
userIndicatorController: UserIndicatorControllerProtocol?,
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
selectedUsers: CurrentValuePublisher<[UserProfile], Never>) {
let parameters = createRoomParameters.value
self.userSession = userSession
self.userIndicatorController = userIndicatorController
self.createRoomParameters = parameters
let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate)
@@ -50,7 +55,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
override func process(viewAction: CreateRoomViewAction) {
switch viewAction {
case .createRoom:
actionsSubject.send(.createRoom)
Task {
await createRoom()
}
case .deselectUser(let user):
actionsSubject.send(.deselectUser(user))
}
@@ -71,4 +78,49 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
}
.store(in: &cancellables)
}
private func displayError(_ type: ClientProxyError) {
switch type {
case .failedCreatingRoom:
state.bindings.alertInfo = AlertInfo(id: .failedCreatingRoom,
title: L10n.commonError,
message: L10n.screenStartChatErrorStartingChat)
case .failedSearchingUsers:
state.bindings.alertInfo = AlertInfo(id: .unknown)
default:
break
}
}
private var clientProxy: ClientProxyProtocol {
userSession.clientProxy
}
private func createRoom() async {
defer {
hideLoadingIndicator()
}
showLoadingIndicator()
switch await clientProxy.createRoom(with: createRoomParameters, userIDs: state.selectedUsers.map(\.userID)) {
case .success(let roomId):
actionsSubject.send(.openRoom(withIdentifier: roomId))
case .failure(let failure):
displayError(failure)
}
}
// MARK: Loading indicator
private static let loadingIndicatorIdentifier = "CreateRoomLoading"
private func showLoadingIndicator() {
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
}
private func hideLoadingIndicator() {
userIndicatorController?.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
}
@@ -166,14 +166,14 @@ struct CreateRoom_Previews: PreviewProvider {
let parameters = CreateRoomFlowParameters()
let selectedUsers: [UserProfile] = [.mockAlice, .mockBob, .mockCharlie]
return CreateRoomViewModel(userSession: userSession, createRoomParameters: .init(parameters), selectedUsers: .init(selectedUsers))
return CreateRoomViewModel(userSession: userSession, userIndicatorController: nil, createRoomParameters: .init(parameters), selectedUsers: .init(selectedUsers))
}()
static let emtpyViewModel = {
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
mediaProvider: MockMediaProvider())
let parameters = CreateRoomFlowParameters()
return CreateRoomViewModel(userSession: userSession, createRoomParameters: .init(parameters), selectedUsers: .init([]))
return CreateRoomViewModel(userSession: userSession, userIndicatorController: nil, createRoomParameters: .init(parameters), selectedUsers: .init([]))
}()
static var previews: some View {
@@ -21,32 +21,35 @@ struct InviteUsersScreen: View {
@ObservedObject var context: InviteUsersScreenViewModel.Context
var body: some View {
VStack {
if !context.viewState.selectedUsers.isEmpty {
selectedUsersSection
mainContent
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
.navigationTitle(L10n.screenCreateRoomActionInvitePeople)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
nextButton
}
}
searchContent
}
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
.navigationTitle(L10n.screenCreateRoomActionInvitePeople)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
nextButton
}
}
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: L10n.commonSearchForSomeone)
.compoundSearchField()
.alert(item: $context.alertInfo) { $0.alert }
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: L10n.commonSearchForSomeone)
.compoundSearchField()
.alert(item: $context.alertInfo) { $0.alert }
.background(ViewFrameReader(frame: $frame))
}
// MARK: - Private
/// The content shown in the form when a search query has been entered.
@ViewBuilder
private var searchContent: some View {
private var mainContent: some View {
Form {
if !context.viewState.selectedUsers.isEmpty {
// this is a fix for having the carousel not clipped, and inside the form, so when the search is dismissed, it wont break the design
Section {
EmptyView()
} header: {
selectedUsersSection
.textCase(.none)
}
}
if context.viewState.hasEmptySearchResults {
noResultsContent
} else {
@@ -84,7 +87,9 @@ struct InviteUsersScreen: View {
.formSectionStyle()
}
@State private var frame: CGRect = .zero
@ScaledMetric private var cellWidth: CGFloat = 64
private var selectedUsersSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollView in
@@ -105,6 +110,7 @@ struct InviteUsersScreen: View {
.padding(.horizontal, 18)
}
}
.frame(width: frame.width)
}
private var nextButton: some View {
@@ -107,6 +107,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
private func openCreateRoomScreen() {
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
userIndicatorController: parameters.userIndicatorController,
createRoomParameters: createRoomParametersPublisher,
selectedUsers: selectedUsersPublisher)
let coordinator = CreateRoomCoordinator(parameters: createParameters)
@@ -116,8 +117,8 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
self?.toggleUser(user)
case .updateDetails(let details):
self?.createRoomParameters.send(details)
case .createRoom:
break
case .openRoom(let identifier):
self?.actionsSubject.send(.openRoom(withIdentifier: identifier))
}
}
.store(in: &cancellables)
@@ -127,10 +127,11 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
}
private func createDirectRoom(with user: UserProfile) async {
defer {
hideLoadingIndicator()
}
showLoadingIndicator()
let result = await clientProxy.createDirectRoom(with: user.userID)
hideLoadingIndicator()
switch result {
switch await clientProxy.createDirectRoom(with: user.userID, expectedRoomName: user.displayName) {
case .success(let roomId):
actionsSubject.send(.openRoom(withIdentifier: roomId))
case .failure(let failure):