Skip to content

Commit

Permalink
Accept current autocorrection/suggestion before sending a message (#3219
Browse files Browse the repository at this point in the history
)

* Fixes #3216 - Accept current autocorrection/suggestion before sending a message.

* Switch from a temporary textField to `inputDelegate` selection changes
  • Loading branch information
stefanceriu authored Sep 2, 2024
1 parent 302df18 commit 062811c
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ struct ComposerToolbarViewStateBindings {
var composerExpanded = false
var formatItems: [FormatItem] = .init()
var alertInfo: AlertInfo<UUID>?

var presendCallback: (() -> Void)?
}

/// An item in the toolbar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,6 +87,7 @@ struct MessageComposer: View {
} else {
MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder,
text: $plainComposerText,
presendCallback: $presendCallback,
maxHeight: 300,
keyHandler: { handleKeyPress($0) },
pasteHandler: pasteAction)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import SwiftUI
struct MessageComposerTextField: View {
let placeholder: String
@Binding var text: NSAttributedString
@Binding var presendCallback: (() -> Void)?

let maxHeight: CGFloat
let keyHandler: GenericKeyHandler
let pasteHandler: PasteHandler

var body: some View {
UITextViewWrapper(text: $text,
presendCallback: $presendCallback,
maxHeight: maxHeight,
keyHandler: keyHandler,
pasteHandler: pasteHandler)
Expand Down Expand Up @@ -58,6 +60,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
@Environment(\.timelineContext) private var timelineContext

@Binding var text: NSAttributedString
@Binding var presendCallback: (() -> Void)?

let maxHeight: CGFloat

Expand All @@ -68,8 +71,8 @@ private struct UITextViewWrapper: UIViewRepresentable {

func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> 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
Expand Down Expand Up @@ -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<UIView>.weakObjects()

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]? {
[UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)),
UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))]
Expand Down Expand Up @@ -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 {
Expand All @@ -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 })
Expand Down

0 comments on commit 062811c

Please sign in to comment.