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 <takshkothari09@gmail.com>
This commit is contained in:
Taksh 2026-07-31 15:29:24 +03:00
parent 22386a42b8
commit d43440d79e

View File

@ -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 {