mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Fix message completion trigger to work anywhere in the message (#3696)
* Fix message completion trigger to work anywhere in the message * Add tests for display suggestion in difference message positions * Fix suggestion trigger could support multiple mention symbol * Remove optional parameter type and tweak argument name to hide * Optimize the suggestions trigger using regex * Fix the cursor at wrong position after selected suggestion * Modify the rawSuggestionText pass data in right way * Modify mention symbol to use regex type * Update ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * Update generated mock --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
+4
-4
@@ -32,7 +32,7 @@ struct CompletionSuggestionView: View {
|
||||
EmptyView()
|
||||
} else {
|
||||
ZStack {
|
||||
MentionSuggestionItemView(mediaProvider: nil, item: .init(id: "", displayName: nil, avatarURL: nil, range: .init()))
|
||||
MentionSuggestionItemView(mediaProvider: nil, item: .init(id: "", displayName: nil, avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
.readFrame($prototypeListItemFrame)
|
||||
.hidden()
|
||||
if showBackgroundShadow {
|
||||
@@ -110,15 +110,15 @@ private struct BackgroundView<Content: View>: View {
|
||||
|
||||
struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview {
|
||||
static let multipleItems: [SuggestionItem] = (0...10).map { index in
|
||||
SuggestionItem.user(item: MentionSuggestionItem(id: "\(index)", displayName: "\(index)", avatarURL: nil, range: .init()))
|
||||
SuggestionItem.user(item: MentionSuggestionItem(id: "\(index)", displayName: "\(index)", avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
// Putting them is VStack allows the preview to work properly in tests
|
||||
VStack(spacing: 8) {
|
||||
CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
items: [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init())),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar, range: .init()))]) { _ in }
|
||||
items: [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init(), rawSuggestionText: "")),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar, range: .init(), rawSuggestionText: ""))]) { _ in }
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
|
||||
@@ -154,6 +154,7 @@ struct ComposerToolbar: View {
|
||||
private var messageComposer: some View {
|
||||
MessageComposer(plainComposerText: $context.plainComposerText,
|
||||
presendCallback: $context.presendCallback,
|
||||
selectedRange: $context.selectedRange,
|
||||
composerView: composerView,
|
||||
mode: context.viewState.composerMode,
|
||||
composerFormattingEnabled: context.composerFormattingEnabled,
|
||||
@@ -199,6 +200,9 @@ struct ComposerToolbar: View {
|
||||
.onChange(of: context.composerFormattingEnabled) {
|
||||
context.send(viewAction: .didToggleFormattingOptions)
|
||||
}
|
||||
.onChange(of: context.selectedRange) {
|
||||
context.send(viewAction: .selectedTextChanged)
|
||||
}
|
||||
.onAppear {
|
||||
composerFocused = context.composerFocused
|
||||
}
|
||||
@@ -307,8 +311,8 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init())),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar, range: .init()))]
|
||||
static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init(), rawSuggestionText: "")),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar, range: .init(), rawSuggestionText: ""))]
|
||||
|
||||
static var previews: some View {
|
||||
ComposerToolbar.mock(focused: true)
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ struct MentionSuggestionItemView_Previews: PreviewProvider, TestablePreview {
|
||||
static let mockMediaProvider = MediaProviderMock(configuration: .init())
|
||||
|
||||
static var previews: some View {
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar, range: .init()))
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test2", displayName: nil, avatarURL: nil, range: .init()))
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar, range: .init(), rawSuggestionText: ""))
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test2", displayName: nil, avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ typealias PasteHandler = (NSItemProvider) -> Void
|
||||
struct MessageComposer: View {
|
||||
@Binding var plainComposerText: NSAttributedString
|
||||
@Binding var presendCallback: (() -> Void)?
|
||||
@Binding var selectedRange: NSRange
|
||||
let composerView: WysiwygComposerView
|
||||
let mode: ComposerMode
|
||||
let composerFormattingEnabled: Bool
|
||||
@@ -66,6 +67,7 @@ struct MessageComposer: View {
|
||||
MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder,
|
||||
text: $plainComposerText,
|
||||
presendCallback: $presendCallback,
|
||||
selectedRange: $selectedRange,
|
||||
maxHeight: ComposerConstant.maxHeight,
|
||||
keyHandler: { handleKeyPress($0) },
|
||||
pasteHandler: pasteAction)
|
||||
@@ -285,6 +287,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
return MessageComposer(plainComposerText: .constant(content),
|
||||
presendCallback: .constant(nil),
|
||||
selectedRange: .constant(NSRange(location: 0, length: 0)),
|
||||
composerView: composerView,
|
||||
mode: mode,
|
||||
composerFormattingEnabled: false,
|
||||
|
||||
+18
@@ -12,6 +12,7 @@ struct MessageComposerTextField: View {
|
||||
let placeholder: String
|
||||
@Binding var text: NSAttributedString
|
||||
@Binding var presendCallback: (() -> Void)?
|
||||
@Binding var selectedRange: NSRange
|
||||
|
||||
let maxHeight: CGFloat
|
||||
let keyHandler: GenericKeyHandler
|
||||
@@ -20,6 +21,7 @@ struct MessageComposerTextField: View {
|
||||
var body: some View {
|
||||
UITextViewWrapper(text: $text,
|
||||
presendCallback: $presendCallback,
|
||||
selectedRange: $selectedRange,
|
||||
maxHeight: maxHeight,
|
||||
keyHandler: keyHandler,
|
||||
pasteHandler: pasteHandler)
|
||||
@@ -54,6 +56,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
@Binding var text: NSAttributedString
|
||||
@Binding var presendCallback: (() -> Void)?
|
||||
@Binding var selectedRange: NSRange
|
||||
|
||||
let maxHeight: CGFloat
|
||||
|
||||
@@ -135,12 +138,15 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
// moves the caret back to the bottom of the composer.
|
||||
// https://github.com/element-hq/element-x-ios/issues/3104
|
||||
textView.selectedTextRange = selection
|
||||
} else {
|
||||
textView.selectedRange = selectedRange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(text: $text,
|
||||
selectedRange: $selectedRange,
|
||||
maxHeight: maxHeight,
|
||||
keyHandler: keyHandler,
|
||||
pasteHandler: pasteHandler)
|
||||
@@ -148,6 +154,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate, ElementTextViewDelegate {
|
||||
private var text: Binding<NSAttributedString>
|
||||
private var selectedRange: Binding<NSRange>
|
||||
|
||||
private let maxHeight: CGFloat
|
||||
|
||||
@@ -155,10 +162,12 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
private let pasteHandler: PasteHandler
|
||||
|
||||
init(text: Binding<NSAttributedString>,
|
||||
selectedRange: Binding<NSRange>,
|
||||
maxHeight: CGFloat,
|
||||
keyHandler: @escaping GenericKeyHandler,
|
||||
pasteHandler: @escaping PasteHandler) {
|
||||
self.text = text
|
||||
self.selectedRange = selectedRange
|
||||
self.maxHeight = maxHeight
|
||||
self.keyHandler = keyHandler
|
||||
self.pasteHandler = pasteHandler
|
||||
@@ -179,6 +188,14 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider) {
|
||||
pasteHandler(provider)
|
||||
}
|
||||
|
||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
if selectedRange.wrappedValue != textView.selectedRange {
|
||||
DispatchQueue.main.async {
|
||||
self.selectedRange.wrappedValue = textView.selectedRange
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +348,7 @@ struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview {
|
||||
MessageComposerTextField(placeholder: "Placeholder",
|
||||
text: $text,
|
||||
presendCallback: .constant(nil),
|
||||
selectedRange: .constant(NSRange(location: 0, length: 0)),
|
||||
maxHeight: 300,
|
||||
keyHandler: { _ in },
|
||||
pasteHandler: { _ in })
|
||||
|
||||
Reference in New Issue
Block a user