From d43440d79eec9b1c8c677dc1c1df8d737d5e1917 Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 31 Jul 2026 15:29:24 +0300 Subject: [PATCH] fix: debounce autocorrect trait flips across token boundaries Avoid thrashing UIKit keyboard traits on every keystroke when the caret crosses / @ # tokens (#969 review). Signed-off-by: Taksh --- bitchat/Views/ContentComposerView.swift | 38 +++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/bitchat/Views/ContentComposerView.swift b/bitchat/Views/ContentComposerView.swift index e9a8f434..eb975203 100644 --- a/bitchat/Views/ContentComposerView.swift +++ b/bitchat/Views/ContentComposerView.swift @@ -19,6 +19,12 @@ struct ContentComposerView: View { @ObservedObject var voiceRecordingVM: VoiceRecordingViewModel @Binding var autocompleteDebounceTimer: Timer? + /// Applied autocorrect gate — updated on a short debounce so crossing a + /// `/` `@` `#` boundary doesn't thrash UIKit text-input traits on every + /// keystroke (can flicker the keyboard on some iOS versions). + @State private var appliedAutocorrectDisabled = false + @State private var autocorrectTraitTimer: Timer? + let onSendMessage: () -> Void #if os(iOS) @@ -79,9 +85,9 @@ struct ContentComposerView: View { // Token-aware (#969): autocorrect for prose, off while the // current token is a /command, @mention, or #channel so the // keyboard doesn't fight exact tokens (or learn them). - .autocorrectionDisabled(shouldDisableAutocorrect) + .autocorrectionDisabled(appliedAutocorrectDisabled) #if os(iOS) - .textInputAutocapitalization(shouldDisableAutocorrect ? .never : .sentences) + .textInputAutocapitalization(appliedAutocorrectDisabled ? .never : .sentences) #endif .submitLabel(.send) .modifier(AutocompleteKeyboardNavigationModifier( @@ -126,6 +132,13 @@ struct ContentComposerView: View { conversationUIModel.updateAutocomplete(for: newValue, cursorPosition: cursorPosition) } } + scheduleAutocorrectTraitUpdate(for: newValue) + } + .onAppear { + appliedAutocorrectDisabled = ComposerAutocorrect.shouldDisable( + for: messageText, + cursorPosition: messageText.count + ) } HStack(alignment: .center, spacing: 4) { @@ -158,6 +171,27 @@ private extension ContentComposerView { ComposerAutocorrect.shouldDisable(for: messageText, cursorPosition: messageText.count) } + /// Debounce trait flips so UIKit isn't asked to reload the keyboard on + /// every character while the user is still deciding the token. + func scheduleAutocorrectTraitUpdate(for text: String) { + let desired = ComposerAutocorrect.shouldDisable(for: text, cursorPosition: text.count) + guard desired != appliedAutocorrectDisabled else { + autocorrectTraitTimer?.invalidate() + return + } + autocorrectTraitTimer?.invalidate() + autocorrectTraitTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: false) { _ in + Task { @MainActor in + let latest = ComposerAutocorrect.shouldDisable( + for: messageText, + cursorPosition: messageText.count + ) + guard latest != appliedAutocorrectDisabled else { return } + appliedAutocorrectDisabled = latest + } + } + } + /// The nearby-only scope toggle appears only where it means something: /// the public mesh channel with the bridge on. var showsNearbyOnlyToggle: Bool {