From 062811cac4a46de80eb54e1710149d2cb748db11 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 2 Sep 2024 16:33:40 +0300 Subject: [PATCH] Accept current autocorrection/suggestion before sending a message (#3219) * Fixes #3216 - Accept current autocorrection/suggestion before sending a message. * Switch from a temporary textField to `inputDelegate` selection changes --- .../ComposerToolbarModels.swift | 2 + .../View/ComposerToolbar.swift | 16 ++++++- .../View/MessageComposer.swift | 3 ++ .../View/MessageComposerTextField.swift | 42 ++++++++++++++++--- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index cbdb9e5687..b96871eae8 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -133,6 +133,8 @@ struct ComposerToolbarViewStateBindings { var composerExpanded = false var formatItems: [FormatItem] = .init() var alertInfo: AlertInfo? + + var presendCallback: (() -> Void)? } /// An item in the toolbar diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index d10b6bb8a2..66d63f51a3 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -137,7 +137,7 @@ struct ComposerToolbar: View { private var sendButton: some View { Button { - context.send(viewAction: .sendMessage) + sendMessage() } label: { CompoundIcon(context.viewState.composerMode.isEdit ? \.check : \.sendSolid) .scaledPadding(6, relativeTo: .title) @@ -156,12 +156,13 @@ struct ComposerToolbar: View { private var messageComposer: some View { MessageComposer(plainComposerText: $context.plainComposerText, + presendCallback: $context.presendCallback, composerView: composerView, mode: context.viewState.composerMode, composerFormattingEnabled: context.composerFormattingEnabled, showResizeGrabber: context.composerFormattingEnabled, isExpanded: $context.composerExpanded) { - context.send(viewAction: .sendMessage) + sendMessage() } editAction: { context.send(viewAction: .editLastMessage) } pasteAction: { provider in @@ -205,6 +206,17 @@ struct ComposerToolbar: View { } } + private func sendMessage() { + // Allow the inner TextField do apply any final processing before + // sending e.g. accepting current autocorrection. + // Fixes https://github.com/element-hq/element-x-ios/issues/3216 + context.presendCallback?() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + context.send(viewAction: .sendMessage) + } + } + private var placeholder: String { switch context.viewState.composerMode { case .reply(_, _, let isThread): diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index def4bacbaa..e39951a28f 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -23,6 +23,7 @@ typealias PasteHandler = (NSItemProvider) -> Void struct MessageComposer: View { @Binding var plainComposerText: NSAttributedString + @Binding var presendCallback: (() -> Void)? let composerView: WysiwygComposerView let mode: ComposerMode let composerFormattingEnabled: Bool @@ -86,6 +87,7 @@ struct MessageComposer: View { } else { MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder, text: $plainComposerText, + presendCallback: $presendCallback, maxHeight: 300, keyHandler: { handleKeyPress($0) }, pasteHandler: pasteAction) @@ -253,6 +255,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { pasteHandler: nil) return MessageComposer(plainComposerText: .constant(content), + presendCallback: .constant(nil), composerView: composerView, mode: mode, composerFormattingEnabled: false, diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift index b127e0894e..ae15d136bc 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift @@ -18,6 +18,7 @@ import SwiftUI struct MessageComposerTextField: View { let placeholder: String @Binding var text: NSAttributedString + @Binding var presendCallback: (() -> Void)? let maxHeight: CGFloat let keyHandler: GenericKeyHandler @@ -25,6 +26,7 @@ struct MessageComposerTextField: View { var body: some View { UITextViewWrapper(text: $text, + presendCallback: $presendCallback, maxHeight: maxHeight, keyHandler: keyHandler, pasteHandler: pasteHandler) @@ -58,6 +60,7 @@ private struct UITextViewWrapper: UIViewRepresentable { @Environment(\.timelineContext) private var timelineContext @Binding var text: NSAttributedString + @Binding var presendCallback: (() -> Void)? let maxHeight: CGFloat @@ -68,8 +71,8 @@ private struct UITextViewWrapper: UIViewRepresentable { func makeUIView(context: UIViewRepresentableContext) -> UITextView { // Need to use TextKit 1 for mentions - let textView = ElementTextView(usingTextLayoutManager: false) - textView.timelineContext = timelineContext + let textView = ElementTextView(timelineContext: timelineContext, + presendCallback: $presendCallback) textView.delegate = context.coordinator textView.elementDelegate = context.coordinator @@ -182,12 +185,29 @@ private protocol ElementTextViewDelegate: AnyObject { } private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { - var timelineContext: TimelineViewModel.Context? + private(set) var timelineContext: TimelineViewModel.Context? + private var presendCallback: Binding<(() -> Void)?> + private var pillViews = NSHashTable.weakObjects() weak var elementDelegate: ElementTextViewDelegate? - private var pillViews = NSHashTable.weakObjects() - + init(timelineContext: TimelineViewModel.Context?, + presendCallback: Binding<(() -> Void)?>) { + self.timelineContext = timelineContext + self.presendCallback = presendCallback + + super.init(frame: .zero, textContainer: nil) + + presendCallback.wrappedValue = { [weak self] in + self?.acceptCurrentSuggestion() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError() + } + override var keyCommands: [UIKeyCommand]? { [UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)), UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))] @@ -270,6 +290,17 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { } pillViews.removeAllObjects() } + + // MARK: - Private + + private func acceptCurrentSuggestion() { + guard isFirstResponder else { + return + } + + inputDelegate?.selectionWillChange(self) + inputDelegate?.selectionDidChange(self) + } } struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview { @@ -292,6 +323,7 @@ struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview { var body: some View { MessageComposerTextField(placeholder: "Placeholder", text: $text, + presendCallback: .constant(nil), maxHeight: 300, keyHandler: { _ in }, pasteHandler: { _ in })