Simplifies geonotes architecture by eliminating all background subscriptions:
- Delete LocationNotesCounter.swift (104 lines) and related tests (58 lines)
- Remove background subscription system entirely
- Icon is now constant darker orange (indicates availability, not state)
- Subscription ONLY happens when user explicitly opens the sheet
- Total: ~220 lines of code removed
Privacy Benefits:
- Zero network traffic until user action
- No location leaking to relays via background polling
- User must explicitly opt-in to discover notes
The icon (note.text in orange) simply indicates the feature is available
when location permission is granted. Notes are only fetched when the
sheet is opened, prioritizing privacy over convenience.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Align DM sheet toolbar with people list
* Gate stale gossip announcements
* Remove stale peer messages during gossip cleanup
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Extract each type to a separate file
* Nostr ID Bridge: Convert static func/vars to instance
* `KeychainHelper` behind a protocol to easily mock
* Update tests with ID Bridge and MockKeychainHelper
---------
Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com>
* Location notes: fix performance and UI issues
Performance fixes:
- Add Set-based duplicate detection (O(1) vs O(n) lookup)
- Eliminates lag when receiving 200+ notes during EOSE
Correctness fixes:
- Fix optimistic echo timestamp to match signed event timestamp
- Add echo IDs to noteIDs set for consistency
UI improvements:
- Remove duplicate "loading recent notes" text in header
- Simplify toolbar icon color logic for immediate green indication
- Icon now reliably turns green when notes exist in geohash
Tests: All 3 LocationNotes tests passing
* Location notes: add remaining robustness fixes
Additional improvements:
- Align counter/manager limits to 200 (prevents showing count higher than displayable)
- Set loading state before clearing notes to eliminate UI flicker on geohash change
- Add geohash validation for building-level precision (8 valid base32 chars)
- Add defensive 500-note memory cap with automatic trimming
- Clear stale notesGeohash state on sheet dismiss
Tests:
- Fix test geohashes to use valid base32 characters
- All 3 LocationNotes tests passing
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Fix test suite peer ID collisions
Use unique peer IDs for each test suite to prevent global registry
collisions when Swift Testing runs suites in parallel.
- PrivateChatE2ETests: PRIV_* prefix
- PublicChatE2ETests: PUB_* prefix
- Update all peer ID references to use actual instance IDs
This fixes the race condition where simplePublicMessage() was receiving
duplicate deliveries due to registry contamination.
* Add Bluetooth permission & state alerts on launch and foreground
Wire up existing Bluetooth alert infrastructure to show notifications
when Bluetooth is off, unauthorized, or unsupported.
Changes:
- Add didUpdateBluetoothState() to BitchatDelegate protocol
- BLEService now notifies delegate when Bluetooth state changes
- ChatViewModel implements delegate method to show alerts
- Check Bluetooth state on app launch (after 100ms delay)
- Check Bluetooth state when app comes to foreground
- Add getCurrentBluetoothState() method to BLEService
The UI alert already existed but wasn't wired up. Now users will see
appropriate alerts for:
- Bluetooth turned off
- Bluetooth permission denied
- Bluetooth unsupported on device
Alert includes a button to open Settings on iOS.
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Use unique peer IDs for each test suite to prevent global registry
collisions when Swift Testing runs suites in parallel.
- PrivateChatE2ETests: PRIV_* prefix
- PublicChatE2ETests: PUB_* prefix
- Update all peer ID references to use actual instance IDs
This fixes the race condition where simplePublicMessage() was receiving
duplicate deliveries due to registry contamination.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Swift Testing: `PrivateChatE2ETests` + minor refactor
* Swift Testing: `PublicChatE2ETests`
* Swift Testing: `FragmentationTests`
* Fix MockBLEService init to accept PeerID and remove _testRegister call
* Add peerID property to MockBLEService and fix ttlDecrement test
* Remove duplicate peerID property and fix type comparisons
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Noise types use PeerID
* Fix tests
* Extract `NoiseSessionManager` into a separate file
* Extract `NoiseSessionState` into a separate file
* Remove `failed` state from `NoiseSessionState`
* Extract `NoiseSessionError` into a separate file
* PeerID 12/n: `GossipSyncManager`
* Noise types use PeerID
* Fix tests
* Extract `NoiseSessionManager` into a separate file
* Extract `NoiseSessionState` into a separate file
* Remove `failed` state from `NoiseSessionState`
* Extract `NoiseSessionError` into a separate file
* Move LocalizationCatalogTests out of Localization/
SPM is treating all the files under Localization as a resource as per the Package.swift, hence it’s not even building it
* Use a class object vs struct to fix build issue
* Explicitly check that the output is not a l10n key
* Remove skipped test
- Replace 474/474 sync divergence with a single descriptive commit
- Keep only intended localization test improvements vs main
- Ensure readable history for code review and future merges
* fix(test): update LocationNotesManagerTests to expect localization key
The tests were failing because in the test environment, String(localized:) returns
the localization key instead of the actual localized value. Updated the assertions
to expect 'location_notes.error.no_relays' instead of the English translation
'no geo relays available near this location. try again soon.'
* Fix LocationNotesManager test assertions for localization bundle
- Update test assertions to use String(localized:) to match manager behavior in test environment
- Fixes issue where tests expected localized text but manager returns raw keys in SPM test environment
- Both manager and tests now consistently handle localization bundle differences
- Resolves P1 issue: Keep LocationNotes error messages localized
- All 124 tests now pass after clean build
* SPM: process bitchat/Localizable.xcstrings in main target to fix CLI warning; keep tests' Localization resource.
* Automated update of relay data - Sun Sep 21 06:26:33 UTC 2025
* chore(l10n): add empty string catalogs
* chore(l10n): populate string catalogs from legacy resources
* test(l10n): add catalog guardrail suite
* chore(l10n): remove legacy localization files
* fix: Add localization resources to Package.swift targets
- Add .process("Localization") to bitchat target resources
- Add .process("Localization") to bitchatTests target resources
- Resolves Bundle.module resource loading for localization files
- Enables proper localization testing in Swift Package Manager builds
* feat: Add Korean localization and convert to UTF-8 format
Korean Language Support:
- Add complete Korean (ko) localization with 191 strings from PR #686
- Include all app strings: UI, features, system messages, alerts
- Include all share extension strings: status messages, errors
- Verified 100% translation coverage for Korean locale
UTF-8 Format Conversion:
- Convert 23,047 Unicode escape sequences to readable UTF-8 characters
- Transform \u sequences (e.g. \u0625\u063a\u0644\u0627\u0642) to native text (إغلاق)
- Improve maintainability across all 15 supported locales
- Preserve all existing translations while enhancing readability
Locales supported: en, ar, de, es, fr, he, id, it, ja, ko, ne, pt-BR, ru, uk, zh-Hans
* test: Enhance dynamic localization test framework
Dynamic Test Framework:
- Replace hardcoded locale tests with data-driven approach
- Add testLocalizationExpectedValues() for dynamic locale validation
- Add testConfiguredLocalesCompleteness() for coverage verification
- Tests now read configuration from PrimaryLocalizationKeys.json
Expanded Test Coverage:
- Increase from 14 to 33 key validations (135% increase)
- Add critical UI strings: common actions, app info, security, sharing
- Cover 364 total string validations across 15 locales
- Include Korean validation with native Korean expected values
Test Categories Added:
- Common UI: cancel, close, copy actions
- App Info: encryption, offline features, app name
- Bluetooth: permission and settings alerts
- Security: verification badges and actions
- Share Extension: all status and error messages
- Content Actions: accessibility and user actions
Maintains 100% test success rate across all supported locales.
* fix: Convert plural strings to correct xcstrings format
Three plural strings (content.accessibility.people_count,
location_channels.row_title, location_notes.header) were using
incorrect format causing runtime String(format:) errors.
Migrated from stringUnit.variations structure to proper
substitutions.variations format across all 14 languages.
* refactor(l10n): migrate to native String(localized:) APIs
Remove L10n.string wrapper in favor of Swift's native localization APIs.
Migrate 100+ localization call sites to use String(localized:) and String(localized:defaultValue:) with string interpolation.
- Update catalog to use interpolation syntax (\(var)) instead of format specifiers (%@)
- Migrate simple strings to String(localized:)
- Migrate strings with arguments to String(localized:defaultValue:) with interpolation
- Keep format strings for plural substitutions (String(format:locale:))
- Remove bitchat/Utils/Localization.swift
Net result: -407 insertions, +130 deletions across 15 files
* fix(l10n): correct interpolation to use format strings
Interpolation in String(localized:defaultValue:) doesn't work as expected -
the interpolation happens at the call site before localization lookup.
Convert dynamic strings to use String(format:String(localized:),args) pattern:
- Update catalog entries from \(var) syntax to %@ placeholders
- Wrap String(localized:) calls with String(format:locale:) for dynamic values
- Affects 17 strings across 6 files
This fixes UI showing literal "\(geohash)" text instead of actual values.
* chore(l10n): remove invalid catalog entries
Remove auto-extracted literal strings (@, #, ✔︎, @%@, bitchat/) that were
generated without proper localization structure. These caused test
decoding failures.
* fix(l10n): remove unused auto-extracted format string
Remove '%@/%@' key that was auto-extracted by Xcode but never used.
This key only existed in English causing locale parity test failures
across all 13 other languages.
Fixes locale parity tests - all 8 localization tests now pass with
only expected failures (incomplete translations in some locales).
* fix(l10n): copy format string to all locales for 100% completion
Add %@/%@ format string to all 14 non-English locales. Format strings
are locale-independent so using the same value everywhere is correct.
This brings all locales to 100% completion (189/189 strings) to prevent
Xcode from reporting incomplete translations when building.
* fix(l10n): prevent auto-extraction of UI literals
Use Text(verbatim:) for non-localizable UI elements:
- App branding ("bitchat/")
- Symbols (@, #, ✔︎)
- Dynamic usernames (@username)
- Count ratios (reached/total)
This prevents Xcode from auto-extracting these literals into the
String Catalog when building through Xcode GUI, which was causing
locales to show 96% completion instead of 100%.
* chore(l10n): remove auto-extracted UI literal entries
Delete 5 auto-extracted keys from catalog that are now using Text(verbatim:):
- @, #, ✔︎, %@, %@/%@
These were showing as stale/incomplete in Xcode causing 97% completion.
All locales now at 100% (188/188 strings).
* fix(l10n): prevent AttributedString from extracting @ symbol
Use string interpolation "\\(at)" instead of literal "@" in
AttributedString to prevent Xcode from auto-extracting it to the
String Catalog during build.
This was the last string causing locales to show 99% instead of 100%.
* fix(l10n): add %@ as non-translatable key in all locales
Mark %@ as non-translatable and add to all 15 locales with same value.
This prevents Xcode from showing incomplete translations when it
auto-extracts this format specifier during GUI builds.
All locales remain at 100% (189/189 strings).
* refactor: move Localizable.xcstrings to bitchat root
Move bitchat/Localization/Localizable.xcstrings to bitchat/ (after LaunchScreen)
and remove empty Localization directory.
* fix(test): update catalog path in localization tests
Update test paths from bitchat/Localization/Localizable.xcstrings to
bitchat/Localizable.xcstrings after moving the file.
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Extract BitchatMessage into a separate file
* Convert `fromBinaryPayload` to `convenience init?`
* Extract message dedup into an extension
* Remove dead `formatMessageContent`
* Minor refactor of timestamp and username formatting
* Remove dead `getSenderColor`
* Fix emote targeting and grammar; add tests
Prevent 'system' mis-target via peerID-derived display name in actions sheet. Correct /hug and /slap usage/error grammar by passing base command name. Improve geohash nickname resolution to match displayName with #suffix. Add CommandProcessor tests.
* Geohash ordering: strict in-order inserts and timestamp clamp
Use channel-aware late-insert threshold with 0s for geohash to keep strict chronological order. Clamp future Nostr event timestamps to 'now' to avoid future-dated items skewing order in geohash timelines.
* Trim trailing/leading spaces in geohash nicknames and tag emission
Sanitize Nostr 'n' tag values on ingest and emit by trimming whitespace/newlines to prevent trailing spaces in displayed usernames. Local nickname is already trimmed on focus loss and submit.
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Add geohash bookmarks: persistence, sampling integration, and UI\n\n- Add GeohashBookmarksStore with UserDefaults persistence and toggle API\n- Sample union of regional + bookmarked geohashes for activity notifications\n- LocationChannelsSheet: bookmark icons on nearby rows and a Bookmarked section\n- Header toolbar: toggle bookmark for current geohash\n- Tests: GeohashBookmarksStoreTests for normalization and persistence\n\nRationale: Bookmarked geohashes are always sampled for new activity notifications and quickly selectable from the sheet.
* Notifications: remove background 'new chats!' nudge; geohash activity only on zero→alive; mesh 'nearby' only on zero→alive\n\n- Geohash sampling: notify only when previous participant count in last 5m was zero, with 30s freshness gate\n- Suppress old sampled events after (re)subscribe\n- Remove channel inactivity nudge\n- Mesh: reset rising-edge gate immediately when peer list goes empty (strict zero→alive)
* Geohash notifications: ensure triggering message appears\n\n- On zero→alive sampling, pre-populate geoTimelines[gh] with the triggering message\n- Dedup on per-geohash timeline and UI messages by message ID to avoid duplicates after subscribe\n- Keeps participant update, block/self checks, and cooldown intact
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Remove dead code and artifacts: drop PeerManager, unused views/types; delete LegacyTestProtocolTypes; update .gitignore; purge TestResult.xcresult and build.log
* Tests: gate verbose prints under DEBUG; ChatViewModel: remove legacy fingerprint helper and rely on UnifiedPeerService
* Share Extension: migrate to UIKit + UTTypes; drop Social/SLComposeServiceViewController
* Remove 'preparing to share …' system message; send shared content immediately
* Inline comment cleanup: drop legacy 'removed' breadcrumbs across protocols, services, view model, and views
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* NIP-17/44: adopt NIP-44 v2 (XChaCha20-Poly1305, v2: base64url), switch rumor kind to 14, randomize BIP-340 aux; add XChaCha20Poly1305Compat and wire-up
* Nostr DMs: ensure delivered/read acks are sent even without Noise key mapping by falling back to direct Nostr (geohash-style) acks to sender pubkey
* NIP-44 v2 decrypt: try both Y parities for x-only sender pubkeys (even then odd) to fix unwrap authentication failures
* Tests: add NIP-44 v2 ACK round-trip tests (delivered/read) using bitchat1 embedding and gift-wrap decrypt path
* UI: fix DM autoscroll IDs by using dm:<peer>|<id> for private chat item IDs and preserve anchors, ensuring scrollTo targets exist when opening/switching/new messages
* UI: fix string interpolation in DM/geohash context keys (remove escaped interpolation) to resolve 'unused ch' warnings and ensure proper IDs
* UI: MeshPeerList shows transport state icon: radio for mesh-connected, purple globe for mutual favorite/Nostr; fallback dim person for others
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* Remove link previews: link URLs inline\n\n- Delete LinkPreviewView and all references\n- Strip preview usage from ContentView\n- Make detected URLs tappable via .link attribute\n- Add MessageTextHelpers for tokens/length checks\n- Wire helpers into iOS/macOS targets
* Peer list UX: match message colors, use map pin icon, stable ordering\n\n- Color list names and icons with same peer color as messages\n- Default icon mappin; teleported uses face.dashed (iOS)\n- Geohash: move teleported peers to bottom\n- Maintain stable order: append new peers, remove missing\n- Mesh list: replace state icons with colored mappin/person
* Fix peer list build errors: remove ambiguous Set type, avoid guard in ViewBuilder, remove duplicate bindings, and simplify person lookup
* Peer list: remove onChange expressions to avoid type-check issues; rely on ObservedObject updates
* Peer lists: simplify view structure to satisfy SwiftUI type checker (replace Group with VStack, flatten body)
* Peer lists: move ordering mutations to onAppear/onChange, keep body pure; fix result builder errors
* Fix: remove trailing modifiers outside if/else to avoid ViewBuilder type issues
* Color parity: fix seed cache to include color scheme; normalize Nostr seeds; switch to mappin.circle icon
* Peer colors: ensure exact match with message colors\n\n- Mesh list uses same seed logic as messages (getNoiseKeyForShortID fallback)\n- Color cache keyed by theme; normalize nostr seeds\n- Icons: use mappin.and.ellipse (default) and face.dashed for teleported\n- Teleport tag: accept "teleported" in addition to "teleport" and presence events
* Fix geohash color mismatch: populate nostrKeyMapping before building messages in resubscribe; also honor t,teleport tag for teleported state
* Message actions: username tap opens sheet via clickable AttributedString; message tap inserts @mention; rename to 'direct message'
* Cashu UX: suppress 'Show more' when message contains a Cashu token (chips handle rendering)
* Cashu detection: broaden regex (allow '.') + shorter min length; avoid long-text fallback when Cashu present so chips render and no full blob
* Geohash levels: rename region->province, country->region; update labels, precision, UI, manager mapping, tests; add Codable backward-compat for renamed cases
* Fix braces in LocationChannel.swift: close enum before extension to satisfy file-scope declarations
* Geohash links: make #geohash tappable and open that channel (bitchat://geohash/<gh>); handle in ContentView.onOpenURL with validation and selection
* Underline tappable #geohash mentions for clarity
* Geochat: sanitize timeline on channel switch to remove any blank-content entries (prevents blank rows)
* Xcode project: sync sources after UI changes (remove LinkPreview, add helpers, update build refs)
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>