diff --git a/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerView.swift b/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerView.swift index 65c0999..1af9090 100644 --- a/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerView.swift +++ b/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerView.swift @@ -88,7 +88,7 @@ public struct WysiwygComposerView: View { @ViewBuilder private var placeholderView: some View { - if viewModel.isContentEmpty { + if viewModel.isContentEmpty, !viewModel.textView.isDictationRunning { Text(placeholder) .font(Font(UIFont.preferredFont(forTextStyle: .body))) .foregroundColor(placeholderColor) diff --git a/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerViewModel.swift b/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerViewModel.swift index 3a55f2d..de9c667 100644 --- a/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerViewModel.swift +++ b/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygComposerViewModel.swift @@ -556,7 +556,8 @@ private extension WysiwygComposerViewModel { /// Reconciliate the content of the model with the content of the text view. func reconciliateIfNeeded() { do { - guard let replacement = try StringDiffer.replacement(from: attributedContent.text.htmlChars, + guard !textView.isDictationRunning, + let replacement = try StringDiffer.replacement(from: attributedContent.text.htmlChars, to: textView.attributedText.htmlChars) else { return } diff --git a/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygTextView.swift b/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygTextView.swift index c6a532a..3120228 100644 --- a/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygTextView.swift +++ b/Sources/WysiwygComposer/Components/WysiwygComposerView/WysiwygTextView.swift @@ -44,6 +44,8 @@ protocol WysiwygTextViewDelegate: AnyObject { public protocol MentionDisplayHelper { } public class WysiwygTextView: UITextView { + private(set) var isDictationRunning = false + /// Internal delegate for the text view. weak var wysiwygDelegate: WysiwygTextViewDelegate? @@ -53,12 +55,42 @@ public class WysiwygTextView: UITextView { override public init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) - contentMode = .redraw + commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) + commonInit() + } + + private func commonInit() { contentMode = .redraw + NotificationCenter.default.addObserver(self, + selector: #selector(textInputCurrentInputModeDidChange), + name: UITextInputMode.currentInputModeDidChangeNotification, + object: nil) + } + + @objc private func textInputCurrentInputModeDidChange(notification: Notification) { + // We don't care about the input mode if this is not the first responder + guard isFirstResponder else { + return + } + + guard let inputMode = textInputMode?.primaryLanguage, + inputMode == "dictation" else { + isDictationRunning = false + return + } + isDictationRunning = true + } + + override public func dictationRecordingDidEnd() { + isDictationRunning = false + } + + override public func dictationRecognitionFailed() { + isDictationRunning = false } /// Register a pill view that has been added through `NSTextAttachmentViewProvider`. diff --git a/Sources/WysiwygComposer/WysiwygComposer.swift b/Sources/WysiwygComposer/WysiwygComposer.swift index 9c83922..59ae9bf 100644 --- a/Sources/WysiwygComposer/WysiwygComposer.swift +++ b/Sources/WysiwygComposer/WysiwygComposer.swift @@ -982,6 +982,73 @@ public func FfiConverterTypeComposerUpdate_lower(_ value: ComposerUpdate) -> Uns return FfiConverterTypeComposerUpdate.lower(value) } +public protocol MentionDetectorProtocol { + func isMention(url: String) -> Bool +} + +public class MentionDetector: MentionDetectorProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + deinit { + try! rustCall { uniffi_uniffi_wysiwyg_composer_fn_free_mentiondetector(pointer, $0) } + } + + public func isMention(url: String) -> Bool { + return try! FfiConverterBool.lift( + try! + rustCall { + uniffi_uniffi_wysiwyg_composer_fn_method_mentiondetector_is_mention(self.pointer, + FfiConverterString.lower(url), $0) + } + ) + } +} + +public struct FfiConverterTypeMentionDetector: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = MentionDetector + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MentionDetector { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: MentionDetector, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> MentionDetector { + return MentionDetector(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: MentionDetector) -> UnsafeMutableRawPointer { + return value.pointer + } +} + +public func FfiConverterTypeMentionDetector_lift(_ pointer: UnsafeMutableRawPointer) throws -> MentionDetector { + return try FfiConverterTypeMentionDetector.lift(pointer) +} + +public func FfiConverterTypeMentionDetector_lower(_ value: MentionDetector) -> UnsafeMutableRawPointer { + return FfiConverterTypeMentionDetector.lower(value) +} + public struct Attribute { public var key: String public var value: String @@ -1828,6 +1895,14 @@ public func newComposerModel() -> ComposerModel { ) } +public func newMentionDetector() -> MentionDetector { + return try! FfiConverterTypeMentionDetector.lift( + try! rustCall { + uniffi_uniffi_wysiwyg_composer_fn_func_new_mention_detector($0) + } + ) +} + private enum InitializationResult { case ok case contractVersionMismatch @@ -1847,6 +1922,9 @@ private var initializationResult: InitializationResult { if uniffi_uniffi_wysiwyg_composer_checksum_func_new_composer_model() != 61235 { return InitializationResult.apiChecksumMismatch } + if uniffi_uniffi_wysiwyg_composer_checksum_func_new_mention_detector() != 30911 { + return InitializationResult.apiChecksumMismatch + } if uniffi_uniffi_wysiwyg_composer_checksum_method_composermodel_action_states() != 7578 { return InitializationResult.apiChecksumMismatch } @@ -1988,6 +2066,9 @@ private var initializationResult: InitializationResult { if uniffi_uniffi_wysiwyg_composer_checksum_method_composerupdate_text_update() != 40178 { return InitializationResult.apiChecksumMismatch } + if uniffi_uniffi_wysiwyg_composer_checksum_method_mentiondetector_is_mention() != 64462 { + return InitializationResult.apiChecksumMismatch + } return InitializationResult.ok } diff --git a/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h b/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h index 1ef461f..968d42d 100644 --- a/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h +++ b/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h @@ -61,6 +61,8 @@ typedef struct RustCallStatus { // Callbacks for UniFFI Futures typedef void (*UniFfiFutureCallbackUInt8)(const void * _Nonnull, uint8_t, RustCallStatus); +typedef void (*UniFfiFutureCallbackInt8)(const void * _Nonnull, int8_t, RustCallStatus); +typedef void (*UniFfiFutureCallbackUnsafeMutableRawPointer)(const void * _Nonnull, void*_Nonnull, RustCallStatus); typedef void (*UniFfiFutureCallbackUnsafeMutableRawPointer)(const void * _Nonnull, void*_Nonnull, RustCallStatus); typedef void (*UniFfiFutureCallbackUnsafeMutableRawPointer)(const void * _Nonnull, void*_Nonnull, RustCallStatus); typedef void (*UniFfiFutureCallbackRustBuffer)(const void * _Nonnull, RustBuffer, RustCallStatus); @@ -164,8 +166,15 @@ RustBuffer uniffi_uniffi_wysiwyg_composer_fn_method_composerupdate_menu_state(vo ); RustBuffer uniffi_uniffi_wysiwyg_composer_fn_method_composerupdate_text_update(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status ); +void uniffi_uniffi_wysiwyg_composer_fn_free_mentiondetector(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +int8_t uniffi_uniffi_wysiwyg_composer_fn_method_mentiondetector_is_mention(void*_Nonnull ptr, RustBuffer url, RustCallStatus *_Nonnull out_status +); void*_Nonnull uniffi_uniffi_wysiwyg_composer_fn_func_new_composer_model(RustCallStatus *_Nonnull out_status +); +void*_Nonnull uniffi_uniffi_wysiwyg_composer_fn_func_new_mention_detector(RustCallStatus *_Nonnull out_status + ); RustBuffer ffi_wysiwyg_composer_rustbuffer_alloc(int32_t size, RustCallStatus *_Nonnull out_status ); @@ -177,6 +186,9 @@ RustBuffer ffi_wysiwyg_composer_rustbuffer_reserve(RustBuffer buf, int32_t addit ); uint16_t uniffi_uniffi_wysiwyg_composer_checksum_func_new_composer_model(void +); +uint16_t uniffi_uniffi_wysiwyg_composer_checksum_func_new_mention_detector(void + ); uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_composermodel_action_states(void @@ -318,6 +330,9 @@ uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_composerupdate_menu_stat ); uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_composerupdate_text_update(void +); +uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_mentiondetector_is_mention(void + ); uint32_t ffi_wysiwyg_composer_uniffi_contract_version(void diff --git a/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/WysiwygComposerFFI b/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/WysiwygComposerFFI index 9224a54..eb34c49 100644 Binary files a/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/WysiwygComposerFFI and b/WysiwygComposerFFI.xcframework/ios-arm64/WysiwygComposerFFI.framework/WysiwygComposerFFI differ diff --git a/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h b/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h index 1ef461f..968d42d 100644 --- a/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h +++ b/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/Headers/WysiwygComposerFFI.h @@ -61,6 +61,8 @@ typedef struct RustCallStatus { // Callbacks for UniFFI Futures typedef void (*UniFfiFutureCallbackUInt8)(const void * _Nonnull, uint8_t, RustCallStatus); +typedef void (*UniFfiFutureCallbackInt8)(const void * _Nonnull, int8_t, RustCallStatus); +typedef void (*UniFfiFutureCallbackUnsafeMutableRawPointer)(const void * _Nonnull, void*_Nonnull, RustCallStatus); typedef void (*UniFfiFutureCallbackUnsafeMutableRawPointer)(const void * _Nonnull, void*_Nonnull, RustCallStatus); typedef void (*UniFfiFutureCallbackUnsafeMutableRawPointer)(const void * _Nonnull, void*_Nonnull, RustCallStatus); typedef void (*UniFfiFutureCallbackRustBuffer)(const void * _Nonnull, RustBuffer, RustCallStatus); @@ -164,8 +166,15 @@ RustBuffer uniffi_uniffi_wysiwyg_composer_fn_method_composerupdate_menu_state(vo ); RustBuffer uniffi_uniffi_wysiwyg_composer_fn_method_composerupdate_text_update(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status ); +void uniffi_uniffi_wysiwyg_composer_fn_free_mentiondetector(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +int8_t uniffi_uniffi_wysiwyg_composer_fn_method_mentiondetector_is_mention(void*_Nonnull ptr, RustBuffer url, RustCallStatus *_Nonnull out_status +); void*_Nonnull uniffi_uniffi_wysiwyg_composer_fn_func_new_composer_model(RustCallStatus *_Nonnull out_status +); +void*_Nonnull uniffi_uniffi_wysiwyg_composer_fn_func_new_mention_detector(RustCallStatus *_Nonnull out_status + ); RustBuffer ffi_wysiwyg_composer_rustbuffer_alloc(int32_t size, RustCallStatus *_Nonnull out_status ); @@ -177,6 +186,9 @@ RustBuffer ffi_wysiwyg_composer_rustbuffer_reserve(RustBuffer buf, int32_t addit ); uint16_t uniffi_uniffi_wysiwyg_composer_checksum_func_new_composer_model(void +); +uint16_t uniffi_uniffi_wysiwyg_composer_checksum_func_new_mention_detector(void + ); uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_composermodel_action_states(void @@ -318,6 +330,9 @@ uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_composerupdate_menu_stat ); uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_composerupdate_text_update(void +); +uint16_t uniffi_uniffi_wysiwyg_composer_checksum_method_mentiondetector_is_mention(void + ); uint32_t ffi_wysiwyg_composer_uniffi_contract_version(void diff --git a/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/WysiwygComposerFFI b/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/WysiwygComposerFFI index 9ea27f2..cf2ef91 100644 Binary files a/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/WysiwygComposerFFI and b/WysiwygComposerFFI.xcframework/ios-arm64_x86_64-simulator/WysiwygComposerFFI.framework/WysiwygComposerFFI differ