Flescio/add room image (#961)
* image media upload for room creation * fix remove duplicate logic * use Async Image, add Focusable Fields in create room * remove alert message which doesn't have a copy * add remove duplicates on crete room image * add button style for preventing undesired touches * add changelog, add error case for file too large * Fix iPad sheet presentation * add error media preprocess * dismissing focus on image picker
This commit is contained in:
@@ -28,6 +28,8 @@ enum CreateRoomCoordinatorAction {
|
||||
case openRoom(withIdentifier: String)
|
||||
case deselectUser(UserProfileProxy)
|
||||
case updateDetails(CreateRoomFlowParameters)
|
||||
case displayMediaPickerWithSource(MediaPickerScreenSource)
|
||||
case removeImage
|
||||
}
|
||||
|
||||
final class CreateRoomCoordinator: CoordinatorProtocol {
|
||||
@@ -53,11 +55,17 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .deselectUser(let user):
|
||||
self.actionsSubject.send(.deselectUser(user))
|
||||
actionsSubject.send(.deselectUser(user))
|
||||
case .openRoom(let identifier):
|
||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
case .updateDetails(let details):
|
||||
self.actionsSubject.send(.updateDetails(details))
|
||||
actionsSubject.send(.updateDetails(details))
|
||||
case .displayCameraPicker:
|
||||
actionsSubject.send(.displayMediaPickerWithSource(.camera))
|
||||
case .displayMediaPicker:
|
||||
actionsSubject.send(.displayMediaPickerWithSource(.photoLibrary))
|
||||
case .removeImage:
|
||||
actionsSubject.send(.removeImage)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -18,6 +18,9 @@ import Foundation
|
||||
|
||||
enum CreateRoomScreenErrorType: Error {
|
||||
case failedCreatingRoom
|
||||
case failedUploadingMedia
|
||||
case fileTooLarge
|
||||
case mediaFileError
|
||||
case unknown
|
||||
}
|
||||
|
||||
@@ -25,12 +28,15 @@ enum CreateRoomViewModelAction {
|
||||
case openRoom(withIdentifier: String)
|
||||
case deselectUser(UserProfileProxy)
|
||||
case updateDetails(CreateRoomFlowParameters)
|
||||
case displayMediaPicker
|
||||
case displayCameraPicker
|
||||
case removeImage
|
||||
}
|
||||
|
||||
struct CreateRoomViewState: BindableState {
|
||||
var selectedUsers: [UserProfileProxy]
|
||||
var bindings: CreateRoomViewStateBindings
|
||||
|
||||
var avatarURL: URL?
|
||||
var canCreateRoom: Bool {
|
||||
!bindings.roomName.isEmpty
|
||||
}
|
||||
@@ -40,6 +46,7 @@ struct CreateRoomViewStateBindings {
|
||||
var roomName: String
|
||||
var roomTopic: String
|
||||
var isRoomPrivate: Bool
|
||||
var showAttachmentConfirmationDialog = false
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<CreateRoomScreenErrorType>?
|
||||
@@ -48,4 +55,7 @@ struct CreateRoomViewStateBindings {
|
||||
enum CreateRoomViewAction {
|
||||
case createRoom
|
||||
case deselectUser(UserProfileProxy)
|
||||
case displayCameraPicker
|
||||
case displayMediaPicker
|
||||
case removeImage
|
||||
}
|
||||
|
||||
@@ -41,6 +41,22 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
|
||||
super.init(initialViewState: CreateRoomViewState(selectedUsers: selectedUsers.value, bindings: bindings), imageProvider: userSession.mediaProvider)
|
||||
|
||||
createRoomParameters
|
||||
.map(\.avatarImageMedia)
|
||||
.removeDuplicates { $0?.url == $1?.url }
|
||||
.sink { [weak self] mediaInfo in
|
||||
self?.createRoomParameters.avatarImageMedia = mediaInfo
|
||||
switch mediaInfo {
|
||||
case .image(_, let thumbnailURL, _):
|
||||
self?.state.avatarURL = thumbnailURL
|
||||
case nil:
|
||||
self?.state.avatarURL = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedUsers
|
||||
.sink { [weak self] users in
|
||||
self?.state.selectedUsers = users
|
||||
@@ -60,6 +76,12 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
}
|
||||
case .deselectUser(let user):
|
||||
actionsSubject.send(.deselectUser(user))
|
||||
case .displayCameraPicker:
|
||||
actionsSubject.send(.displayCameraPicker)
|
||||
case .displayMediaPicker:
|
||||
actionsSubject.send(.displayMediaPicker)
|
||||
case .removeImage:
|
||||
actionsSubject.send(.removeImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +91,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
context.$viewState
|
||||
.map(\.bindings)
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||
.removeDuplicates { old, new in
|
||||
old.roomName == new.roomName && old.roomTopic == new.roomTopic && old.isRoomPrivate == new.isRoomPrivate
|
||||
}
|
||||
.sink { [weak self] bindings in
|
||||
guard let self else { return }
|
||||
createRoomParameters.name = bindings.roomName
|
||||
@@ -87,6 +112,16 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
message: L10n.screenStartChatErrorStartingChat)
|
||||
case .failedSearchingUsers:
|
||||
state.bindings.alertInfo = AlertInfo(id: .unknown)
|
||||
case .failedUploadingMedia(let matrixError):
|
||||
switch matrixError {
|
||||
case .fileTooLarge:
|
||||
// waiting for proper copy
|
||||
state.bindings.alertInfo = AlertInfo(id: .fileTooLarge)
|
||||
default:
|
||||
state.bindings.alertInfo = AlertInfo(id: .failedUploadingMedia)
|
||||
}
|
||||
case .mediaFileError:
|
||||
state.bindings.alertInfo = AlertInfo(id: .mediaFileError)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -101,7 +136,25 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
showLoadingIndicator()
|
||||
switch await clientProxy.createRoom(with: createRoomParameters, userIDs: state.selectedUsers.map(\.userID)) {
|
||||
|
||||
let avatarURL: URL?
|
||||
if let media = createRoomParameters.avatarImageMedia {
|
||||
switch await clientProxy.uploadMedia(media) {
|
||||
case .success(let url):
|
||||
avatarURL = URL(string: url)
|
||||
case .failure(let error):
|
||||
displayError(error)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
avatarURL = nil
|
||||
}
|
||||
|
||||
switch await clientProxy.createRoom(name: createRoomParameters.name,
|
||||
topic: createRoomParameters.topic,
|
||||
isRoomPrivate: createRoomParameters.isRoomPrivate,
|
||||
userIDs: state.selectedUsers.map(\.userID),
|
||||
avatarURL: avatarURL) {
|
||||
case .success(let roomId):
|
||||
actionsSubject.send(.openRoom(withIdentifier: roomId))
|
||||
case .failure(let failure):
|
||||
|
||||
@@ -18,7 +18,13 @@ import SwiftUI
|
||||
|
||||
struct CreateRoomScreen: View {
|
||||
@ObservedObject var context: CreateRoomViewModel.Context
|
||||
|
||||
@FocusState private var focus: Focus?
|
||||
|
||||
private enum Focus {
|
||||
case name
|
||||
case topic
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
@@ -32,6 +38,7 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
}
|
||||
.background(ViewFrameReader(frame: $frame))
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
}
|
||||
|
||||
/// The main content of the view to be shown in a scroll view.
|
||||
@@ -49,11 +56,38 @@ struct CreateRoomScreen: View {
|
||||
private var roomSection: some View {
|
||||
Section {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
Image(systemName: "camera")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.frame(width: roomIconSize, height: roomIconSize)
|
||||
.background(Color.element.quinaryContent)
|
||||
.clipShape(Circle())
|
||||
Button {
|
||||
focus = nil
|
||||
context.showAttachmentConfirmationDialog = true
|
||||
} label: {
|
||||
if let url = context.viewState.avatarURL {
|
||||
AsyncImage(url: url) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
.frame(width: roomIconSize, height: roomIconSize)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
cameraImage
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.confirmationDialog("", isPresented: $context.showAttachmentConfirmationDialog) {
|
||||
Button(L10n.actionTakePhoto) {
|
||||
context.send(viewAction: .displayCameraPicker)
|
||||
}
|
||||
Button(L10n.actionChoosePhoto) {
|
||||
context.send(viewAction: .displayMediaPicker)
|
||||
}
|
||||
if context.viewState.avatarURL != nil {
|
||||
Button(L10n.actionRemove, role: .destructive) {
|
||||
context.send(viewAction: .removeImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(L10n.screenCreateRoomRoomNameLabel.uppercased())
|
||||
.font(.compound.bodyXS)
|
||||
@@ -63,6 +97,7 @@ struct CreateRoomScreen: View {
|
||||
text: $context.roomName,
|
||||
prompt: Text(L10n.screenCreateRoomRoomNamePlaceholder),
|
||||
axis: .horizontal)
|
||||
.focused($focus, equals: .name)
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomName)
|
||||
.padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16))
|
||||
.background(Color.element.formRowBackground)
|
||||
@@ -75,12 +110,21 @@ struct CreateRoomScreen: View {
|
||||
.formSectionStyle()
|
||||
}
|
||||
|
||||
private var cameraImage: some View {
|
||||
Image(systemName: "camera")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.frame(width: roomIconSize, height: roomIconSize)
|
||||
.background(Color.element.quinaryContent)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private var topicSection: some View {
|
||||
Section {
|
||||
TextField(L10n.screenCreateRoomTopicLabel,
|
||||
text: $context.roomTopic,
|
||||
prompt: Text(L10n.screenCreateRoomTopicPlaceholder),
|
||||
axis: .vertical)
|
||||
.focused($focus, equals: .topic)
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomTopic)
|
||||
.lineLimit(3, reservesSpace: false)
|
||||
} header: {
|
||||
@@ -150,7 +194,10 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
|
||||
private var createButton: some View {
|
||||
Button { context.send(viewAction: .createRoom) } label: {
|
||||
Button {
|
||||
focus = nil
|
||||
context.send(viewAction: .createRoom)
|
||||
} label: {
|
||||
Text(L10n.actionCreate)
|
||||
}
|
||||
.disabled(!context.viewState.canCreateRoom)
|
||||
|
||||
@@ -20,7 +20,7 @@ import SwiftUI
|
||||
struct StartChatScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||
let navigationStackCoordinator: NavigationStackCoordinator?
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let userDiscoveryService: UserDiscoveryServiceProtocol
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
selectedUsers.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator {
|
||||
parameters.navigationStackCoordinator
|
||||
}
|
||||
|
||||
private var userIndicatorController: UserIndicatorControllerProtocol? {
|
||||
parameters.userIndicatorController
|
||||
}
|
||||
|
||||
var actions: AnyPublisher<StartChatScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
@@ -60,12 +68,12 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .close:
|
||||
self.actionsSubject.send(.close)
|
||||
actionsSubject.send(.close)
|
||||
case .createRoom:
|
||||
// before creating a room we select the users we would like to invite in that room
|
||||
self.presentInviteUsersScreen()
|
||||
presentInviteUsersScreen()
|
||||
case .openRoom(let identifier):
|
||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -99,7 +107,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator?.push(coordinator) { [weak self] in
|
||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||
self?.createRoomParameters.send(.init())
|
||||
self?.selectedUsers.send([])
|
||||
}
|
||||
@@ -107,27 +115,71 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
private func openCreateRoomScreen() {
|
||||
let createParameters = CreateRoomCoordinatorParameters(userSession: parameters.userSession,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
userIndicatorController: userIndicatorController,
|
||||
createRoomParameters: createRoomParametersPublisher,
|
||||
selectedUsers: selectedUsersPublisher)
|
||||
let coordinator = CreateRoomCoordinator(parameters: createParameters)
|
||||
coordinator.actions.sink { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .deselectUser(let user):
|
||||
self?.toggleUser(user)
|
||||
self.toggleUser(user)
|
||||
case .updateDetails(let details):
|
||||
self?.createRoomParameters.send(details)
|
||||
self.createRoomParameters.send(details)
|
||||
case .openRoom(let identifier):
|
||||
self?.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
self.actionsSubject.send(.openRoom(withIdentifier: identifier))
|
||||
case .displayMediaPickerWithSource(let source):
|
||||
self.displayMediaPickerWithSource(source)
|
||||
case .removeImage:
|
||||
var parameters = self.createRoomParameters.value
|
||||
parameters.avatarImageMedia = nil
|
||||
self.createRoomParameters.send(parameters)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
parameters.navigationStackCoordinator?.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
let mediaUploadingPreprocessor = MediaUploadingPreprocessor()
|
||||
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let userIndicatorController = UserIndicatorController(rootCoordinator: stackCoordinator)
|
||||
|
||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, source: source) { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .cancel:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
case .selectMediaAtURL(let url):
|
||||
processAvatar(from: url)
|
||||
}
|
||||
}
|
||||
|
||||
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
||||
}
|
||||
|
||||
private func processAvatar(from url: URL) {
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
showLoadingIndicator()
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
let media = try await mediaUploadingPreprocessor.processMedia(at: url).get()
|
||||
var parameters = createRoomParameters.value
|
||||
parameters.avatarImageMedia = media
|
||||
createRoomParameters.send(parameters)
|
||||
} catch {
|
||||
userIndicatorController?.alertInfo = AlertInfo(id: .init(), title: L10n.commonError, message: L10n.errorUnknown)
|
||||
}
|
||||
hideLoadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleUser(_ user: UserProfileProxy) {
|
||||
var selectedUsers = selectedUsers.value
|
||||
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
|
||||
@@ -137,4 +189,19 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
self.selectedUsers.send(selectedUsers)
|
||||
}
|
||||
|
||||
// MARK: Loading indicator
|
||||
|
||||
private static let loadingIndicatorIdentifier = "StartChatCoordinatorLoading"
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
userIndicatorController?.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,19 @@ enum MatrixErrorCode: String, CaseIterable {
|
||||
case unknown = "M_UNKNOWN"
|
||||
case userDeactivated = "M_USER_DEACTIVATED"
|
||||
case forbidden = "M_FORBIDDEN"
|
||||
case fileTooLarge = "M_TOO_LARGE"
|
||||
}
|
||||
|
||||
extension ClientError {
|
||||
var code: MatrixErrorCode {
|
||||
guard case let .Generic(message) = self else { return .unknown }
|
||||
|
||||
guard let first = MatrixErrorCode.allCases.first(where: { message.contains($0.rawValue) }) else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
return first
|
||||
}
|
||||
}
|
||||
|
||||
extension AuthenticationError {
|
||||
|
||||
@@ -197,17 +197,17 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return await waitForRoomSummary(with: result, name: expectedRoomName)
|
||||
}
|
||||
|
||||
func createRoom(with parameters: CreateRoomFlowParameters, userIDs: [String]) async -> Result<String, ClientProxyError> {
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
|
||||
let result: Result<String, ClientProxyError> = await Task.dispatch(on: clientQueue) {
|
||||
do {
|
||||
let parameters = CreateRoomParameters(name: parameters.name,
|
||||
topic: parameters.topic,
|
||||
isEncrypted: parameters.isRoomPrivate,
|
||||
let parameters = CreateRoomParameters(name: name,
|
||||
topic: topic,
|
||||
isEncrypted: isRoomPrivate,
|
||||
isDirect: false,
|
||||
visibility: parameters.isRoomPrivate ? .private : .public,
|
||||
preset: parameters.isRoomPrivate ? .privateChat : .publicChat,
|
||||
visibility: isRoomPrivate ? .private : .public,
|
||||
preset: isRoomPrivate ? .privateChat : .publicChat,
|
||||
invite: userIDs,
|
||||
avatar: nil)
|
||||
avatar: avatarURL?.absoluteString)
|
||||
let roomId = try self.client.createRoom(request: parameters)
|
||||
return .success(roomId)
|
||||
} catch {
|
||||
@@ -215,7 +215,22 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
return await waitForRoomSummary(with: result, name: parameters.name)
|
||||
return await waitForRoomSummary(with: result, name: name)
|
||||
}
|
||||
|
||||
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError> {
|
||||
guard let mimeType = media.mimeType else { return .failure(ClientProxyError.mediaFileError) }
|
||||
return await Task.dispatch(on: clientQueue) {
|
||||
do {
|
||||
let data = try Data(contentsOf: media.url)
|
||||
let matrixUrl = try self.client.uploadMedia(mimeType: mimeType, data: [UInt8](data))
|
||||
return .success(matrixUrl)
|
||||
} catch let error as ClientError {
|
||||
return .failure(ClientProxyError.failedUploadingMedia(error.code))
|
||||
} catch {
|
||||
return .failure(ClientProxyError.mediaFileError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Await the room to be available in the room summary list
|
||||
|
||||
@@ -41,6 +41,8 @@ enum ClientProxyError: Error {
|
||||
case failedSettingAccountData
|
||||
case failedRetrievingSessionVerificationController
|
||||
case failedLoadingMedia
|
||||
case mediaFileError
|
||||
case failedUploadingMedia(MatrixErrorCode)
|
||||
case failedSearchingUsers
|
||||
case failedGettingUserProfile
|
||||
}
|
||||
@@ -92,7 +94,9 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
||||
|
||||
func createRoom(with parameters: CreateRoomFlowParameters, userIDs: [String]) async -> Result<String, ClientProxyError>
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError>
|
||||
|
||||
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError>
|
||||
|
||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
||||
|
||||
|
||||
@@ -57,10 +57,14 @@ class MockClientProxy: ClientProxyProtocol {
|
||||
.failure(.failedCreatingRoom)
|
||||
}
|
||||
|
||||
func createRoom(with parameters: CreateRoomFlowParameters, userIDs: [String]) async -> Result<String, ClientProxyError> {
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
|
||||
.failure(.failedCreatingRoom)
|
||||
}
|
||||
|
||||
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError> {
|
||||
.failure(.failedUploadingMedia(.unknown))
|
||||
}
|
||||
|
||||
var roomForIdentifierMocks: [String: RoomProxyMock] = .init()
|
||||
@MainActor
|
||||
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
||||
|
||||
@@ -21,4 +21,5 @@ struct CreateRoomFlowParameters {
|
||||
var name = ""
|
||||
var topic = ""
|
||||
var isRoomPrivate = true
|
||||
var avatarImageMedia: MediaInfo?
|
||||
}
|
||||
|
||||
1
changelog.d/961.feature
Normal file
1
changelog.d/961.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add the image picker flow for the creation of a room
|
||||
Reference in New Issue
Block a user