diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift index a045956f91..9785f61b9c 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift @@ -197,7 +197,10 @@ private class GlobalSearchTextField: UITextField { } override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { - guard let key = presses.first?.key else { return } + guard let key = presses.first?.key else { + super.pressesBegan(presses, with: event) + return + } if keyPressHandler(key.keyCode) { return diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 7a0aafad00..37d9359061 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -32,6 +32,7 @@ enum ComposerToolbarVoiceMessageAction { enum ComposerToolbarViewModelAction { case sendMessage(plain: String, html: String?, mode: RoomScreenComposerMode, intentionalMentions: IntentionalMentions) + case editLastMessage case attach(ComposerAttachmentType) case handlePasteOrDrop(provider: NSItemProvider) @@ -47,6 +48,7 @@ enum ComposerToolbarViewModelAction { enum ComposerToolbarViewAction { case composerAppeared case sendMessage + case editLastMessage case cancelReply case cancelEdit case attach(ComposerAttachmentType) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index 1247f02715..c8c19e03ff 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -135,6 +135,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool sendPlainComposerText() } } + case .editLastMessage: + actionsSubject.send(.editLastMessage) case .cancelReply: set(mode: .default) case .cancelEdit: diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index 94a0eac332..a49181e771 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -161,12 +161,19 @@ struct ComposerToolbar: View { showResizeGrabber: context.viewState.bindings.composerActionsEnabled, isExpanded: $context.composerExpanded) { context.send(viewAction: .sendMessage) + } editAction: { + context.send(viewAction: .editLastMessage) } pasteAction: { provider in context.send(viewAction: .handlePasteOrDrop(provider: provider)) - } replyCancellationAction: { - context.send(viewAction: .cancelReply) - } editCancellationAction: { - context.send(viewAction: .cancelEdit) + } cancellationAction: { + switch context.viewState.composerMode { + case .edit: + context.send(viewAction: .cancelEdit) + case .reply: + context.send(viewAction: .cancelReply) + default: + break + } } onAppearAction: { context.send(viewAction: .composerAppeared) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index 071f72c2df..9dbbd12387 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -18,7 +18,7 @@ import Compound import SwiftUI import WysiwygComposer -typealias EnterKeyHandler = () -> Void +typealias GenericKeyHandler = (_ key: UIKeyboardHIDUsage) -> Void typealias PasteHandler = (NSItemProvider) -> Void struct MessageComposer: View { @@ -27,10 +27,10 @@ struct MessageComposer: View { let mode: RoomScreenComposerMode let showResizeGrabber: Bool @Binding var isExpanded: Bool - let sendAction: EnterKeyHandler + let sendAction: () -> Void + let editAction: () -> Void let pasteAction: PasteHandler - let replyCancellationAction: () -> Void - let editCancellationAction: () -> Void + let cancellationAction: () -> Void let onAppearAction: () -> Void @State private var composerTranslation: CGFloat = 0 @@ -86,7 +86,7 @@ struct MessageComposer: View { MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder, text: $plainComposerText, maxHeight: 300, - enterKeyHandler: sendAction, + keyHandler: { handleKeyPress($0) }, pasteHandler: pasteAction) .tint(.compound.iconAccentTertiary) .padding(.vertical, 10) @@ -103,9 +103,9 @@ struct MessageComposer: View { private var header: some View { switch mode { case .reply(_, let replyDetails, _): - MessageComposerReplyHeader(replyDetails: replyDetails, action: replyCancellationAction) + MessageComposerReplyHeader(replyDetails: replyDetails, action: cancellationAction) case .edit: - MessageComposerEditHeader(action: editCancellationAction) + MessageComposerEditHeader(action: cancellationAction) case .recordVoiceMessage, .previewVoiceMessage, .default: EmptyView() } @@ -135,6 +135,19 @@ struct MessageComposer: View { } } } + + private func handleKeyPress(_ key: UIKeyboardHIDUsage) { + switch key { + case .keyboardReturnOrEnter: + sendAction() + case .keyboardUpArrow: + editAction() + case .keyboardEscape: + cancellationAction() + default: + break + } + } } private struct MessageComposerReplyHeader: View { @@ -244,9 +257,9 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { showResizeGrabber: false, isExpanded: .constant(false), sendAction: { }, + editAction: { }, pasteAction: { _ in }, - replyCancellationAction: { }, - editCancellationAction: { }, + cancellationAction: { }, onAppearAction: { viewModel.setup() }) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift index 601de08a4e..444b77389f 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift @@ -20,13 +20,13 @@ struct MessageComposerTextField: View { @Binding var text: NSAttributedString let maxHeight: CGFloat - let enterKeyHandler: EnterKeyHandler + let keyHandler: GenericKeyHandler let pasteHandler: PasteHandler var body: some View { UITextViewWrapper(text: $text, maxHeight: maxHeight, - enterKeyHandler: enterKeyHandler, + keyHandler: keyHandler, pasteHandler: pasteHandler) .accessibilityLabel(placeholder) .background(placeholderView, alignment: .topLeading) @@ -49,7 +49,7 @@ private struct UITextViewWrapper: UIViewRepresentable { let maxHeight: CGFloat - let enterKeyHandler: EnterKeyHandler + let keyHandler: GenericKeyHandler let pasteHandler: PasteHandler private let font = UIFont.preferredFont(forTextStyle: .body) @@ -115,7 +115,7 @@ private struct UITextViewWrapper: UIViewRepresentable { func makeCoordinator() -> Coordinator { Coordinator(text: $text, maxHeight: maxHeight, - enterKeyHandler: enterKeyHandler, + keyHandler: keyHandler, pasteHandler: pasteHandler) } @@ -124,16 +124,16 @@ private struct UITextViewWrapper: UIViewRepresentable { private let maxHeight: CGFloat - private let enterKeyHandler: EnterKeyHandler + private let keyHandler: GenericKeyHandler private let pasteHandler: PasteHandler init(text: Binding, maxHeight: CGFloat, - enterKeyHandler: @escaping EnterKeyHandler, + keyHandler: @escaping GenericKeyHandler, pasteHandler: @escaping PasteHandler) { self.text = text self.maxHeight = maxHeight - self.enterKeyHandler = enterKeyHandler + self.keyHandler = keyHandler self.pasteHandler = pasteHandler } @@ -141,10 +141,10 @@ private struct UITextViewWrapper: UIViewRepresentable { text.wrappedValue = textView.attributedText } - func textViewDidReceiveEnterKeyPress(_ textView: UITextView) { - enterKeyHandler() + func textViewDidReceiveKeyPress(_ textView: UITextView, key: UIKeyboardHIDUsage) { + keyHandler(key) } - + func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) { textView.insertText("\n") } @@ -157,7 +157,7 @@ private struct UITextViewWrapper: UIViewRepresentable { private protocol ElementTextViewDelegate: AnyObject { func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) - func textViewDidReceiveEnterKeyPress(_ textView: UITextView) + func textViewDidReceiveKeyPress(_ textView: UITextView, key: UIKeyboardHIDUsage) func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider) } @@ -176,9 +176,28 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { @objc func shiftEnterKeyPressed(sender: UIKeyCommand) { elementDelegate?.textViewDidReceiveShiftEnterKeyPress(self) } - + @objc func enterKeyPressed(sender: UIKeyCommand) { - elementDelegate?.textViewDidReceiveEnterKeyPress(self) + elementDelegate?.textViewDidReceiveKeyPress(self, key: .keyboardReturnOrEnter) + } + + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + guard let key = presses.first?.key else { + super.pressesBegan(presses, with: event) + return + } + + if key.keyCode == .keyboardUpArrow, selectedRange.location == 0 { + elementDelegate?.textViewDidReceiveKeyPress(self, key: key.keyCode) + return + } + + if key.keyCode == .keyboardEscape { + elementDelegate?.textViewDidReceiveKeyPress(self, key: key.keyCode) + return + } + + super.pressesBegan(presses, with: event) } // Pasting support @@ -254,7 +273,7 @@ struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview { MessageComposerTextField(placeholder: "Placeholder", text: $text, maxHeight: 300, - enterKeyHandler: { }, + keyHandler: { _ in }, pasteHandler: { _ in }) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index f0331b637b..ca2edd74d0 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -206,6 +206,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol mode: mode, intentionalMentions: intentionalMentions) } + case .editLastMessage: + editLastMessage() case .attach(let attachment): attach(attachment) case .handlePasteOrDrop(let provider): @@ -264,6 +266,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } + private func editLastMessage() { + guard let item = timelineController.timelineItems.reversed().first(where: { + guard let item = $0 as? EventBasedMessageTimelineItemProtocol else { + return false + } + + return item.sender.id == roomProxy.ownUserID && item.isEditable + }) else { + return + } + + roomScreenInteractionHandler.processTimelineItemMenuAction(.edit, itemID: item.id) + } + private func attach(_ attachment: ComposerAttachmentType) { switch attachment { case .camera: diff --git a/changelog.d/2765.feature b/changelog.d/2765.feature new file mode 100644 index 0000000000..5f2742c1fc --- /dev/null +++ b/changelog.d/2765.feature @@ -0,0 +1 @@ +Use the keyboard up arrow to edit the last message, use escape to cancel editing or replying. \ No newline at end of file