- Use Intent(ACTION_VIEW) + startActivity instead of LocalUriHandler so
ActivityNotFoundException is catchable
- Show ErrorMessageDialog with title/message when no app handles the URI,
matching the pattern used by zap payment errors
- Always show the wallet icon (profile button + reactions row), showing a
disabled "no targets" row in the dialog when the author has none set
- Add no_payment_app_found and error_dialog_payment_error string resources
https://claude.ai/code/session_018G4g1cChqjeNEM5TztD3FE
Add Account.updateSendKind0EventsToLocalRelay() suspend fun that calls
sendNewAppSpecificData() after updating the setting, matching the pattern
used by updateFilterSpam/updateWarnReports. Update AccountViewModel to
call this via launchSigner instead of directly mutating settings.
https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F
- Move the setting from a raw AccountSettings field into
AccountSyncedSettingsInternal / AccountSecurityPreferences so it is
serialised with the rest of the synced app-specific data (NIP-78)
- Remove one-off LocalPreferences persistence (no longer needed)
- Update ForwardKind0ToLocalRelayState to read from syncedSettings.security
- Move the UI toggle from inside renderLocalItems into the relay
settings screen (AllRelayListScreen) as a proper SettingsRow item
in the local-relay section, with title and description
- Fix Account initialisation order so localRelayList is declared before
ForwardKind0ToLocalRelayState
https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F
Adds a toggle in the Local Relays settings section that, when enabled,
forwards all received kind 0 (profile/metadata) events to the user's
configured local relays.
- AccountSettings: add sendKind0EventsToLocalRelay MutableStateFlow + toggle method
- LocalPreferences: persist the new setting
- ForwardKind0ToLocalRelayState: uses EventCollector to intercept kind 0
events from any relay and publish them to local relays when enabled
- Account: instantiate ForwardKind0ToLocalRelayState
- AccountViewModel: expose toggleSendKind0ToLocalRelay
- LocalRelayListView: add Switch toggle above the relay list
- strings.xml: add label and description strings
https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F
When a peer rejects a group call, CallManager updated its state but never
notified CallController to dispose the peer's PeerSession. This caused two
issues:
1. The rejected peer's PeerConnection lingered in HAVE_LOCAL_OFFER state
until the entire call ended (resource leak visible as "disposing 2 peer
sessions" when only 1 should exist).
2. In onPeerDisconnected, the allDisconnected check required ALL sessions
to be CLOSED. The stale session blocked this check, preventing automatic
hangup when ICE failed — meaning the remote peer never received a hangup
event and kept ringing.
Fix: invoke onPeerLeft when a peer rejects in Offering/Connecting/Connected
states, and make onPeerDisconnected treat sessions without a remote
description as inactive.
https://claude.ai/code/session_01UXaFKmHVPDyTzfHL8fkSw1
Four bugs fixed in group call signaling:
1. Self-event filtering: Own answer/ICE/hangup events echoed from relays
were processed as if from another peer, causing callees to attempt
callee-to-callee connections with themselves.
2. Lost peer discovery: When a callee received another callee's answer
while still ringing (IncomingCall state), the peer was silently
discarded. Now stored in discoveredCalleePeers and used for mesh
setup after accepting.
3. Duplicate onNewPeerInGroupCall: Both CallManager and CallController
triggered mesh setup for the same peer, causing duplicate log entries
and wasted work. Removed CallManager's redundant calls since
CallController handles this internally via onCallAnswerReceived.
4. No per-peer session cleanup: When a peer hung up but the call
continued, their WebRTC PeerSession was never disposed — it lingered
until ICE timeout. Added onPeerLeft callback and disposePeerSession
to cleanly close individual sessions and update video tracks.
Also added strategic debug logging for session lifecycle, peer
discovery, and disconnect handling.
https://claude.ai/code/session_01SWDsQJibYi1MMgvu55puSY
When a group member (e.g. Bob) rejects a call, the rejection event is
sent to all group members. The caller (Alice) correctly removes Bob,
but other callees (Charlie) were not removing Bob because the
IncomingCall handler in onCallRejected only handled self-rejection
for multi-device sync. Now it also removes the rejecting peer from
groupMembers, matching the behavior already present in onPeerHangup.
https://claude.ai/code/session_01PMwKCEU1fRDGNQ8WdX8CFW
Three issues addressed:
1. Make cleanup() exception-safe: wrap each WebRTC dispose call in
its own try-catch so that a failure in one (e.g. native crash
disposing a PeerConnection) does not skip releasing the camera,
audio mode, foreground service, or EGL context.
2. Upgrade the Idle safety net to call cleanup() instead of only
stopping ringing/notifications. If the Ended state is missed due
to StateFlow conflation, the Idle handler now performs full
resource cleanup (camera, WebRTC, audio mode, foreground service,
proximity wake lock). cleanup() is idempotent since all resources
are null-checked and nulled out.
3. Call cleanup() in AccountViewModel.onCleared() so that WebRTC
resources, camera, audio mode, and foreground service are released
when the ViewModel is destroyed (e.g. logout, activity recreation).
4. Clear processedEventIds when transitioning to Idle to prevent
unbounded memory growth across calls.
https://claude.ai/code/session_01GWRdrVAa29BsDkv8R7Z3Y9
StateFlow is conflated — when the state transitions rapidly from
Offering → Connecting → Connected, the collector may skip Connecting
entirely and only see Connected. Since stopRingbackTone() was only
called in the Connecting handler, the ringback tone would keep playing
through the entire call.
Fix: duplicate the ringing/notification cleanup in the Connected handler
so the tone is always stopped regardless of which state the collector
sees first. Also make switchToCallAudioMode() idempotent and clean up
the previous ToneGenerator in startRingbackTone() to prevent leaks.
https://claude.ai/code/session_01GWRdrVAa29BsDkv8R7Z3Y9
The state collector in CallController was blocking on
showIncomingCallNotification() which downloads the caller's profile
picture over the network. Because StateFlow is conflated, if the call
ended (peer hangup, reject, timeout) while the collector was suspended,
the Ended→Idle transition would cause the Ended emission to be lost.
Since cleanup/stopRinging only ran in the Ended handler, the ringtone
and vibration would continue indefinitely.
Two fixes:
1. Launch showIncomingCallNotification in a separate coroutine so the
collector is never blocked by network I/O.
2. Add a safety-net Idle handler that stops ringing, ringback tone,
and cancels the call notification.
https://claude.ai/code/session_01NfyLNgR4d8yjnxaQJ6FUt5
The Offering, Ended, Connected, and PipConnected call states were showing
the logged-in user's picture alongside other call members. IncomingCall and
Connecting already filtered correctly. Now all states consistently exclude
the current user via `- accountViewModel.account.signer.pubKey`.
https://claude.ai/code/session_01PFzKU8Y3nUQXhshA3MT5EM
Replaces the single-PeerConnection architecture with full mesh topology
where each participant maintains one PeerConnection per peer, as
specified by NIP-AC.
WebRtcCallSession: Refactored to a pure PeerConnection wrapper that
accepts a shared PeerConnectionFactory. No longer manages media sources,
tracks, or camera — those are now shared across all peer sessions.
CallController: Manages per-peer sessions via ConcurrentHashMap. Shared
resources (PeerConnectionFactory, EglBase, audio/video sources, camera)
are initialized once and reused. Each peer gets its own WebRtcCallSession
with per-peer ICE candidate routing. Supports callee-to-callee mesh
connections with pubkey-based tie-breaking to avoid ofer glare.
CallManager: New methods for per-peer offer/answer publishing
(publishOfferToPeer, publishAnswerToPeer, beginOffering). Forwards ALL
answers to CallController (not just the first). New callbacks
onNewPeerInGroupCall and onMidCallOfferReceived for mesh setup. Handles
mid-call offers (same call-id) when already in Connecting/Connected state.
AccountViewModel: Updated callback wiring to pass peer pubkey with
answer events and register new group call callbacks.
https://claude.ai/code/session_01J5fJx9YbSBx1BsBMctiAm8
Two bugs caused WebRTC group calls to get stuck in "Connecting":
1. CallController.onLocalIceCandidate() used currentPeerPubKey() which
returns only the first peer via firstOrNull(). ICE candidates were
gift-wrapped to only one peer; the others never received them.
Fixed by iterating over all currentPeerPubKeys().
2. CallManager.acceptCall() set Connecting.peerPubKeys to groupMembers
which includes the local user's own pubkey. This caused
currentPeerPubKey() to potentially return self, sending ICE
candidates to oneself instead of the caller.
Fixed by filtering out signer.pubKey from the peer set.
https://claude.ai/code/session_01J5fJx9YbSBx1BsBMctiAm8
1. Filter out the logged-in user from the Connecting state UI, matching
the existing behavior in IncomingCall state.
2. Change GroupCallNames default textColor from Color.Unspecified to
MaterialTheme.colorScheme.onSurface so names are visible in dark mode
(Box+background doesn't set LocalContentColor like Surface does).
https://claude.ai/code/session_016sDH1SL7P8aXhcyFaFtzYu
In group calls (e.g. Alice calls Bob and Charlie), when Bob accepts the
call, a CallAnswerEvent is gift-wrapped to all group members including
Charlie. Charlie's CallManager, still in IncomingCall state, was treating
ANY CallAnswerEvent as "answered elsewhere" (intended for multi-device
scenarios) and ending the call prematurely.
The fix checks whether the answering/rejecting peer is actually the
current user (signer.pubKey) before treating it as an "answered/rejected
elsewhere" event. If it's a different group member, we simply ignore it
and keep ringing.
https://claude.ai/code/session_01EYzWB93PZRqw15QuQadCf4
Adds debug logging across CallManager, CallController, and WebRtcCallSession
to trace the full call lifecycle: SDP offer/answer creation, remote description
set success/failure, ICE candidate exchange, ICE connection state transitions,
and signaling state changes. The noOpSdpObserver is replaced with a logging
observer so setRemoteDescription success/failure is visible in logcat.
Filter logcat with: CallManager|CallController|WebRtcCallSession
https://claude.ai/code/session_01M3yyyRyu8KWJZ5PjNt4V2H