mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 15:45:11 +00:00
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
This commit is contained in:
@@ -133,6 +133,8 @@ struct ComposerToolbarViewStateBindings {
|
|||||||
var composerExpanded = false
|
var composerExpanded = false
|
||||||
var formatItems: [FormatItem] = .init()
|
var formatItems: [FormatItem] = .init()
|
||||||
var alertInfo: AlertInfo<UUID>?
|
var alertInfo: AlertInfo<UUID>?
|
||||||
|
|
||||||
|
var presendCallback: (() -> Void)?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An item in the toolbar
|
/// An item in the toolbar
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ struct ComposerToolbar: View {
|
|||||||
|
|
||||||
private var sendButton: some View {
|
private var sendButton: some View {
|
||||||
Button {
|
Button {
|
||||||
context.send(viewAction: .sendMessage)
|
sendMessage()
|
||||||
} label: {
|
} label: {
|
||||||
CompoundIcon(context.viewState.composerMode.isEdit ? \.check : \.sendSolid)
|
CompoundIcon(context.viewState.composerMode.isEdit ? \.check : \.sendSolid)
|
||||||
.scaledPadding(6, relativeTo: .title)
|
.scaledPadding(6, relativeTo: .title)
|
||||||
@@ -156,12 +156,13 @@ struct ComposerToolbar: View {
|
|||||||
|
|
||||||
private var messageComposer: some View {
|
private var messageComposer: some View {
|
||||||
MessageComposer(plainComposerText: $context.plainComposerText,
|
MessageComposer(plainComposerText: $context.plainComposerText,
|
||||||
|
presendCallback: $context.presendCallback,
|
||||||
composerView: composerView,
|
composerView: composerView,
|
||||||
mode: context.viewState.composerMode,
|
mode: context.viewState.composerMode,
|
||||||
composerFormattingEnabled: context.composerFormattingEnabled,
|
composerFormattingEnabled: context.composerFormattingEnabled,
|
||||||
showResizeGrabber: context.composerFormattingEnabled,
|
showResizeGrabber: context.composerFormattingEnabled,
|
||||||
isExpanded: $context.composerExpanded) {
|
isExpanded: $context.composerExpanded) {
|
||||||
context.send(viewAction: .sendMessage)
|
sendMessage()
|
||||||
} editAction: {
|
} editAction: {
|
||||||
context.send(viewAction: .editLastMessage)
|
context.send(viewAction: .editLastMessage)
|
||||||
} pasteAction: { provider in
|
} 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 {
|
private var placeholder: String {
|
||||||
switch context.viewState.composerMode {
|
switch context.viewState.composerMode {
|
||||||
case .reply(_, _, let isThread):
|
case .reply(_, _, let isThread):
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ typealias PasteHandler = (NSItemProvider) -> Void
|
|||||||
|
|
||||||
struct MessageComposer: View {
|
struct MessageComposer: View {
|
||||||
@Binding var plainComposerText: NSAttributedString
|
@Binding var plainComposerText: NSAttributedString
|
||||||
|
@Binding var presendCallback: (() -> Void)?
|
||||||
let composerView: WysiwygComposerView
|
let composerView: WysiwygComposerView
|
||||||
let mode: ComposerMode
|
let mode: ComposerMode
|
||||||
let composerFormattingEnabled: Bool
|
let composerFormattingEnabled: Bool
|
||||||
@@ -86,6 +87,7 @@ struct MessageComposer: View {
|
|||||||
} else {
|
} else {
|
||||||
MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder,
|
MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder,
|
||||||
text: $plainComposerText,
|
text: $plainComposerText,
|
||||||
|
presendCallback: $presendCallback,
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
keyHandler: { handleKeyPress($0) },
|
keyHandler: { handleKeyPress($0) },
|
||||||
pasteHandler: pasteAction)
|
pasteHandler: pasteAction)
|
||||||
@@ -253,6 +255,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
|||||||
pasteHandler: nil)
|
pasteHandler: nil)
|
||||||
|
|
||||||
return MessageComposer(plainComposerText: .constant(content),
|
return MessageComposer(plainComposerText: .constant(content),
|
||||||
|
presendCallback: .constant(nil),
|
||||||
composerView: composerView,
|
composerView: composerView,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
composerFormattingEnabled: false,
|
composerFormattingEnabled: false,
|
||||||
|
|||||||
+37
-5
@@ -18,6 +18,7 @@ import SwiftUI
|
|||||||
struct MessageComposerTextField: View {
|
struct MessageComposerTextField: View {
|
||||||
let placeholder: String
|
let placeholder: String
|
||||||
@Binding var text: NSAttributedString
|
@Binding var text: NSAttributedString
|
||||||
|
@Binding var presendCallback: (() -> Void)?
|
||||||
|
|
||||||
let maxHeight: CGFloat
|
let maxHeight: CGFloat
|
||||||
let keyHandler: GenericKeyHandler
|
let keyHandler: GenericKeyHandler
|
||||||
@@ -25,6 +26,7 @@ struct MessageComposerTextField: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
UITextViewWrapper(text: $text,
|
UITextViewWrapper(text: $text,
|
||||||
|
presendCallback: $presendCallback,
|
||||||
maxHeight: maxHeight,
|
maxHeight: maxHeight,
|
||||||
keyHandler: keyHandler,
|
keyHandler: keyHandler,
|
||||||
pasteHandler: pasteHandler)
|
pasteHandler: pasteHandler)
|
||||||
@@ -58,6 +60,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
@Environment(\.timelineContext) private var timelineContext
|
@Environment(\.timelineContext) private var timelineContext
|
||||||
|
|
||||||
@Binding var text: NSAttributedString
|
@Binding var text: NSAttributedString
|
||||||
|
@Binding var presendCallback: (() -> Void)?
|
||||||
|
|
||||||
let maxHeight: CGFloat
|
let maxHeight: CGFloat
|
||||||
|
|
||||||
@@ -68,8 +71,8 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
|
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
|
||||||
// Need to use TextKit 1 for mentions
|
// Need to use TextKit 1 for mentions
|
||||||
let textView = ElementTextView(usingTextLayoutManager: false)
|
let textView = ElementTextView(timelineContext: timelineContext,
|
||||||
textView.timelineContext = timelineContext
|
presendCallback: $presendCallback)
|
||||||
|
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
textView.elementDelegate = context.coordinator
|
textView.elementDelegate = context.coordinator
|
||||||
@@ -182,12 +185,29 @@ private protocol ElementTextViewDelegate: AnyObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||||
var timelineContext: TimelineViewModel.Context?
|
private(set) var timelineContext: TimelineViewModel.Context?
|
||||||
|
private var presendCallback: Binding<(() -> Void)?>
|
||||||
|
private var pillViews = NSHashTable<UIView>.weakObjects()
|
||||||
|
|
||||||
weak var elementDelegate: ElementTextViewDelegate?
|
weak var elementDelegate: ElementTextViewDelegate?
|
||||||
|
|
||||||
private var pillViews = NSHashTable<UIView>.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]? {
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
[UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)),
|
[UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)),
|
||||||
UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))]
|
UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))]
|
||||||
@@ -270,6 +290,17 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate {
|
|||||||
}
|
}
|
||||||
pillViews.removeAllObjects()
|
pillViews.removeAllObjects()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func acceptCurrentSuggestion() {
|
||||||
|
guard isFirstResponder else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputDelegate?.selectionWillChange(self)
|
||||||
|
inputDelegate?.selectionDidChange(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview {
|
struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview {
|
||||||
@@ -292,6 +323,7 @@ struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
MessageComposerTextField(placeholder: "Placeholder",
|
MessageComposerTextField(placeholder: "Placeholder",
|
||||||
text: $text,
|
text: $text,
|
||||||
|
presendCallback: .constant(nil),
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
keyHandler: { _ in },
|
keyHandler: { _ in },
|
||||||
pasteHandler: { _ in })
|
pasteHandler: { _ in })
|
||||||
|
|||||||
Reference in New Issue
Block a user