Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions bitchat/Utils/ComposerAutocorrectPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

/// When the composer should suppress system autocorrect.
///
/// `/commands`, `@mentions`, and `#geohash` tokens need the exact typed text
/// for autocomplete. Prose should still get autocorrect. The token under the
/// cursor is the substring after the last whitespace before `cursor`.
enum ComposerAutocorrectPolicy {
static func shouldDisable(for text: String, cursor: Int) -> Bool {
guard let first = currentToken(in: text, cursor: cursor).first else { return false }
return first == "/" || first == "@" || first == "#"
}

static func currentToken(in text: String, cursor: Int) -> String {
guard !text.isEmpty else { return "" }
let clamped = max(0, min(cursor, text.count))
let idx = text.index(text.startIndex, offsetBy: clamped)
let before = text[..<idx]
if let lastWhitespace = before.lastIndex(where: { $0.isWhitespace }) {
return String(before[before.index(after: lastWhitespace)...])
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize punctuation-adjacent mention and geohash tokens

When a valid token follows punctuation instead of whitespace, such as hello,@al or (#u4pr, this returns the entire whitespace-delimited run and its first character is not @ or #, so autocorrection is enabled. The existing mention and hashtag regexes recognize these substrings without requiring preceding whitespace, meaning this newly enables corrections for supported autocomplete/formatting tokens; locate the marker within the run or use the same token-boundary rules as those parsers.

Useful? React with 👍 / 👎.

}
return String(before)
}
}
7 changes: 6 additions & 1 deletion bitchat/Views/ContentComposerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ struct ContentComposerView: View {
.bitchatFont(size: 15)
.foregroundColor(palette.primary)
.focused(isTextFieldFocused)
.autocorrectionDisabled(true)
.autocorrectionDisabled(
ComposerAutocorrectPolicy.shouldDisable(
for: messageText,
cursor: messageText.count
)
Comment on lines +80 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the text field's real cursor location

When a user moves the caret back into a token that is not at the end of the message—for example, editing Please notify @al tomorrow at @al—this always supplies the end-of-message position. The policy therefore examines tomorrow, enables autocorrection, and can rewrite the mention or geohash that this change is meant to preserve. Track the text field selection/insertion point (or apply a conservative fallback) instead of using messageText.count.

Useful? React with 👍 / 👎.

)
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
Expand Down
26 changes: 26 additions & 0 deletions bitchatTests/ComposerAutocorrectPolicyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Testing
@testable import bitchat

struct ComposerAutocorrectPolicyTests {
@Test func proseLeavesAutocorrectEnabled() {
#expect(!ComposerAutocorrectPolicy.shouldDisable(for: "hello there", cursor: 11))
#expect(!ComposerAutocorrectPolicy.shouldDisable(for: "", cursor: 0))
#expect(!ComposerAutocorrectPolicy.shouldDisable(for: "hello ", cursor: 6))
}

@Test func commandMentionAndGeohashTokensDisableAutocorrect() {
#expect(ComposerAutocorrectPolicy.shouldDisable(for: "/msg", cursor: 4))
#expect(ComposerAutocorrectPolicy.shouldDisable(for: "hi @al", cursor: 6))
#expect(ComposerAutocorrectPolicy.shouldDisable(for: "see #u4pr", cursor: 9))
}

@Test func completedCommandArgumentIsProseAgain() {
#expect(!ComposerAutocorrectPolicy.shouldDisable(for: "/msg alice", cursor: 10))
}

@Test func currentTokenIsTheRunBeforeTheCursor() {
#expect(ComposerAutocorrectPolicy.currentToken(in: "hello /j", cursor: 8) == "/j")
#expect(ComposerAutocorrectPolicy.currentToken(in: "hello ", cursor: 6) == "")
#expect(ComposerAutocorrectPolicy.currentToken(in: "/msg", cursor: 2) == "/m")
}
}
Loading