- Change EXPIRATION_SECONDS from 300 to 20 for all call signaling
events (offer, answer, ICE, hangup, reject, renegotiate)
- Change MAX_EVENT_AGE_SECONDS from 30 to 20 to match
- Update NIP-AC doc accordingly
- Fix SecurityException crash on SDK 36: phoneCall foreground service
type requires MANAGE_OWN_CALLS permission. Switch to microphone
type which only needs RECORD_AUDIO (already granted).
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Fix call connection stuck at "Connecting":
- Inner events from unwrapped GiftWraps are dispatched through
EventProcessor.consumeEvent(), not LocalCache.newEventBundles.
The previous approach using newEventBundles observer never saw
the inner call events because they're created internally during
GiftWrap processing, not from relay arrivals.
- Restored callManager routing in EventProcessor.consumeEvent()
and wired it via account.newNotesPreProcessor.callManager in
AccountViewModel.initCallController()
Video call button:
- Added Videocam icon button in DM chat header (RenderRoomTopBar)
next to the voice call button
- onVideoCallClick initiates a VIDEO type call with camera capturer
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Ringtone & vibration:
- CallAudioManager plays default ringtone (looping) and vibrates
in a 1s-on/1s-off pattern when IncomingCall state is active
- Automatically stops on accept/reject/hangup via cleanup()
Proximity sensor:
- PROXIMITY_SCREEN_OFF_WAKE_LOCK acquired during Connecting and
Connected states, turns off screen when held to ear
- Released on call end
Camera for video calls:
- Camera2Enumerator finds front-facing camera
- CameraVideoCapturer attached to VideoSource at 640x480@30fps
- Camera stopped and disposed on call end
Full-screen intent on lock screen:
- Incoming call notification uses setFullScreenIntent() to show
the call screen even when device is locked
- USE_FULL_SCREEN_INTENT permission added to manifest
- Notification has VISIBILITY_PUBLIC for lock screen display
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
- Deduplicate signaling events via processedEventIds set in
CallManager to prevent duplicate processing from multiple relays
- BackHandler on CallScreen calls hangup() before navigating back
- KeepScreenOn composable adds FLAG_KEEP_SCREEN_ON during calls
and clears it on dispose
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Runtime permissions:
- RECORD_AUDIO permission prompted before initiating or accepting calls
- rememberCallWithPermission() composable wraps call actions with
Android runtime permission flow via accompanist
Audio routing:
- CallController.setAudioMuted/setVideoEnabled/setSpeakerOn now
control WebRtcCallSession and Android AudioManager
- Mute/speaker/video toggles in ConnectedCallUI wired to actual
hardware controls
Incoming call notifications:
- EventNotificationConsumer handles CallOfferEvent with follow-gate
and 30s staleness check
- New CALL_CHANNEL notification channel (IMPORTANCE_HIGH)
- NotificationUtils.sendCallNotification shows caller name with
auto-dismiss after 60 seconds
- Call notification cancelled on cleanup (accept/reject/hangup)
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Two fixes for call connectivity:
1. ICE candidate buffering: Candidates arriving before the WebRTC
session exists (callee ringing) or before remote description is
set (caller waiting for answer) are now queued in
pendingIceCandidates and flushed once setRemoteDescription is
called. This was the root cause of calls getting stuck at
"Connecting" — ICE candidates were silently dropped.
2. Stale event filter: All signaling events older than 30 seconds
are discarded in CallManager.onSignalingEvent() to prevent
old cached events from triggering phantom calls.
Also: removed cleanup() from WebRTC onDisconnected callback to
avoid double-cleanup race with the CallManager state observer.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
CallManager now auto-resets from Ended to Idle after 2 seconds via
transitionToEnded(). Previously reset() was called from a
LaunchedEffect in CallScreen which could be cancelled when the
composable was disposed via popBack(), leaving the state stuck at
Ended and silently dropping subsequent incoming calls.
Also: CallController now observes CallManager state and auto-cleans
up the WebRTC session when a call ends (handles peer hangup case).
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
- Add ObserveIncomingCalls composable in AppNavigation that watches
callManager.state and navigates to ActiveCall screen when an
IncomingCall is detected
- Wire the accept button in CallScreen to call
callController.acceptIncomingCall(sdpOffer), which sets the remote
SDP, creates a WebRTC answer, and sends it back gift-wrapped
- Pass CallController to CallScreen for accept functionality
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Remove callManager injection from EventProcessor — let the existing
gift wrap pipeline consume and index call events normally. Instead,
observe new notes from LocalCache.live.newEventBundles in
AccountViewModel and route call signaling events to CallManager
from there. This keeps the EventProcessor clean and follows the
existing pattern for UI-layer event observation.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Add CallOfferEvent, CallAnswerEvent, CallIceCandidateEvent,
CallHangupEvent, CallRejectEvent, and CallRenegotiateEvent to
LocalCache.justConsumeInnerInner() so they are properly consumed
and indexed when received from relays.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Connects all call infrastructure so calls flow through the system:
- EventProcessor routes unwrapped call events (offer/answer/ICE/
hangup/reject) from gift wraps to CallManager
- CallController orchestrates WebRTC session lifecycle: creates
PeerConnection, generates SDP offers/answers, exchanges ICE
candidates via gift-wrapped events, and manages foreground service
- AccountViewModel initializes CallManager + CallController and
wires answer/ICE callbacks between them
- Account.publishCallSignaling() publishes gift-wrapped events
- DM chat top bar gets a call button (1-on-1 rooms only) that
initiates a voice call and navigates to ActiveCall screen
- ActiveCall route registered in AppNavigation with CallScreen
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Specifies the signaling protocol for P2P voice/video calls:
- 6 event kinds (25050-25055) for offer/answer/ICE/hangup/reject/renegotiate
- NIP-59 gift wrap delivery (no seal layer)
- Follow-gated spam prevention
- Short expiration for ephemeral signaling data
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Implements P2P calling over Nostr relays using WebRTC for media
transport and NIP-59 Gift Wraps for encrypted signaling. No custom
server required — only public STUN servers for NAT traversal.
Protocol layer (quartz/nip100WebRtcCalls):
- 6 new event kinds (25050-25055): offer, answer, ICE candidate,
hangup, reject, renegotiate
- WebRtcCallFactory for creating and gift-wrapping signaling events
- CallIdTag and CallTypeTag for event metadata
- Events registered in EventFactory
Call state machine (commons/call):
- CallState sealed interface with full lifecycle states
- CallManager orchestrating signaling and state transitions
- Follow-gate spam prevention: only followed users can ring,
non-follows are silently ignored
Android WebRTC integration (amethyst/service/call):
- WebRtcCallSession wrapping Google WebRTC PeerConnection
- CallForegroundService for keeping calls alive in background
- IceServerConfig with default public STUN servers
- User-configurable TURN server support
Android UI (amethyst/ui/call):
- CallScreen with offering, connecting, connected, and ended states
- IncomingCallUI with accept/reject buttons
- ConnectedCallUI with mute, video toggle, speaker, and timer
- Call button added to 1-on-1 DM chat header
- ActiveCall route added to navigation
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Remove callManager injection from EventProcessor — let the existing
gift wrap pipeline consume and index call events normally. Instead,
observe new notes from LocalCache.live.newEventBundles in
AccountViewModel and route call signaling events to CallManager
from there. This keeps the EventProcessor clean and follows the
existing pattern for UI-layer event observation.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Add CallOfferEvent, CallAnswerEvent, CallIceCandidateEvent,
CallHangupEvent, CallRejectEvent, and CallRenegotiateEvent to
LocalCache.justConsumeInnerInner() so they are properly consumed
and indexed when received from relays.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Connects all call infrastructure so calls flow through the system:
- EventProcessor routes unwrapped call events (offer/answer/ICE/
hangup/reject) from gift wraps to CallManager
- CallController orchestrates WebRTC session lifecycle: creates
PeerConnection, generates SDP offers/answers, exchanges ICE
candidates via gift-wrapped events, and manages foreground service
- AccountViewModel initializes CallManager + CallController and
wires answer/ICE callbacks between them
- Account.publishCallSignaling() publishes gift-wrapped events
- DM chat top bar gets a call button (1-on-1 rooms only) that
initiates a voice call and navigates to ActiveCall screen
- ActiveCall route registered in AppNavigation with CallScreen
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Specifies the signaling protocol for P2P voice/video calls:
- 6 event kinds (25050-25055) for offer/answer/ICE/hangup/reject/renegotiate
- NIP-59 gift wrap delivery (no seal layer)
- Follow-gated spam prevention
- Short expiration for ephemeral signaling data
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Implements P2P calling over Nostr relays using WebRTC for media
transport and NIP-59 Gift Wraps for encrypted signaling. No custom
server required — only public STUN servers for NAT traversal.
Protocol layer (quartz/nip100WebRtcCalls):
- 6 new event kinds (25050-25055): offer, answer, ICE candidate,
hangup, reject, renegotiate
- WebRtcCallFactory for creating and gift-wrapping signaling events
- CallIdTag and CallTypeTag for event metadata
- Events registered in EventFactory
Call state machine (commons/call):
- CallState sealed interface with full lifecycle states
- CallManager orchestrating signaling and state transitions
- Follow-gate spam prevention: only followed users can ring,
non-follows are silently ignored
Android WebRTC integration (amethyst/service/call):
- WebRtcCallSession wrapping Google WebRTC PeerConnection
- CallForegroundService for keeping calls alive in background
- IceServerConfig with default public STUN servers
- User-configurable TURN server support
Android UI (amethyst/ui/call):
- CallScreen with offering, connecting, connected, and ended states
- IncomingCallUI with accept/reject buttons
- ConnectedCallUI with mute, video toggle, speaker, and timer
- Call button added to 1-on-1 DM chat header
- ActiveCall route added to navigation
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Fix call connection stuck at "Connecting":
- Inner events from unwrapped GiftWraps are dispatched through
EventProcessor.consumeEvent(), not LocalCache.newEventBundles.
The previous approach using newEventBundles observer never saw
the inner call events because they're created internally during
GiftWrap processing, not from relay arrivals.
- Restored callManager routing in EventProcessor.consumeEvent()
and wired it via account.newNotesPreProcessor.callManager in
AccountViewModel.initCallController()
Video call button:
- Added Videocam icon button in DM chat header (RenderRoomTopBar)
next to the voice call button
- onVideoCallClick initiates a VIDEO type call with camera capturer
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Ringtone & vibration:
- CallAudioManager plays default ringtone (looping) and vibrates
in a 1s-on/1s-off pattern when IncomingCall state is active
- Automatically stops on accept/reject/hangup via cleanup()
Proximity sensor:
- PROXIMITY_SCREEN_OFF_WAKE_LOCK acquired during Connecting and
Connected states, turns off screen when held to ear
- Released on call end
Camera for video calls:
- Camera2Enumerator finds front-facing camera
- CameraVideoCapturer attached to VideoSource at 640x480@30fps
- Camera stopped and disposed on call end
Full-screen intent on lock screen:
- Incoming call notification uses setFullScreenIntent() to show
the call screen even when device is locked
- USE_FULL_SCREEN_INTENT permission added to manifest
- Notification has VISIBILITY_PUBLIC for lock screen display
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
- Deduplicate signaling events via processedEventIds set in
CallManager to prevent duplicate processing from multiple relays
- BackHandler on CallScreen calls hangup() before navigating back
- KeepScreenOn composable adds FLAG_KEEP_SCREEN_ON during calls
and clears it on dispose
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Runtime permissions:
- RECORD_AUDIO permission prompted before initiating or accepting calls
- rememberCallWithPermission() composable wraps call actions with
Android runtime permission flow via accompanist
Audio routing:
- CallController.setAudioMuted/setVideoEnabled/setSpeakerOn now
control WebRtcCallSession and Android AudioManager
- Mute/speaker/video toggles in ConnectedCallUI wired to actual
hardware controls
Incoming call notifications:
- EventNotificationConsumer handles CallOfferEvent with follow-gate
and 30s staleness check
- New CALL_CHANNEL notification channel (IMPORTANCE_HIGH)
- NotificationUtils.sendCallNotification shows caller name with
auto-dismiss after 60 seconds
- Call notification cancelled on cleanup (accept/reject/hangup)
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Two fixes for call connectivity:
1. ICE candidate buffering: Candidates arriving before the WebRTC
session exists (callee ringing) or before remote description is
set (caller waiting for answer) are now queued in
pendingIceCandidates and flushed once setRemoteDescription is
called. This was the root cause of calls getting stuck at
"Connecting" — ICE candidates were silently dropped.
2. Stale event filter: All signaling events older than 30 seconds
are discarded in CallManager.onSignalingEvent() to prevent
old cached events from triggering phantom calls.
Also: removed cleanup() from WebRTC onDisconnected callback to
avoid double-cleanup race with the CallManager state observer.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
CallManager now auto-resets from Ended to Idle after 2 seconds via
transitionToEnded(). Previously reset() was called from a
LaunchedEffect in CallScreen which could be cancelled when the
composable was disposed via popBack(), leaving the state stuck at
Ended and silently dropping subsequent incoming calls.
Also: CallController now observes CallManager state and auto-cleans
up the WebRTC session when a call ends (handles peer hangup case).
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
- Add ObserveIncomingCalls composable in AppNavigation that watches
callManager.state and navigates to ActiveCall screen when an
IncomingCall is detected
- Wire the accept button in CallScreen to call
callController.acceptIncomingCall(sdpOffer), which sets the remote
SDP, creates a WebRTC answer, and sends it back gift-wrapped
- Pass CallController to CallScreen for accept functionality
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Remove callManager injection from EventProcessor — let the existing
gift wrap pipeline consume and index call events normally. Instead,
observe new notes from LocalCache.live.newEventBundles in
AccountViewModel and route call signaling events to CallManager
from there. This keeps the EventProcessor clean and follows the
existing pattern for UI-layer event observation.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Add CallOfferEvent, CallAnswerEvent, CallIceCandidateEvent,
CallHangupEvent, CallRejectEvent, and CallRenegotiateEvent to
LocalCache.justConsumeInnerInner() so they are properly consumed
and indexed when received from relays.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Connects all call infrastructure so calls flow through the system:
- EventProcessor routes unwrapped call events (offer/answer/ICE/
hangup/reject) from gift wraps to CallManager
- CallController orchestrates WebRTC session lifecycle: creates
PeerConnection, generates SDP offers/answers, exchanges ICE
candidates via gift-wrapped events, and manages foreground service
- AccountViewModel initializes CallManager + CallController and
wires answer/ICE callbacks between them
- Account.publishCallSignaling() publishes gift-wrapped events
- DM chat top bar gets a call button (1-on-1 rooms only) that
initiates a voice call and navigates to ActiveCall screen
- ActiveCall route registered in AppNavigation with CallScreen
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Specifies the signaling protocol for P2P voice/video calls:
- 6 event kinds (25050-25055) for offer/answer/ICE/hangup/reject/renegotiate
- NIP-59 gift wrap delivery (no seal layer)
- Follow-gated spam prevention
- Short expiration for ephemeral signaling data
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Implements P2P calling over Nostr relays using WebRTC for media
transport and NIP-59 Gift Wraps for encrypted signaling. No custom
server required — only public STUN servers for NAT traversal.
Protocol layer (quartz/nip100WebRtcCalls):
- 6 new event kinds (25050-25055): offer, answer, ICE candidate,
hangup, reject, renegotiate
- WebRtcCallFactory for creating and gift-wrapping signaling events
- CallIdTag and CallTypeTag for event metadata
- Events registered in EventFactory
Call state machine (commons/call):
- CallState sealed interface with full lifecycle states
- CallManager orchestrating signaling and state transitions
- Follow-gate spam prevention: only followed users can ring,
non-follows are silently ignored
Android WebRTC integration (amethyst/service/call):
- WebRtcCallSession wrapping Google WebRTC PeerConnection
- CallForegroundService for keeping calls alive in background
- IceServerConfig with default public STUN servers
- User-configurable TURN server support
Android UI (amethyst/ui/call):
- CallScreen with offering, connecting, connected, and ended states
- IncomingCallUI with accept/reject buttons
- ConnectedCallUI with mute, video toggle, speaker, and timer
- Call button added to 1-on-1 DM chat header
- ActiveCall route added to navigation
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Remove callManager injection from EventProcessor — let the existing
gift wrap pipeline consume and index call events normally. Instead,
observe new notes from LocalCache.live.newEventBundles in
AccountViewModel and route call signaling events to CallManager
from there. This keeps the EventProcessor clean and follows the
existing pattern for UI-layer event observation.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Add CallOfferEvent, CallAnswerEvent, CallIceCandidateEvent,
CallHangupEvent, CallRejectEvent, and CallRenegotiateEvent to
LocalCache.justConsumeInnerInner() so they are properly consumed
and indexed when received from relays.
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Connects all call infrastructure so calls flow through the system:
- EventProcessor routes unwrapped call events (offer/answer/ICE/
hangup/reject) from gift wraps to CallManager
- CallController orchestrates WebRTC session lifecycle: creates
PeerConnection, generates SDP offers/answers, exchanges ICE
candidates via gift-wrapped events, and manages foreground service
- AccountViewModel initializes CallManager + CallController and
wires answer/ICE callbacks between them
- Account.publishCallSignaling() publishes gift-wrapped events
- DM chat top bar gets a call button (1-on-1 rooms only) that
initiates a voice call and navigates to ActiveCall screen
- ActiveCall route registered in AppNavigation with CallScreen
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Specifies the signaling protocol for P2P voice/video calls:
- 6 event kinds (25050-25055) for offer/answer/ICE/hangup/reject/renegotiate
- NIP-59 gift wrap delivery (no seal layer)
- Follow-gated spam prevention
- Short expiration for ephemeral signaling data
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
Implements P2P calling over Nostr relays using WebRTC for media
transport and NIP-59 Gift Wraps for encrypted signaling. No custom
server required — only public STUN servers for NAT traversal.
Protocol layer (quartz/nip100WebRtcCalls):
- 6 new event kinds (25050-25055): offer, answer, ICE candidate,
hangup, reject, renegotiate
- WebRtcCallFactory for creating and gift-wrapping signaling events
- CallIdTag and CallTypeTag for event metadata
- Events registered in EventFactory
Call state machine (commons/call):
- CallState sealed interface with full lifecycle states
- CallManager orchestrating signaling and state transitions
- Follow-gate spam prevention: only followed users can ring,
non-follows are silently ignored
Android WebRTC integration (amethyst/service/call):
- WebRtcCallSession wrapping Google WebRTC PeerConnection
- CallForegroundService for keeping calls alive in background
- IceServerConfig with default public STUN servers
- User-configurable TURN server support
Android UI (amethyst/ui/call):
- CallScreen with offering, connecting, connected, and ended states
- IncomingCallUI with accept/reject buttons
- ConnectedCallUI with mute, video toggle, speaker, and timer
- Call button added to 1-on-1 DM chat header
- ActiveCall route added to navigation
https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS