* Quality pass on the 1.7.0 batch: fix confirmed bugs, bump to 1.7.1
Post-merge review of PRs #1400–#1417 (push-to-talk, mesh bridging, DM
store-and-forward, empty-mesh liveliness, geo-notes). Fixes the confirmed,
well-scoped findings; deeper architectural/security items are tracked
separately.
- PTT hot-mic leak: releasing the mic during VoiceCaptureSession.start()'s
150ms retry pause left the mic live and streaming for up to 120s, because
cancel() no-op'd once `completed` was set. Bail after the sleep if the hold
was released, and make cancel() always tear down a late-started capture.
- Bridge courier depositDrop reported success and burned the dedup slot before
the drop was actually published (evicted/compose-fail = lying 📦 "carried"
with no retry). Only consume publishedDropKeys on durable accept; add
BoundedIDSet.remove() to release evicted/failed slots (uses the dead dedupKey).
- Blocked senders resurfaced via archived "heard here earlier" echoes, the one
path that bypassed the live block filter — filter at seed time.
- A late optimistic .sent clobbered the router's .carried state; extend
ConversationStore.shouldSkipStatusUpdate to a full precedence guard
(sending < sent < carried < delivered < read).
- Read receipts were permanently burned when the router dropped them (marked
sent then dropped). sendReadReceipt/routeReadReceipt now return Bool; only
record as sent on a successful route, else retry on the next read scan.
- MessageRouter.cleanupExpiredMessages() had no production caller, so DMs to a
peer that never reconnects sat on .sending until relaunch — run it in the
120s bridge sweep.
- Sightings tally now rolls over at midnight while idle; wave notification
action localized across all 29 locales; bridged anon#tag uses suffix(4) like
everything else; makeThrowawayIdentity delegates to NostrIdentity.generate();
.swiftlint.yml excludes .claude worktrees.
- Add regression tests: carried→sent no-downgrade, carried→delivered upgrade,
evicted pending drop stays retryable.
- Bump MARKETING_VERSION to 1.7.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fix CI: adjust delivery-status benchmark and drop now-dead addSystemMessage
The stricter no-downgrade guard (delivered/carried never regress to sent) broke
two things the earlier commit didn't catch locally (perf tests are skipped in
the default run, and Periphery runs only in CI):
- PerformanceBaselineTests delivery benchmarks alternated sent <-> delivered
assuming both directions apply; the delivered -> sent half is now correctly
skipped, so the pass measured 0 updates. Alternate two delivered timestamps
instead — every update is real, no downgrade.
- Routing the geoDM "not in a location channel" error into the thread removed
the only caller of ChatPrivateConversationContext.addSystemMessage, leaving
it (and its mock) dead per Periphery. Drop the protocol requirement, the mock
impl, and the now-vacuous systemMessages.isEmpty assertions (the invariant is
compile-time enforced: the context can no longer emit a public system line).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address review findings: read-receipt dedup, carried-vs-sending, block-time echo purge
- Read receipts: claim the receipt in sentReadReceipts synchronously
before spawning the routing task (chat open runs two read scans in one
MainActor stretch, so the async insert let every unread message route
twice), and release the claim when the route fails so the
retry-on-failed-route behavior is preserved.
- Delivery status: extend the no-downgrade guard so the `.sending`
stamp a pre-handshake resend emits can no longer clobber
carried/delivered/read (the 📦 indicator survived `.sent` but not
`.sending`).
- Archived echoes: blocking a peer now purges their carried public
messages from the gossip archive at block time (UnifiedPeerService
and /block), while the fingerprint-to-peerID mapping is still known —
the seed-time filter can't resolve offline non-favorite strangers and
stays only as defense-in-depth. New Transport hook (default no-op) +
GossipSyncManager.removePublicMessages with immediate persist.
- Bridge courier: an envelope that can't encode within the drop size
caps fails identically on every attempt; consume the dedup slot so
the 120s retry sweep stops re-running Noise sealing on it.
- MeshSightingsTracker: cache the day-key DateFormatter instead of
building one per call.
Tests: double-markAsRead dedup + failed-route retry, carried→sending
no-downgrade matrix, block-time purge (manager + service wiring),
oversize-drop slot consumption.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Also skip the sent→sending downgrade in the delivery-status guard
Codex review follow-up: sendPrivateMessage without an established Noise
session emits `.sending` asynchronously, so it can land after the
message already reached `.sent` and visibly walk "Sent" back to
"Sending...". Treat `.sending` as weaker than `.sent` too — the status
was already truthful. `.failed` → `.sending` stays allowed so a retry
after a real failure remains visible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Retain Codable properties in Periphery scan (same fix as #1421)
The noiseKey assign-only false positive fired persistently on this branch
(twice, including a rerun) despite the baselined USR. Byte-identical to the
fix on fix/announce-replay-link-steal so the branches merge cleanly in
either order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Empty-mesh liveliness: nearby conversations, echoes, wave action, dead drops, radar
The empty mesh timeline was a dead end: a grey zero and "nobody in range
yet". This turns it into a live surface and gives the app pull when the
mesh wakes up:
- Nearest conversation: background geohash sampling now tracks actual
chat messages (not just presence) per regional channel; the empty state
surfaces the busiest nearby conversation with a preview, one tap to
join (GeohashChatActivityTracker, fed from GeoPresenceTracker).
- Echoes: the carried 6h store-and-forward window renders as dimmed
"heard here earlier" rows at launch (new Transport
collectArchivedPublicMessages -> GossipSyncManager snapshot, decoded
with signature-derived nicknames; content-identity dedup guards
against re-synced duplicates).
- Wave: the "bitchatters nearby" notification gains a "wave" quick
action that broadcasts a mesh 👋 straight from the notification, even
backgrounded (first UNNotificationCategory in the app).
- Dead drops: /drop pins a note to the current building geohash as a
kind-1 location note with a 24h NIP-40 expiry; expired notes are now
dropped client-side at ingest; the notices sheet shows "fades in Xh";
a "location notes" toggle plus location-permission controls live in
app info (also fixes the duplicated Voice section).
- Radar: an ambient sonar animation shows the radio scanning, with a
privacy-safe daily tally ("N devices passed within range today" via
salted per-day hashes) and a "notes left here" hint that opens the
notices geo tab.
All new user-facing strings ship in all 29 locales. 1403 tests green,
including new suites for the activity tracker, sightings tally, and
note expiry/drop publishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review round: app-info polish, urgent/expiry parity, centered radar, pin fill, Codex P2s
- App info: LOCATION header uppercased like sibling sections; location
notes and live voice descriptions shortened (29 locales); redundant
"location access granted" line removed.
- Notices parity: urgent + expiry controls now show on the geo tab too;
the bridged Nostr note carries ["t","urgent"] and NIP-40 so relay-side
readers see both; urgent parsed back from incoming notes.
- Radar moved from the top of the empty state to the center of the chat
area, below the help text (empty state fills the visible height).
- Header pin fills whenever the scope has notices (was: only unseen),
and Nostr-only nearby notes now light it too.
- Codex P2 fixes: notification completion deferred until the wave action
is handled (background suspension dropped the send); NIP-40 notes now
prune on a timer when they expire while displayed; the location-notes
kill switch retargets the nearby-notes counter immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Hide macOS segmented picker's built-in label in notices composer (duplicate 'expires in')
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Geo notes: permanent (∞) expiry default, urgent stays mesh-only
- Geo expiry picker gains ∞ as the default: a permanent note posts as a
pure relay note (no NIP-40 tag, no mesh-board copy — a board copy must
fade within days, contradicting the ∞ the user picked). 1/3/7d keep
the board + bridged-note path with NIP-40.
- The notes manager is now owned by the notices sheet (not the list) so
the composer local-echoes ∞ notes into the list; it revives via
refresh() after a tab-switch cancel, and its expiry-prune timer
survives cancel (weak self, dies with the instance).
- Urgent toggle returns to mesh-only per review — notes are ambient;
the read-side urgent-tag parse stays so tagged notes still render.
- macOS: hide the segmented picker's built-in label (duplicate
"expires in").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Clearing the mesh timeline dismisses echoes for good; tighter divider copy
Triple-tap /clear emptied the timeline but the next launch re-seeded
"heard here earlier" from the persisted archive. A MeshEchoSettings
watermark now records the clear; only messages heard after it come back
(the archive itself still carries everything for peers' sync). The
echo dedup keys reset with it, and panic wipe drops the watermark.
Divider copy tightened to "heard here earlier · last 6h" (29 locales).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Echoes visual polish: tinted history block, radar-captioned tally, ambient footer
Device-test feedback on the echoes screen:
- Archived echoes now sit on a subtle tinted background (secondary at
8%) in addition to the dim, so "heard here earlier" reads as one
distinct block; the divider carries the echo ID prefix to join it.
- "N devices passed within range today" moves out of the narration
lines to sit centered under the radar as its caption.
- When the timeline holds only echoes/system lines, a compact ambient
footer (small radar + tally + live hints) renders below the history
instead of the whole ambient layer vanishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Notes strip persists above the mesh chat; leaner empty-state narration
- The 📍 "notes left here" line was empty-state-only, so starting a
conversation hid it. It is now a tappable strip pinned above the mesh
timeline whenever unexpired notes exist at this place (opens the
notices geo tab); the nearby-notes counter runs for the whole mesh
timeline, not just the empty state.
- Empty state narration drops "nobody in range yet..." (the radar and
the sightings caption already say it) and the nearby-conversation
hint moves below the help line instead of splitting the narration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Radar means searching: hide the sweep once mesh peers are connected or reachable
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Mechanical style fixes across the enabled rule set, mostly via
swiftlint --fix (trailing_comma, comma, colon, trailing_newline,
comment_spacing, unused_closure_parameter, unneeded_break_in_switch,
opening_brace) plus hand fixes:
- non_optional_string_data_conversion (45): .data(using: .utf8)! and
?? Data() fallbacks replaced with the non-optional Data(_.utf8),
including two production sites (NIP-44 HKDF info constant and the
announce canonicalization context/nickname bytes — byte-identical
output, only the impossible-nil handling is gone).
- switch_case_alignment: LocationChannel had a misindented closing
brace; also repaired an --fix artifact in BLEService's .none case.
- redundant_string_enum_value: TrustLevel raw values equal to the case
names (encoded form unchanged).
- unused_optional_binding: let _ = binds replaced with != nil / is Bool.
- static_over_final_class: PreviewView.layerClass.
- Resolved the BinaryProtocolTests TODO by documenting that 8-byte
recipient ID truncation is the fixed wire-field size, not a bug.
The 4 remaining violations are all todo markers for a shared
test-helpers module (tracked in #1088) and one Reuse note.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Expand coverage for relay, identity, and location flows
* Fix macOS SwiftPM CI failures
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* fix: Rate limit iOS peer notifications to prevent flood
- Remove aggressive formIntersection that cleared recentlySeenPeers
when peers temporarily dropped, causing them to be treated as "new"
- Add 5-minute cooldown between notifications (aligns with Android)
- Use fixed notification identifier so iOS updates existing notification
instead of creating new ones
- Only mark peers as seen when notification is sent, so peers arriving
during cooldown are included in the next notification
Fixes notification spam every 10-30 seconds when peers fluctuate.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: Bump version to 1.5.1
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add checks for GITHUB_ACTIONS, CI, and XCTestBundlePath environment
variables to reliably detect test/CI environments where notification
APIs should be skipped.
Remove Bundle.main.bundleIdentifier == nil from isRunningTests
detection as it may have unintended side effects in CI environments.
The XCTestCase class check and XCTestConfigurationFilePath environment
variable are sufficient for detecting both XCTest and Swift Testing.
* Simplify validation, compression heuristics, and notification scheduling
* Consolidate notification logic and add InputValidator monitoring
Follow-up improvements to address PR feedback:
1. Consolidate notification functions
- Add interruptionLevel parameter to sendLocalNotification
- Refactor sendNetworkAvailableNotification to use consolidated function
- Removes 12 lines of duplicate code
2. Add monitoring to InputValidator
- Log control character rejections for production monitoring
- Privacy-preserving: logs length + count, not actual content
- Uses .security category for proper log routing
3. Add comprehensive InputValidator tests
- 28 test cases covering validation, control characters, unicode, edge cases
- Ensures behavioral changes are well-tested and documented
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
* UI: replace textual 'close' with X icon\n\n- AppInfoView (iOS): use xmark icon in nav bar to match Location Notes style.\n- LocationChannelsSheet: use xmark icon for close on iOS/macOS toolbars; add accessibility label.
* Location Notes: prefix usernames with @ and lighten #geohash\n\n- Show @ before usernames in notes list.\n- Split header into '@' and '#geohash' and color the geohash with secondary green for consistency.
* Header spacing: add breathing room between channel badge, notes button, and people count\n\n- Add trailing padding after #mesh/#geohash badge.\n- Add leading padding before notes button and people counter to improve readability.
* Header: nudge #mesh/#geohash badge right with leading padding
* Location Notes + Header polish\n\n- Header: add space in '@ #geohash' and use darker green for geohash.\n- Notes list: render '@name#abcd' with darker green for #abcd to match chat.\n- Header: move geochat bookmark icon after #geohash badge with consistent spacing.
* Location Notes: @name regular green, #abcd darker; nudge #hash\n\n- Render '@' and base name in regular green, suffix '#abcd' in darker green.\n- Add extra left padding before '#geohash' in notes header.\n- Increase leading padding for channel badge to push #mesh/#geohash further right.
* Fix notes icon color: subscribe/count at block-level geohash\n\n- Use block (precision 7) geohash for notes: when opening sheet, on channel changes, and when subscribing the counter.\n- Aligns with LocationNotesManager which publishes at street-level geohash, allowing the counter to detect notes and turn icon blue.
* Header spacing: move #mesh/#geohash closer to notes/bookmark\n\n- Reduce trailing padding on channel badge and leading padding on notes/bookmark to cluster them together.\n- Keeps larger gap before people count for readability.
* Notes: standardize on building-level (8 chars) for publish/read\n\n- Use .building geohash when opening notes, reacting to channel updates, and subscribing the counter.\n- Update comments to reflect building-level scope.
* Location Channels sheet: use black sheet background like other sheets\n\n- Add backgroundColor and apply to container and list.\n- Hide list default background with scrollContentBackground(.hidden).
* Notes icon: use green when notes exist (matches app green)
* Location Channels: boxed 'bookmarked' section; keep 'remove location access' outside box\n\n- Wrap bookmarked list in a rounded, subtle grey box within the list.\n- Ensure the 'remove location access' button is not inside the box and clears list row background.
* Notes counter UX: avoid grey flicker when closing sheet\n\n- Preserve last count during resubscribe to prevent brief 0 state.\n- Keep existing subscription when building geohash temporarily unavailable; only cancel if none or permission revoked.
* Notes counter: unsubscribe without clearing count on resubscribe\n\n- Avoid calling cancel() in subscribe; just unsubscribe old sub to prevent wiping count to 0.\n- Prevents green→grey flicker after closing sheet or location updates.
* Notes icon: use sheet count until counter finishes initial load; don’t zero on sheet close\n\n- Compute hasNotes using max(count, sheetCount) while initialLoadComplete is false.\n- Remove sheetNotesCount reset on sheet disappear to avoid transient grey.
* Location Notes header: remove extra gap before #geohash (drop leading padding)
* Notes sheet: color #abcd suffix as darker green via opacity (match chat)
* Notes sheet: show timestamp in brackets; drop #abcd from @name
* chore: commit remaining local changes
* Header: move notes + bookmark to left of #mesh/#geohash
* Header spacing: tighten gap between #mesh/#geohash and peer count
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
- Use minimal-distance hue palette for mesh and geohash lists; align chat sender colors with list palette.
- Add foreground geo notifications for different channels; deep-link to bitchat://geohash/<gh>; per-geohash 60s cooldown; respect self/blocks.
- Global geohash sampling runs outside the sheet; delegate handles deeplinks and suppresses when already in-channel.
- Switch location channel sheet to continuous CoreLocation with 21m distance filter; add config knob.
- Only link geohash hashtags when standalone (no @name#abcd or word#abcd).
- Include mesh-reachable peers in mesh counts and in "bitchatters nearby" notification.
- Add TransportConfig knobs for palette and geo notifications.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
- Don't show PM notifications when the private chat is already open
- Store peer ID in notification userInfo for reliable chat opening
- Tap notification to open the correct private chat using peer ID
- Check both in ChatViewModel (when receiving) and NotificationDelegate (when presenting)
- Completely remove UIApplication/NSApplication state checks
- Let NotificationDelegate handle foreground presentation
- All notifications now sent regardless of app state
- Fixes all thread checker warnings
- Move all app state checks inside DispatchQueue.main.async
- Ensures NSApplication.isActive is only accessed from main thread
- Prevents thread checker warnings on macOS
- Favorite notifications now bypass sendLocalNotification to avoid thread issues
- All notification code properly wrapped in DispatchQueue.main.async
- Added logging for favorite notifications
- Network and favorite notifications now work independently
- Wrap UIApplication.applicationState access in DispatchQueue.main.async
- Network notifications bypass app state check entirely
- Added more detailed logging for debugging
- Notifications now handled properly from background threads
- Network notifications now show even when app is in foreground
- Added unique identifiers to prevent iOS deduplication
- Set interruptionLevel to timeSensitive for prominence
- Added comprehensive logging throughout notification flow
- Improved error handling for notification permissions
- Show notification when network transitions from empty to having peers
- Single notification per session, resets when network becomes empty
- Background Bluetooth modes enabled for iOS
- Generic message: 'bitchatters nearby\! 1 person around' or 'X people around'
- Changed system messages from green to grey with consistent 12pt font
- Fixed text wrapping to flow naturally under timestamps
- Changed default nickname to anonXXXX format
- Replaced text with icon representations in status bar
- Added icons to sidebar section headers
- Made autocomplete UI consistent between commands and @mentions
- Added welcome message for new users (3 second delay)
- Changed sidebar header to 'YOUR NETWORK'
- Added command aliases (/join, /msg)
- Implemented /hug and /slap commands with haptic feedback
- Improved command help display with alphabetization
- Renamed 'rooms' to 'channels' throughout entire codebase
- Fix NotificationService warnings by replacing unused error parameters with _
- Add public domain header to NotificationService.swift
- Fix BluetoothMeshService compilation errors:
- Replace removeAll with filter for processedKeyExchanges cleanup
- Remove duplicate cleanupStalePeers function declaration
- Remove duplicate peerLastSeenTimestamps property declaration
- All code now compiles cleanly for both iOS and macOS targets
- Added public domain headers to all test files
- Updated Info.plist with required App Store keys:
- ITSAppUsesNonExemptEncryption = NO
- LSApplicationCategoryType = Social Networking
- UIRequiresFullScreen = YES
- Created proper Assets.xcassets structure
- Configured AppIcon.appiconset with all icon references
- Removed last TODO comment
- Created comprehensive App Store submission checklist
- Updated project.yml to include Assets.xcassets
The app is now ready for App Store submission. All debug code has been removed,
icons are configured, and privacy/security compliance is documented.
- Add tracking to prevent sending cached messages multiple times to the same peer
- Only cache messages for offline favorites, not online ones
- Sort messages by timestamp when adding to ensure proper chronological order
- Clean up tracking sets when peers disconnect to allow re-sending on reconnect
- Cache our own messages when sending to offline favorites
- Update favorite online notification text to "wanna get in there?"
- Add macOS support to NotificationService
- Use NSApplication.shared.isActive to check app state on macOS
- Move notification logic outside of iOS-only block in ChatViewModel
- Notifications now work for both mentions and private messages on macOS
- Keep haptic feedback iOS-only as it's not available on macOS
- Fix mic button gesture handling by removing Button wrapper
- Use direct gesture on ZStack for more reliable touch detection
- Add debug logging for recording start/stop
- Fix notification delivery on iOS
- Change from checking .background to checking != .active
- Notifications now work when app is inactive or background
- Add NSUserNotificationsUsageDescription to Info.plist
- Improve voice note fragment transmission
- Reduce fragment size from 400 to 200 bytes for better reliability
- Simplify fragment timing from batch-based to linear 100ms delays
- Lower packet size threshold from 512 to 400 bytes for fragmentation
- Add peripheral connection state checking before writes
- Better error logging throughout transmission pipeline
- Add comprehensive debug logging for troubleshooting
- Log audio file size and duration
- Log peripheral connection states during broadcast
- Log fragment send progress with timing info
- Add favorites functionality for peers with persistent storage
- Star icon to toggle favorites in sidebar
- Favorites appear at top of peer list
- Storage persists across app launches using peer ID
- Add local notifications for mentions and private messages
- Notifications appear when app is in background
- Separate notification types for mentions vs private messages
- Request notification permissions on app launch
- Fix sidebar header alignment to match main toolbar (44pt)
- Add background color to sidebar header for visual consistency
- Improve voice note fragment transmission
- Send fragments in batches of 5 with delays to prevent congestion
- Better logging to debug fragment transmission issues
- Reduce delay between fragments from 50ms to batch-based timing
- Fix other UI issues
- Ensure mic recording ripples originate from mic button
- Update sidebar to swipe from right edge smoothly