mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +00:00
78a291ab77008dc045b063f8cafebc0d8f4d1e60
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
78a291ab77 |
Public-mesh push-to-talk: signed live voice bursts in the mesh channel (#1406)
* Live push-to-talk voice for DMs: stream while you talk, voice note as fallback Holding the mic in a DM now streams AAC frames live over the Noise session (walkie-talkie style, ~0.5s mouth-to-ear at one hop) while recording the same audio as a normal voice note. On release the note ships through the existing fileTransfer pipeline; receivers that heard the live stream absorb it silently into the same bubble (matched by the burst ID embedded in the file name), so reliability comes for free and nobody sees duplicates. Protocol: - NoisePayloadType.voiceFrame = 0x08 carrying VoiceBurstPacket (burstID + seq + START/data/END/CANCELED, length-prefixed AAC frames) - 210-byte burst-content budget keeps each Noise packet inside the 256-byte padding bucket: one BLE frame, never the fragment scheduler - fire-and-forget: frames are dropped (never queued) without an established session; live is only offered when the peer is mesh-reachable Receive: - ChatLiveVoiceCoordinator assembles bursts (jitter-ordered, 0.5s gap skip, 3s idle end, flood/size caps), persists progressively as ADTS .aac so even a partial burst is a replayable bubble - live autoplay only when the conversation is on screen, app active, and the new app-info "live voice messages" toggle is on (also gates live sending) - one-playback-at-a-time via a shared ExclusivePlayback slot Capture: - PTTCaptureEngine taps AVAudioEngine, dual-encodes: live AAC frames + the finalized .m4a (same 16kHz/mono/16kbps settings as VoiceRecorder) - VoiceRecordingViewModel now drives a pluggable VoiceCaptureSession; the composer HUD shows a pulsing LIVE treatment when streaming Includes the push-to-talk design doc, 6 new localization keys across all 29 locales, and unit tests for framing, packetizer budget, ADTS output, codec round-trip, and the assembly/absorb lifecycle. Public-mesh PTT (MessageType 0x29) lands separately on top of this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Public-mesh push-to-talk: signed live voice bursts in the mesh channel Extends live PTT from DMs to the public mesh timeline. Holding the mic in the mesh channel now broadcasts the burst live as signed voiceFrame packets (MessageType 0x29) while the finalized voice note still ships on release — new clients hear you as you speak and absorb the note silently into the live bubble; old clients (and late joiners) keep receiving the note exactly as before, so mixed-version meshes lose nothing. Wire/relay: - MessageType.voiceFrame = 0x29: ephemeral signed broadcast, never gossip-synced (SyncTypeFlags maps it to no bit), never padded (padding to the 512 block would push every ~490-byte signed packet into fragmentation) - RelayController treats voiceFrame like media fragments: dense-graph TTL clamp contains the sustained ~15 pkt/s per-talker stream, tight 8-25 ms jitter keeps multi-hop latency inside the receiver's 350 ms jitter buffer - inbound gate mirrors public messages: broadcast-only, 30 s freshness cap, packet signature verified against the claimed sender's announce before any audio reaches the UI App: - ChatLiveVoiceCoordinator gains burst scopes: public bubbles land in the mesh timeline, autoplay only while that timeline is on screen, and the finalized-note absorb is scope-bound (a public note can't replace a DM burst or vice versa) - floor courtesy: while someone talks live in the public channel the composer mic tints red and pulses, with an accessibility value naming the talker ("%@ is speaking", localized in all 29 locales); holding still works — a decentralized mesh has no floor arbiter, the tint just discourages talk-over Tests: relay policy (sparse cap + dense clamp), public bubble + talker indicator lifecycle, note absorption into the mesh store, and scope-binding rejection; full suite green (1382 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * PTT follow-ups from review + field test: peer-ID normalization, toggle gates inbound, drop-path diagnostics Codex review fixes (#1403): - makeVoiceCaptureSession normalizes the selected peer with toShort() before the reachability/session checks and binds the send target to that same routing ID — a conversation selected under the stable 64-hex Noise key no longer silently falls back to a classic note while the short-ID session is established - the live-voice toggle now gates inbound bursts too: off means classic-notes-only in both directions (no live bubble, partial file, or early notification; the finalized note still arrives), with a test Field-test diagnostics (first device run: DM frames decrypted but no bubble appeared, with no log evidence of which guard dropped them): - coordinator logs undecodable frames (size + hex prefix) and blocked drops - makeAssembly logs directory/file-handle failures instead of returning nil silently - PTTLiveVoiceSession logs capture start and finish (packet/frame/duration counts); PTTCaptureEngine logs engine start success/failure with the input format; BLEService.sendVoiceFrame logs no-session drops Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix iPhone live-capture failure: dead input unit (AURemoteIO -10851, 0 Hz) Field testing showed the phone's live capture failing at mic enable with AURemoteIO -10851 and an input format of 0 Hz / 2 ch — an input unit bound to an earlier (playback-only or settling) audio session. The Mac, which has no session lifecycle, captured fine, which is why public bursts from the Mac worked while phone-side sends degraded from working (first hold) to sporadic to dead across holds. Three layers of defense: - PTTCaptureEngine recreates its AVAudioEngine on every start(), after the session is configured, so the input unit binds to the session that is active now; a dead input (0 Hz or 0 channels) is now a distinct, logged error instead of a silent setup failure - PTTLiveVoiceSession retries the capture start once after a 150 ms route-settle pause - VoiceRecordingViewModel falls back to the classic VoiceRecorder within the same hold if the live engine still cannot start — a route glitch now costs the live stream, never the voice note Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Blue mic when the hold will stream live The mic button now shows readiness at a glance — and doubles as a build marker for device testing: - blue: holding will stream live (DM peer reachable with an established Noise session, or the public mesh channel) - accent (orange in DMs): holding records a classic voice note (no session yet, peer unreachable, or live voice toggled off) - red states unchanged (recording, floor busy) Refactors capture-backend selection into a single liveVoiceTarget() so the indicator and makeVoiceCaptureSession can never disagree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Revert the blue live-ready mic to the normal accent color The build-verification marker did its job; idle mic color goes back to the accent. The LIVE recording HUD remains the signal for whether a hold is streaming. Keeps the liveVoiceTarget() refactor so backend selection stays in one place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Leave a trace on every mic press and every inbound-frame drop Field testing read "tap does nothing" as breakage: the mic start is async (permission check + engine spin-up), so releasing before recording begins has always been a silent cancel — for classic voice notes too. Every press now logs which backend it chose and, for quick presses, that it released before recording started. Also logs the two remaining silent drops: inbound voice frames rejected by the live-voice toggle (the one unlogged guard left in the receive path) and the classic-note fallback now includes the toggle state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix mic hold dying instantly in DMs: sheet swipe gesture starved the composer Field logs showed every DM mic hold ending 3-10 ms after it began, on both platforms, while public-channel holds worked — the private sheet wraps its entire content (composer included) in a high-priority swipe-right-to-leave DragGesture, and a high-priority ancestor drag cancels the mic button's press-and-hold within milliseconds. Same starvation mechanism as the DM image-reveal bug (#1402), hitting a drag instead of a tap. The swipe-to-leave gesture now lives on the message list only, so the composer's gestures (mic hold, text field, buttons) are out of its reach and the swipe still works where users actually swipe. Also stops touching the capture engine when a hold cancels before the engine ever started: probing inputNode on a never-started engine instantiates its input unit against whatever session is active and spams benign-but-alarming AURemoteIO -10851 errors into field logs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reorder app info sheet: usage first, then settings, then reference New section order: HOW TO USE, then the adjustable bits (appearance, voice, network), then the reference material (features, privacy, symbols legend). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * App info: flow HOW TO USE into one paragraph; "list" and "person" wording The six how-to-use bullets now read as a single comma-separated paragraph (same instruction strings, legacy bullet prefix stripped at render). Two wording updates across all 29 locales: the people icon opens the "list" (not "sidebar"), and you tap a "person's" name (not a "peer's") to start a DM. 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> |
||
|
|
295f855b6f |
Harden REQUEST_SYNC and stop gossip-sync re-send loops (#1371)
* Harden REQUEST_SYNC and stop gossip-sync re-send loops Two fixes from an end-to-end review of the sync path: Efficiency: the GCS filter (400B, p=7) covers ~355 packet IDs, but stores hold up to 1000 messages + 600 fragments + 200 files. Once a mesh accumulates more than the filter can cover, responders re-sent the entire older tail to every requester every round — ~120KB per pair per 30s during file transfers, dropped by dedup after the airtime was already burned. Requesters now stamp the dormant sinceTimestamp TLV with the oldest timestamp their filter covers, and responders skip older packets (announces exempt: they carry the signing keys needed to verify everything else). Periodic sync also sends one request per type schedule instead of a union filter, so fragment floods can't crowd messages out of the filter budget. Security: a ~40-byte unsigned REQUEST_SYNC with an empty filter could elicit a full store replay (~900KB) — an unauthenticated >10,000x amplification vector, repeatable in a tight loop and relayable with crafted TTL to fan the drain out of every reachable node. Requests now require ttl == 0, a valid signature from the claimed sender's announced signing key, and a matching link binding; REQUEST_SYNC is never relayed regardless of TTL; and responses are rate-limited per peer (8 per 30s sliding window, ~3x the legitimate cadence). Cross-platform: verified against bitchat-android — it signs REQUEST_SYNC and sends SYNC_TTL_HOPS = 0, so both gates hold; it neither sends nor honors sinceTimestamp yet, so mixed pairs keep today's behavior with no regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address Codex review: enforce no-relay on route path, exact since-cursor Two P2 findings from Codex on the REQUEST_SYNC hardening: - Route-forwarding bypass: handleRequestSync's early return for a rejected (nonzero-TTL / unsigned) request still fell through to forwardAlongRouteIfNeeded, which relays any routed packet with ttl > 1 regardless of type. The no-relay invariant was only enforced on the flood path. BLERouteForwardingPolicy now suppresses REQUEST_SYNC outright, so a crafted request with a route and TTL headroom can't be forwarded to the next hop either. - Inexact since-cursor: GCSFilter.buildFilter trimmed by hash order when the encoding overflowed the byte budget, so the cursor (computed from the untrimmed prefix) could claim coverage of timestamps whose packets were dropped from the filter — re-sending exactly those every round. buildFilter now trims from the input tail (oldest, since candidates are newest-first) and reports includedCount; the cursor is derived from that, so the covered set is always a contiguous newest-prefix and the cursor is exact. Adds GCSFilter includedCount coverage (full vs trimmed), a route-forwarding test for REQUEST_SYNC, and makes the truncated-cursor test robust to trim variance. Full suite: 1029 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
266827ceff |
Extend BLE mesh range: relax RSSI gates, lift sparse TTL clamps, faster walk-back reconnects (#1338)
* Extend mesh range: relax RSSI gates, lift sparse TTL clamps, drain connection queue Range improvements to the BLE mesh, all policy-level (no wire/protocol changes): - Drain the connection candidate queue from the maintenance tick. Weak-RSSI discoveries are enqueued rather than connected, but the queue was only drained on disconnect/failure/timeout events — an isolated node surrounded only by weak (distant) peers queued them all and never connected to anyone. - Relax isolated RSSI floors from -90/-92 to -95/-100 and relax after 30s instead of 60s. When isolated, a fringe connection beats no connection; CoreBluetooth rarely reports below -100 so prolonged isolation now effectively accepts any decodable peer. - Drop the global high-timeout RSSI escalation (-80 after 3 timeouts in 60s). One flaky distant peer could blind the node to every other edge-of-range peer; per-peripheral cooldown, the discovery ignore window, and score bias already contain flaky links individually. - Relay at full incoming TTL in thin chains (degree <= 2). Sparse line topologies are exactly where every hop counts and where flood cost is minimal; previously messages lost a hop to the clamp. - Raise the fragment relay TTL cap from 5 to 7 in sparse graphs so media reaches as far as text; dense graphs keep the 5-hop clamp to contain full-fanout fragment floods. - Extend peer reachability retention from 21s to 60s (verified) / 45s (unverified) so duty-cycled nodes (worst-case dense announce interval 38s) don't forget peers between announces. - Extend the directed store-and-forward spool window from 15s to 60s so brief link gaps heal via the periodic flush. 957 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reconnect quickly after walk-away disconnects Field test (walk away + return between two devices) showed reconnect landing exactly 15.0s after the supervision-timeout disconnect: the scheduler records a dropped established connection via recordDisconnectError into the same map as connect timeouts, and handleDiscovery hard-ignores rediscoveries for 15s. Those are different situations. A connect attempt that timed out means the peer likely isn't reachable, so backing off is right. A dropped established connection usually means the peer walked out of range and will return — track it separately and only ignore rediscoveries for 3s (enough for CoreBluetooth to settle), so walking back into range reconnects ~12s sooner. Disconnect errors also no longer feed the weak-link cooldown or the candidate-score timeout bias; those penalties now apply only to peers that never answered a connect attempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Honor the disconnect settle window on the queue drain path Codex review caught that the 3s settle window was only enforced in handleDiscovery. A candidate can already be sitting in the queue when its peripheral drops (weak-RSSI adverts are enqueued even while connected, since the RSSI check precedes the existing-state check), and didDisconnectPeripheral immediately drains the queue — so the stale entry could reconnect right through the window, recreating the reconnect/cancel thrash it exists to prevent. nextCandidate now defers such candidates with retryAfter for the window's remainder, mirroring the weak-link cooldown pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Relay on one link per bound peer instead of both dual-role links Three-device field test (star topology) showed every relayed fragment arriving twice at the leaf: dual-role pairs hold two live links (we as central writing to their peripheral, they as central subscribed to ours) and broadcast/relay fanout sent the same packet down both — 2x airtime on exactly the pairs that talk most, with the receiver just discarding the duplicate. The fanout selector now collapses link selection to one link per bound peer, preferring the peripheral (write) side since it has per-link flow control via canSendWriteWithoutResponse, while notifications share the peripheral manager's update queue across all centrals. Links with no bound peer yet (pre-announce) pass through untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Only notify "bitchatters nearby" on the empty-to-populated transition Devices sitting idle and connected kept re-firing the notification. Two bugs in handleNetworkAvailability: - Peers first sighted during the 5-minute cooldown were never added to recentlySeenPeers (the formUnion only ran when a notification fired), so they stayed "new" forever and re-triggered on the next routine peer-list event once the cooldown lapsed. - There was no went-from-zero gate at all: any unseen peer notified, even while already meshed with others who are visible in the app. Every sighted peer is now recorded regardless of cooldown, and the notification only fires when the mesh transitions from confirmed-empty to populated with genuinely new peers. meshWasEmpty resets only via the existing confirmed-empty paths (30s empty confirmation, 10-minute quiet reset), so brief link flaps stay silent. The cooldown becomes injectable so tests can prove the transition gate independently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bump version to 1.5.2; Xcode 26.5 project settings update Marketing version 1.5.1 -> 1.5.2 (pbxproj + Release.xcconfig). Project settings refresh from Xcode 26.5: upgrade-check stamp, drop redundant DEVELOPMENT_TEAM self-references, scheme version stamps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Disable string catalog symbol generation The Xcode 26.5 settings refresh enabled STRING_CATALOG_GENERATE_SYMBOLS (the new default), which fails on the literal "%@" key in Localizable.xcstrings — a pure format placeholder can't become a Swift identifier. Nothing in the codebase references generated catalog symbols, so turn the feature off rather than renaming keys around it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Guard _PreviewHelpers references for archive builds Archiving for TestFlight failed: _PreviewHelpers is a development asset, so its sources (PreviewKeychainManager, BitchatMessage.preview) are excluded from Release/archive builds, and two call sites referenced them unconditionally: - TextMessageView's #Preview block — now wrapped in #if DEBUG - FavoritesPersistenceService.makeDefaultKeychain's test branch — the in-memory-keychain-under-test path is now #if DEBUG; tests always run Debug so behavior is unchanged, and Release always gets the real KeychainManager Verified with an iOS Release arm64 build (the archive configuration). 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> |
||
|
|
df36b19afe |
[codex] Refine BLE ingress fanout (#1280)
* Refactor BLE transport event handling * Make image output paths unique * Keep queued Nostr read receipts alive * Refine BLE ingress fanout * Rediscover BLE service after invalidation * Extract BLE notification retry buffer * Extract BLE inbound write buffer * Extract BLE fragment assembly buffer * Tidy secure log handling from device run * Allow self-authored RSR ingress replies * Harden read receipt queue test timing --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
806c420313 |
Add comprehensive test coverage for ChatViewModel and BLEService (#962)
* Add comprehensive test coverage for ChatViewModel and BLEService This commit adds 14 new tests to improve the safety net for future refactoring of ChatViewModel and BLEService: New test files: - ChatViewModelTorTests: Tor lifecycle notification handlers (8 tests) - ChatViewModelDeliveryStatusTests: Delivery status state machine (6 tests) - ChatViewModelRefactoringTests: Command routing and message handling (4 tests) - BLEServiceCoreTests: Packet deduplication and stale broadcast filtering (2 tests) - PublicMessagePipelineTests: Message ordering and deduplication (4 tests) - MessageRouterTests: Transport selection and outbox behavior (4 tests) - PrivateChatManagerTests: Chat selection and read receipts (2 tests) - UnifiedPeerServiceTests: Fingerprint resolution and blocking (2 tests) - RelayControllerTests: TTL, handshake, and fragment relay logic (4 tests) Modified files: - MockTransport: Added peer snapshot publishing on connect/disconnect - TestHelpers: Added waitUntil polling helper for async tests - MockIdentityManager: Extended for new test scenarios Total: 454 tests across 64 suites (was 440) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix flaky test by using waitUntil instead of fixed sleep Replace fixed 200ms sleep with waitUntil helper for more reliable async assertion in routing tests. This prevents timing issues on CI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |