From a3cc13e391e3bdfb0bb26ace96948aab4a4e8a6e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 18 Apr 2022 15:13:30 +0300 Subject: [PATCH] Add quote and copy context menu actions. --- ElementX.xcodeproj/project.pbxproj | 4 +++ .../Screens/RoomScreen/RoomScreenModels.swift | 7 ++++ .../RoomScreen/RoomScreenViewModel.swift | 33 +++++++++++++++++++ .../View/MessageComposerTextField.swift | 6 ++-- .../View/TimelineItemContextMenu.swift | 31 +++++++++++++++++ .../RoomScreen/View/TimelineItemList.swift | 3 ++ 6 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index b129f4be8..64b48f952 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */; }; 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109C0201D8CB3F947340DC80 /* WeakDictionary.swift */; }; 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; 03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */; }; @@ -254,6 +255,7 @@ AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = ""; }; B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; + B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummary.swift; sourceTree = ""; }; B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenCoordinator.swift; sourceTree = ""; }; @@ -541,6 +543,7 @@ E18CF12478983A5EB390FB26 /* MessageComposer.swift */, BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, + B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */, 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */, 874A1842477895F199567BD7 /* TimelineView.swift */, B7D3886505ECC85A06DA8258 /* Timeline */, @@ -1062,6 +1065,7 @@ D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */, 7963F98CDFDEAC75E072BD81 /* TextRoomTimelineItem.swift in Sources */, 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */, + 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */, 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */, 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */, 15FEC447B7FEA62F418732AC /* ToastActivityPresenter.swift in Sources */, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 89e3b13db..49431f7d2 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -20,6 +20,11 @@ enum RoomScreenViewModelResult { } +enum TimelineItemContextMenuAction: Hashable { + case copy + case quote +} + enum RoomScreenViewAction { case loadPreviousPage case itemAppeared(id: String) @@ -34,6 +39,8 @@ struct RoomScreenViewState: BindableState { var isBackPaginating = false var bindings: RoomScreenViewStateBindings + var contextMenuBuilder: ((_ itemId: String) -> TimelineItemContextMenu)? + var sendButtonDisabled: Bool { bindings.composerText.count == 0 } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index b2cec8630..b2493955b 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -56,6 +56,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } }.store(in: &cancellables) + state.contextMenuBuilder = buildContexMenuForItemId(_:) + buildTimelineViews() } @@ -93,4 +95,35 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.items = stateItems } + + // MARK: ContextMenus + + private func buildContexMenuForItemId(_ itemId: String) -> TimelineItemContextMenu { + TimelineItemContextMenu(contextMenuActions: self.contextMenuActionsForItemId(itemId)) { [weak self] action in + self?.processContentMenuAction(action, itemId: itemId) + } + } + + private func contextMenuActionsForItemId(_ itemId: String) -> [TimelineItemContextMenuAction] { + guard let timelineItem = self.timelineController.timelineItems.first(where: { $0.id == itemId }), + timelineItem is EventBasedTimelineItemProtocol else { + return [] + } + + return [.copy, .quote] + } + + private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) { + guard let timelineItem = self.timelineController.timelineItems.first(where: { $0.id == itemId }), + let item = timelineItem as? EventBasedTimelineItemProtocol else { + return + } + + switch action { + case .copy: + UIPasteboard.general.string = item.text + case .quote: + state.bindings.composerText = "> \(item.text)" + } + } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/View/MessageComposerTextField.swift index 27a42529c..952fc3dcb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/MessageComposerTextField.swift @@ -129,9 +129,9 @@ private struct UITextViewWrapper: UIViewRepresentable { self.maxHeight = maxHeight } - func textViewDidChange(_ uiView: UITextView) { - text.wrappedValue = uiView.text - UITextViewWrapper.recalculateHeight(view: uiView, + func textViewDidChange(_ textView: UITextView) { + text.wrappedValue = textView.text + UITextViewWrapper.recalculateHeight(view: textView, result: calculatedHeight, maxHeight: maxHeight) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift new file mode 100644 index 000000000..c9fa06bee --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift @@ -0,0 +1,31 @@ +// +// TimelineItemContextMenu.swift +// ElementX +// +// Created by Stefan Ceriu on 18/04/2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import SwiftUI + +public struct TimelineItemContextMenu: View { + + let contextMenuActions: [TimelineItemContextMenuAction] + let callback: (TimelineItemContextMenuAction) -> Void + + @ViewBuilder + public var body: some View { + ForEach(contextMenuActions, id: \.self) { item in + switch item { + case .copy: + Button("Copy") { + callback(item) + } + case .quote: + Button("Quote") { + callback(item) + } + } + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift index 8f2bc0763..40d30f8b5 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemList.swift @@ -36,6 +36,9 @@ struct TimelineItemList: View { // No idea why previews don't work otherwise ForEach(isPreview ? context.viewState.items : timelineItems) { timelineItem in timelineItem + .contextMenu(menuItems: { + context.viewState.contextMenuBuilder?(timelineItem.id) + }) .listRowSeparator(.hidden) .onAppear { context.send(viewAction: .itemAppeared(id: timelineItem.id))