Implements #745, #746, #747 - Initial media uploading flows

- room composer contextual menu for selecting source
- coordinator for presenting the different sources
- system picker handling and URL passback
- file preview after selection
- feature flag
This commit is contained in:
Stefan Ceriu
2023-03-28 17:47:34 +03:00
committed by Stefan Ceriu
parent 2359910857
commit 5f6edacc1a
27 changed files with 787 additions and 29 deletions
@@ -62,6 +62,12 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
self.displayEmojiPickerScreen(for: itemId)
case .displayReportContent(let itemId):
self.displayReportContent(for: itemId)
case .displayCameraPicker:
self.displayMediaPickerWithSource(.camera)
case .displayMediaPicker:
self.displayMediaPickerWithSource(.photoLibrary)
case .displayDocumentPicker:
self.displayMediaPickerWithSource(.documents)
}
}
}
@@ -74,8 +80,32 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
func toPresentable() -> AnyView {
AnyView(RoomScreen(context: viewModel.context))
}
// MARK: - Private
private func displayMediaPickerWithSource(_ source: MediaPickerSource) {
let mediaPickerCoordinator = MediaPickerCoordinator(source: source) { [weak self] action in
switch action {
case .cancel:
self?.navigationStackCoordinator.setSheetCoordinator(nil)
case .error:
break
case .selectMediaAtURL(let url):
let mediaPickerPreviewScreenCoordinator = MediaPickerPreviewScreenCoordinator(parameters: .init(url: url, title: url.lastPathComponent)) { action in
switch action {
case .send:
self?.navigationStackCoordinator.setSheetCoordinator(nil)
case .cancel:
self?.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
self?.navigationStackCoordinator.setSheetCoordinator(mediaPickerPreviewScreenCoordinator)
}
}
navigationStackCoordinator.setSheetCoordinator(mediaPickerCoordinator)
}
private func displayFilePreview(for file: MediaFileHandleProxy, with title: String?) {
let params = FilePreviewCoordinatorParameters(mediaFile: file, title: title)
@@ -23,6 +23,9 @@ enum RoomScreenViewModelAction {
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
case displayEmojiPicker(itemId: String)
case displayReportContent(itemId: String)
case displayCameraPicker
case displayMediaPicker
case displayDocumentPicker
}
enum RoomScreenComposerMode: Equatable {
@@ -55,6 +58,10 @@ enum RoomScreenViewAction {
/// Mark the entire room as read - this is heavy handed as a starting point for now.
case markRoomAsRead
case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction)
case displayCameraPicker
case displayMediaPicker
case displayDocumentPicker
}
struct RoomScreenViewState: BindableState {
@@ -66,6 +73,7 @@ struct RoomScreenViewState: BindableState {
var isBackPaginating = false
var showLoading = false
var timelineStyle: TimelineStyle
var mediaUploadingFlowEnabled: Bool
var bindings: RoomScreenViewStateBindings
@@ -39,6 +39,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
roomTitle: roomName ?? "Unknown room 💥",
roomAvatarURL: roomAvatarUrl,
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
mediaUploadingFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled,
bindings: .init(composerText: "", composerFocused: false)),
imageProvider: mediaProvider)
@@ -74,6 +75,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
.weakAssign(to: \.state.timelineStyle, on: self)
.store(in: &cancellables)
ServiceLocator.shared.settings.$mediaUploadingFlowEnabled
.weakAssign(to: \.state.mediaUploadingFlowEnabled, on: self)
.store(in: &cancellables)
buildTimelineViews()
}
@@ -111,6 +116,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
await markRoomAsRead()
case .contextMenuAction(let itemID, let action):
processContentMenuAction(action, itemID: itemID)
case .displayCameraPicker:
callback?(.displayCameraPicker)
case .displayMediaPicker:
callback?(.displayMediaPicker)
case .displayDocumentPicker:
callback?(.displayDocumentPicker)
}
}
@@ -23,7 +23,16 @@ struct RoomScreen: View {
var body: some View {
timeline
.background(Color.element.background.ignoresSafeArea()) // Kills the toolbar translucency.
.safeAreaInset(edge: .bottom, spacing: 0) { messageComposer }
.safeAreaInset(edge: .bottom, spacing: 0) {
HStack(spacing: 4.0) {
if context.viewState.mediaUploadingFlowEnabled {
sendAttachmentButton
}
messageComposer
}
.padding()
}
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.toolbarRole(.editor) // Hide the back button title.
@@ -40,7 +49,7 @@ struct RoomScreen: View {
}
}
var timeline: some View {
private var timeline: some View {
TimelineView()
.id(context.viewState.roomId)
.environmentObject(context)
@@ -48,7 +57,7 @@ struct RoomScreen: View {
.overlay(alignment: .bottomTrailing) { scrollToBottomButton }
}
var messageComposer: some View {
private var messageComposer: some View {
MessageComposer(text: $context.composerText,
focused: $context.composerFocused,
sendingDisabled: context.viewState.sendButtonDisabled,
@@ -59,10 +68,9 @@ struct RoomScreen: View {
} editCancellationAction: {
context.send(viewAction: .cancelEdit)
}
.padding()
}
var scrollToBottomButton: some View {
private var scrollToBottomButton: some View {
Button { context.viewState.scrollToBottomPublisher.send(()) } label: {
Image(systemName: "chevron.down")
.font(.element.body)
@@ -84,7 +92,7 @@ struct RoomScreen: View {
}
@ViewBuilder
var loadingIndicator: some View {
private var loadingIndicator: some View {
if context.viewState.showLoading {
ProgressView()
.progressViewStyle(.circular)
@@ -96,7 +104,7 @@ struct RoomScreen: View {
}
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
private var toolbar: some ToolbarContent {
// .principal + .primaryAction works better than .navigation leading + trailing
// as the latter disables interaction in the action button for rooms with long names
ToolbarItem(placement: .principal) {
@@ -104,6 +112,30 @@ struct RoomScreen: View {
}
}
private var sendAttachmentButton: some View {
Menu {
Button {
context.send(viewAction: .displayDocumentPicker)
} label: {
Label(UntranslatedL10n.mediaUploadDocumentPicker, systemImage: "doc")
}
Button {
context.send(viewAction: .displayMediaPicker)
} label: {
Label(UntranslatedL10n.mediaUploadPhotoAndVideoPicker, systemImage: "photo")
}
Button {
context.send(viewAction: .displayCameraPicker)
} label: {
Label(UntranslatedL10n.mediaUploadCameraPicker, systemImage: "camera")
}
} label: {
Image(systemName: "plus.circle")
.font(.element.title1)
.foregroundColor(.element.brand)
}
}
private func sendMessage() {
guard !context.viewState.sendButtonDisabled else { return }
context.send(viewAction: .sendMessage)