From 4e49a4be86810a6323d8bd09786d671141207de0 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 27 Jul 2026 10:07:52 +0300 Subject: [PATCH 1/6] fix: enable autocorrect in the message composer Leave autocorrection on for chat text so iOS can fix typos while typing. Nickname and geohash fields stay disabled. --- bitchat/Views/ContentComposerView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitchat/Views/ContentComposerView.swift b/bitchat/Views/ContentComposerView.swift index 64021d52..a17b5a7a 100644 --- a/bitchat/Views/ContentComposerView.swift +++ b/bitchat/Views/ContentComposerView.swift @@ -76,7 +76,8 @@ struct ContentComposerView: View { .bitchatFont(size: 15) .foregroundColor(palette.primary) .focused(isTextFieldFocused) - .autocorrectionDisabled(true) + // Autocorrect left enabled for chat typing (#969). Nicknames + // and geohashes keep `.autocorrectionDisabled(true)` elsewhere. #if os(iOS) .textInputAutocapitalization(.sentences) #endif From 22386a42b8062e1be2a74675c1eb321eae151ec8 Mon Sep 17 00:00:00 2001 From: Taksh Date: Wed, 29 Jul 2026 11:43:15 +0300 Subject: [PATCH 2/6] fix: make composer autocorrect token-aware Enable autocorrect for prose, but disable it while the current token starts with /, @, or # so commands, mentions, and channels stay exact and aren't fed into keyboard learning. --- bitchat/Utils/ComposerAutocorrect.swift | 45 +++++++++++++++++++++ bitchat/Views/ContentComposerView.swift | 14 +++++-- bitchatTests/ComposerAutocorrectTests.swift | 33 +++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 bitchat/Utils/ComposerAutocorrect.swift create mode 100644 bitchatTests/ComposerAutocorrectTests.swift diff --git a/bitchat/Utils/ComposerAutocorrect.swift b/bitchat/Utils/ComposerAutocorrect.swift new file mode 100644 index 00000000..8a20d0b7 --- /dev/null +++ b/bitchat/Utils/ComposerAutocorrect.swift @@ -0,0 +1,45 @@ +// +// ComposerAutocorrect.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +/// Token-aware autocorrect policy for the message composer (#969). +/// +/// Autocorrect helps with prose but fights `/commands`, `@mentions`, and +/// `#channels`. Disable it while the token under the cursor starts with one +/// of those sigils; leave it on otherwise. The composer TextField only exposes +/// the text (not a live selection), so callers pass the caret — typically the +/// end of the string, matching autocomplete. +enum ComposerAutocorrect { + /// Sigils that mean "exact token, don't rewrite me". + static let specialPrefixes: Set = ["/", "@", "#"] + + /// Whether `.autocorrectionDisabled` should be on for this caret position. + static func shouldDisable(for text: String, cursorPosition: Int) -> Bool { + let token = currentToken(in: text, cursorPosition: cursorPosition) + guard let first = token.first else { return false } + return specialPrefixes.contains(first) + } + + /// The whitespace-delimited token containing `cursorPosition` (or ending + /// at it when the caret sits on a boundary). + static func currentToken(in text: String, cursorPosition: Int) -> String { + guard !text.isEmpty else { return "" } + let clamped = min(max(0, cursorPosition), text.count) + let end = text.index(text.startIndex, offsetBy: clamped) + let before = text[.. Date: Fri, 31 Jul 2026 15:29:24 +0300 Subject: [PATCH 3/6] 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 { From d1ce261d935e3256e3a5d1cb20db3fe82751ef86 Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 31 Jul 2026 15:29:51 +0300 Subject: [PATCH 4/6] docs: note mid-string caret limitation for token-aware autocorrect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callers that only pass text.count still autocorrect inside earlier sigil tokens — track as a v1 limit until selection is available. Signed-off-by: Taksh --- bitchat/Utils/ComposerAutocorrect.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bitchat/Utils/ComposerAutocorrect.swift b/bitchat/Utils/ComposerAutocorrect.swift index 8a20d0b7..51d2a90c 100644 --- a/bitchat/Utils/ComposerAutocorrect.swift +++ b/bitchat/Utils/ComposerAutocorrect.swift @@ -15,6 +15,10 @@ import Foundation /// of those sigils; leave it on otherwise. The composer TextField only exposes /// the text (not a live selection), so callers pass the caret — typically the /// end of the string, matching autocomplete. +/// +/// Mid-string caret is a known v1 limitation: when callers only know +/// `text.count`, moving the caret back into an existing `/` `@` `#` token +/// still leaves autocorrect on. Plumb selection through when SwiftUI exposes it. enum ComposerAutocorrect { /// Sigils that mean "exact token, don't rewrite me". static let specialPrefixes: Set = ["/", "@", "#"] From 6f43dcadbcb6f0c21321c74d1cf17c8da40baac9 Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 31 Jul 2026 17:47:31 +0300 Subject: [PATCH 5/6] fix: drop dead autocorrect helper and invalidate trait timer Periphery flagged shouldDisableAutocorrect after the debounce path switched to appliedAutocorrectDisabled. Also cancel autocorrectTraitTimer on disappear so a pending tick cannot fire after the view is gone. --- bitchat/Views/ContentComposerView.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bitchat/Views/ContentComposerView.swift b/bitchat/Views/ContentComposerView.swift index eb975203..fe777724 100644 --- a/bitchat/Views/ContentComposerView.swift +++ b/bitchat/Views/ContentComposerView.swift @@ -160,17 +160,12 @@ struct ContentComposerView: View { .themedChromePanel(edge: .bottom) .onDisappear { autocompleteDebounceTimer?.invalidate() + autocorrectTraitTimer?.invalidate() } } } private extension ContentComposerView { - /// Mirror autocomplete's end-of-string caret: SwiftUI's TextField doesn't - /// expose selection, and suggestions already assume the caret is at the end. - var shouldDisableAutocorrect: Bool { - 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) {