Commit Graph

10581 Commits

Author SHA1 Message Date
Claude 24d7fb1a92 refactor: fix race conditions, null safety, and dead code
Critical fixes:
- CallController.initiateCall/acceptIncomingCall now validate
  WebRTC session creation with try/catch and null check before
  proceeding. Errors shown to user via errorMessage flow.
- AccountViewModel.initCallController() is now @Synchronized to
  prevent double initialization. EventProcessor callManager wired
  before creating CallController. Callbacks set before exposing
  controller to avoid timing races.
- Removed duplicate state (currentCallId/currentPeerPubKey) from
  CallController — uses callManager.currentCallId()/
  currentPeerPubKey() as single source of truth.
- Removed dead network callback code (was only logging).

Code quality:
- CallScreen null-safety patterns now use remember{} for fallback
  StateFlow instances instead of creating new ones per recomposition.
- Removed dead rememberCallPermissionLauncher from CallPermissions.
- CallController cleaned up from 363 to 304 lines.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 02:45:36 +00:00
Claude 44d6345e16 refactor: improve separation of concerns in call feature
1. Move UI toggle state out of CallState.Connected:
   - Removed isAudioMuted/isVideoEnabled/isSpeakerOn from domain
     CallState. These are UI concerns, not call state.
   - Added StateFlows for toggles in CallController (isAudioMuted,
     isVideoEnabled, isSpeakerOn) with proper toggle methods.
   - CallScreen reads toggle state from CallController flows.
   - Removed toggleAudioMute/toggleVideo/toggleSpeaker from
     CallManager (no longer manages UI state).

2. Move ICE candidate parsing to quartz layer:
   - Added candidateSdp(), sdpMid(), sdpMLineIndex() parsers and
     serializeCandidate() to CallIceCandidateEvent in quartz.
   - Removed companion object with parseIceCandidate/
     serializeIceCandidate from CallController.
   - CallController now uses quartz-layer parsing directly.
   - Updated IceCandidateSerializationTest accordingly.

3. Add helper methods to CallManager:
   - currentCallId() and currentPeerPubKey() extract from any
     active state, reducing repeated when expressions.

4. Fix empty callId bug in ChatroomScreen:
   - Navigation to ActiveCall now uses callManager.currentCallId()
     instead of hardcoded empty string.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 02:39:41 +00:00
Claude f344f00e9d test: add previews and unit tests for call feature
Previews (CallScreenPreviews.kt):
- PreviewCallingScreen: outgoing call "Calling..." state
- PreviewConnectingScreen: WebRTC connecting state
- PreviewCallEndedScreen: call ended state
- PreviewIncomingCallScreen: incoming call with accept/reject
- PreviewConnectedCallScreen: active call with controls
- PreviewConnectedCallMuted: muted mic + speaker on state
All use ThemeComparisonColumn for dark/light side-by-side

Unit tests - quartz (CallTagsTest.kt):
- CallIdTag parse/assemble/roundtrip, edge cases (empty, wrong name)
- CallTypeTag parse voice/video, unknown types, roundtrip
- CallType.fromString validation

Unit tests - quartz (CallEventsTest.kt):
- All 6 event kinds have correct constants (25050-25055)
- CallOfferEvent.build includes call-id, call-type, p, expiration tags
- Expiration is 20 seconds
- Answer/ICE/Hangup/Reject/Renegotiate template content verification

Unit tests - amethyst (IceCandidateSerializationTest.kt):
- Parse ICE candidate JSON with all fields
- Parse with different sdpMLineIndex
- Parse with missing fields (defaults)
- Serialize produces valid JSON
- Roundtrip serialization preserves all fields

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 02:27:23 +00:00
Claude 8354bcd616 debug: add logging to trace call signaling flow
Temporary diagnostic logging to identify why calls hang at
Connecting. Logs added to:
- CallManager.onSignalingEvent: event kind, age, state
- CallController: answer received, ICE candidates (buffered vs
  direct), flush count, local ICE generation

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 02:12:37 +00:00
Claude ca21a8489f feat: complete call UI with video, audio routing, error handling
1. Audio mode: AudioManager switches to MODE_IN_COMMUNICATION on
   Connecting, restores to MODE_NORMAL on call end. Audio routes
   through earpiece by default during calls.

2. Notification actions: Accept/Reject buttons added to incoming
   call notification for quick action without opening the app.

3. Background handling: Full-screen intent brings app to foreground.
   CallManager state persists in ViewModel so the call screen
   picks up current state when navigated to via notification.

4. Call screen icons: Replaced text labels with Material icons -
   Mic/MicOff for mute, Videocam/VideocamOff for camera,
   VolumeUp/VolumeOff for speaker. Color-coded toggle states.

5. Video rendering: SurfaceViewRenderer composable wraps WebRTC
   video tracks. Remote video displays full-screen, local video
   as a small PIP in the top-right corner. Falls back to avatar
   display when no video tracks are active.

6. Ringback tone: ToneGenerator plays TONE_SUP_RINGTONE during
   Offering state (caller hears ringing). Stops on connect/end.

7. Error handling: CallController catches WebRTC session creation
   failures, SDP errors, and ICE failures. Errors exposed via
   errorMessage StateFlow and displayed as Snackbar on call screen.

8. Network handling: ConnectivityManager.NetworkCallback registered
   during active calls to detect WiFi/cellular changes.
   Unregistered on call end.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 02:08:56 +00:00
Claude b01ddc8958 fix: use microphone foreground service type instead of phoneCall
phoneCall type requires MANAGE_OWN_CALLS on SDK 34+, which needs
the app to be a default dialer. microphone type only requires
RECORD_AUDIO which is already granted via runtime permission.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 01:58:21 +00:00
Vitor Pamplona 7643fc05f1 Merge branch 'claude/add-webrtc-calls-4kBSR' of https://github.com/vitorpamplona/amethyst into claude/add-webrtc-calls-4kBSR
# Conflicts:
#	amethyst/src/main/AndroidManifest.xml
#	amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt
#	commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallIceCandidateEvent.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallOfferEvent.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt
2026-04-01 21:52:02 -04:00
Claude 1016e46d50 fix: reduce event expiration to 20s and fix foreground service crash
- 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
2026-04-02 01:28:00 +00:00
Claude d93b384f72 fix: route call events via EventProcessor and add video call button
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
2026-04-02 01:20:53 +00:00
Claude b0f3943e69 feat: add ringtone, vibration, proximity sensor, camera, and lock screen call UI
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
2026-04-02 01:20:53 +00:00
Claude 3f0d1aa60f fix: add event dedup, back button handling, and wake lock for calls
- 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
2026-04-02 01:20:53 +00:00
Claude 8114739166 feat: add runtime permissions, audio routing, and call notifications
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
2026-04-02 01:20:53 +00:00
Claude 0688a46604 fix: buffer ICE candidates and discard stale signaling events
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
2026-04-02 01:20:52 +00:00
Claude 8ddec3a0d7 fix: auto-reset CallManager state after call ends
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
2026-04-02 01:20:52 +00:00
Claude 43776c5bdc feat: wire incoming call navigation and accept button
- 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
2026-04-02 01:20:52 +00:00
Claude 0c43e11d46 refactor: observe call events from LocalCache instead of EventProcessor
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
2026-04-02 01:20:52 +00:00
Claude 76ddeeaa3a refactor: rename NIP-100 to NIP-AC for WebRTC calls
Rename package nip100WebRtcCalls -> nipACWebRtcCalls and
NIP-100.md -> NIP-AC.md across all modules.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 01:20:52 +00:00
Claude 0683a9b612 fix: register call event kinds in LocalCache consumer
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
2026-04-02 01:20:45 +00:00
Claude d27137c5ef feat: wire WebRTC call signaling end-to-end
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
2026-04-02 01:20:45 +00:00
Claude 4c4c21f6a4 docs: add NIP-100 draft for WebRTC calls over Nostr
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
2026-04-02 01:20:31 +00:00
Claude 71ef072c6d feat: add WebRTC voice/video call infrastructure
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
2026-04-02 01:20:31 +00:00
Vitor Pamplona 01eb1dcd28 Improves the language of the NIP 2026-04-02 01:20:01 +00:00
Vitor Pamplona c12394be64 Fixes infinite loop 2026-04-02 01:20:01 +00:00
Claude 902f7d8c97 refactor: observe call events from LocalCache instead of EventProcessor
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
2026-04-02 01:20:00 +00:00
Claude 69469ae756 refactor: rename NIP-100 to NIP-AC for WebRTC calls
Rename package nip100WebRtcCalls -> nipACWebRtcCalls and
NIP-100.md -> NIP-AC.md across all modules.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-02 01:20:00 +00:00
Claude e59dbfebc4 fix: register call event kinds in LocalCache consumer
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
2026-04-02 01:20:00 +00:00
Claude d39c8801c9 feat: wire WebRTC call signaling end-to-end
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
2026-04-02 01:20:00 +00:00
Claude 3c486a383b docs: add NIP-100 draft for WebRTC calls over Nostr
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
2026-04-02 01:20:00 +00:00
Claude 2ee48da064 feat: add WebRTC voice/video call infrastructure
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
2026-04-02 01:20:00 +00:00
Vitor Pamplona 6fd6cd82c5 Fixes missing permission 2026-04-01 21:18:49 -04:00
Claude 731f0e0273 fix: route call events via EventProcessor and add video call button
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
2026-04-02 01:12:04 +00:00
Claude c2528d249b feat: add ringtone, vibration, proximity sensor, camera, and lock screen call UI
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
2026-04-02 00:50:25 +00:00
Claude 073e9f052a fix: add event dedup, back button handling, and wake lock for calls
- 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
2026-04-02 00:46:44 +00:00
Claude b04d052507 feat: add runtime permissions, audio routing, and call notifications
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
2026-04-02 00:35:24 +00:00
Claude e072b9ac99 fix: buffer ICE candidates and discard stale signaling events
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
2026-04-02 00:22:41 +00:00
Claude f1fd5ec786 fix: auto-reset CallManager state after call ends
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
2026-04-02 00:07:39 +00:00
Vitor Pamplona d263106562 Merge branch 'claude/add-webrtc-calls-4kBSR' of https://github.com/vitorpamplona/amethyst into claude/add-webrtc-calls-4kBSR 2026-04-01 19:52:43 -04:00
Vitor Pamplona 8af5131e81 Merge branch 'claude/add-webrtc-calls-4kBSR' of https://github.com/vitorpamplona/amethyst into claude/add-webrtc-calls-4kBSR
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/WebRtcCallSession.kt
#	quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md
2026-04-01 19:51:43 -04:00
Vitor Pamplona e42258e3c3 Improves the language of the NIP 2026-04-01 19:48:01 -04:00
Claude 336adc4948 feat: wire incoming call navigation and accept button
- 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
2026-04-01 23:47:35 +00:00
Claude 9b9ee52317 refactor: observe call events from LocalCache instead of EventProcessor
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
2026-04-01 23:28:40 +00:00
Claude a261727d95 refactor: rename NIP-100 to NIP-AC for WebRTC calls
Rename package nip100WebRtcCalls -> nipACWebRtcCalls and
NIP-100.md -> NIP-AC.md across all modules.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-01 23:28:40 +00:00
Claude 934ebd3a89 fix: register call event kinds in LocalCache consumer
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
2026-04-01 23:28:40 +00:00
Claude 6128e35765 feat: wire WebRTC call signaling end-to-end
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
2026-04-01 23:28:40 +00:00
Claude 8ae65df96b docs: add NIP-100 draft for WebRTC calls over Nostr
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
2026-04-01 23:28:40 +00:00
Claude 12b2731eb5 feat: add WebRTC voice/video call infrastructure
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
2026-04-01 23:28:39 +00:00
Vitor Pamplona 7bba3cda9c Fixes infinite loop 2026-04-01 19:27:39 -04:00
Claude 55368db5d9 refactor: observe call events from LocalCache instead of EventProcessor
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
2026-04-01 23:23:45 +00:00
Claude ecccf8b681 refactor: rename NIP-100 to NIP-AC for WebRTC calls
Rename package nip100WebRtcCalls -> nipACWebRtcCalls and
NIP-100.md -> NIP-AC.md across all modules.

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
2026-04-01 23:18:50 +00:00
Claude 2837bcbe43 fix: register call event kinds in LocalCache consumer
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
2026-04-01 23:06:13 +00:00