mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
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)
|
||||
|
||||
Reference in New Issue
Block a user