Commit Graph

473 Commits

Author SHA1 Message Date
Claude fd42c7b725 refactor: use quartz event builders in CallManager test helpers
Replace manual tag assembly in test helpers (makeOffer, makeAnswer, etc.)
with the existing build() methods from quartz event classes. This keeps
test tag structures in sync with production code automatically.

https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
2026-04-05 13:54:03 +00:00
Claude 9b356d31ba feat: add thumbnail disk cache for profile pictures
Profile pictures are displayed at small fixed sizes (18-100dp) but Coil's
disk cache stores full-size originals, causing expensive re-decode on every
memory cache miss. With 60 avatars on screen, this creates significant I/O.

Adds a ProfilePictureFetcher with a dedicated ThumbnailDiskCache that stores
pre-resized 256px JPEG thumbnails (~5-10KB each). On cache hit, reads a tiny
file instead of re-decoding the full original. The zoomable full-screen dialog
bypasses this cache and loads the original via the normal Coil pipeline.

https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
2026-04-05 03:06:52 +00:00
Claude 9854209843 test: add comprehensive NIP-AC WebRTC call state machine tests
Add 81 tests across two test suites covering the full NIP-AC spec:

- quartz/NipACStateMachineTest (31 tests): Protocol compliance test vectors
  for event structure, tags, P2P/group flows, ICE serialization, staleness,
  renegotiation glare rules, and multi-device support

- commons/CallManagerTest (50 tests): State machine integration tests using
  real NostrSignerInternal with actual crypto, covering:
  * Full call lifecycle (Idle → Offering/IncomingCall → Connecting → Connected → Ended → Idle)
  * Call rejection, busy auto-reject, hangup from any state
  * Self-event filtering (ICE, hangup, answer-elsewhere)
  * Mid-call renegotiation (voice ↔ video)
  * Group calls (mesh discovery, partial disconnect, invite peer)
  * Interface-level tests with real signing + gift wrapping pipeline
  * Full end-to-end P2P flow with two CallManager instances

Also adds test vector tables to NIP-AC.md spec for other implementers.

https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
2026-04-04 15:26:20 +00:00
Claude 799ebd692f feat: switch NIP-AC from GiftWrap with expiration to EphemeralGiftWrap
Use EphemeralGiftWrapEvent (kind 21059) instead of GiftWrapEvent (kind
1059) for WebRTC call signaling. The ephemeral kind signals to relays
that these events are transient and should not be persisted, eliminating
the need for expiration tags on both inner signaling events and outer
wraps.

Changes:
- Remove expiration tags from all 6 call event types (25050-25055)
- Switch WebRtcCallFactory to produce EphemeralGiftWrapEvent wraps
- Update CallManager and CallController publishEvent types
- Update Account.publishCallSignaling signature
- Use CallManager.MAX_EVENT_AGE_SECONDS for staleness checks
- Update NIP-AC spec to document EphemeralGiftWrap usage
- Remove expiration-related tests

https://claude.ai/code/session_014kyBgZx7cNyeUXYWV25M4j
2026-04-03 22:11:19 +00:00
Claude 303e68331a fix: clean up rejected peer's PeerSession to prevent stale sessions blocking hangup
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
2026-04-03 19:33:30 +00:00
Vitor Pamplona 35a1a20bde Merge pull request #2110 from vitorpamplona/claude/debug-webrtc-calls-KqELh
Improve group call peer management and signaling robustness
2026-04-03 15:01:22 -04:00
Claude bde17099a4 fix: WebRTC group call bugs — self-event filtering, mesh setup, per-peer cleanup
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
2026-04-03 19:00:04 +00:00
Claude d08d101b7d fix: remove rejecting peer from group members in IncomingCall state
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
2026-04-03 18:47:04 +00:00
Claude fe13b77e22 fix: harden call cleanup to guarantee nothing is left running
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
2026-04-03 18:16:09 +00:00
Claude fcecb5241c feat: implement full-mesh WebRTC group calls with per-peer PeerConnections
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
2026-04-03 16:54:00 +00:00
Claude 0488245200 fix: send ICE candidates to all peers and exclude self from peer set
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
2026-04-03 15:36:50 +00:00
Claude 4a529a304c fix: prevent group call participants from disconnecting when another member answers/rejects
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
2026-04-03 14:37:29 +00:00
Claude 76477373d9 debug: add comprehensive WebRTC call flow logging to diagnose "stuck on Connecting"
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
2026-04-03 13:30:50 +00:00
Vitor Pamplona 316290ac32 Merge pull request #2092 from vitorpamplona/claude/add-hashtag-limit-filter-N8sMZ
Add maximum hashtag limit security filter
2026-04-03 08:55:13 -04:00
Claude 1bc94f5a29 refactor: remove P2P vs group branching in CallManager
The group factory methods work correctly with any number of members,
including single-peer calls. Remove the if/else branches that
duplicated P2P vs group logic in acceptCall, rejectCall, hangup,
sendRenegotiation, and sendRenegotiationAnswer.

https://claude.ai/code/session_013h2E7spwHDgSjunqumsYgp
2026-04-03 12:35:53 +00:00
Claude 2a24b711cd feat: add configurable max hashtag limit filter for spam prevention
Adds a security setting to hide posts with more than X hashtags (t tags),
defaulting to 5. Applied at the feed filter level via FilterByListParams.match()
and Account.isAcceptable() to cover all feeds. Configurable in Security Filters
settings, with 0 to disable.

https://claude.ai/code/session_01NMVypgGSU9EjQA7hZ9uvjD
2026-04-03 12:34:32 +00:00
Claude 7020eb003c fix: per-peer SDP handling and invitePeer group context
SDP payloads (offer, answer, renegotiate) are specific to individual
PeerConnections and cannot be shared across peers. This commit:

- Adds group-context overloads to createCallOffer, createCallAnswer,
  and createRenegotiate that include all member p-tags but wrap to
  a single target peer
- Removes createGroupRenegotiate (renegotiation is always per-peer)
- Updates sendRenegotiation/sendRenegotiationAnswer to take explicit
  peerPubKey and use per-peer wrapping with group p-tags
- Updates invitePeer to include all existing group members + invitee
  in p-tags so the new peer sees the full group composition
- Documents SDP per-peer vs sign-once distinction in NIP-AC spec

https://claude.ai/code/session_013h2E7spwHDgSjunqumsYgp
2026-04-03 11:57:32 +00:00
Claude b2584e6705 feat: include all group members in p-tags of inner ephemeral events
All signaling event types in group calls (answer, hangup, reject,
renegotiate) now include p-tags for every group member, matching the
pattern already used by CallOfferEvent. This allows signing the inner
event once and gift-wrapping it separately for each recipient.

- Add Set<HexKey> build() overloads to CallAnswerEvent, CallHangupEvent,
  CallRejectEvent, and CallRenegotiateEvent
- Add createGroupCallAnswer, createGroupRenegotiate factory methods
- Fix createGroupHangup to sign once instead of per-peer
- Update createGroupReject to accept full member set
- Update CallManager to use group methods when in group call
- Document group call p-tag convention in NIP-AC spec
- Add tests for all group build overloads

https://claude.ai/code/session_013h2E7spwHDgSjunqumsYgp
2026-04-03 04:53:17 +00:00
Claude ea65dd76e7 fix: correct group call hangup signaling and connection resilience
- Include pending peers in hangup notification so ringing users get
  immediate feedback instead of waiting for the 60s timeout
- Sign individual hangup events per peer with correct p-tag instead
  of reusing a single event with the first peer's tag
- Stop treating ICE DISCONNECTED as terminal; only hang up on FAILED
  since DISCONNECTED is often transient (network switch, packet loss)
- Send "busy" reject when receiving a call while already in one

https://claude.ai/code/session_01HpowdWMK77pA35ABn9XWpD
2026-04-03 02:49:37 +00:00
Claude a65e192f03 feat: add participant to existing call + comprehensive call previews
Add ability to invite new users into an ongoing call:
- CallManager.invitePeer() sends a call offer to a new peer using
  the current callId, adding them to pendingPeerPubKeys
- CallController.invitePeer() exposes this to the UI layer
- Add PersonAdd button to connected call controls
- Add AddParticipantDialog with user search (reuses UserSuggestionState)
- New strings: call_add_participant, call_search_users

Rewrite CallScreenPreviews to showcase all call states:
1. Offering P2P call (Calling...)
2. Offering group call (3 avatars)
3. Connecting
4. Incoming voice call
5. Incoming video call
6. Incoming group call (4 avatars)
7. Connected P2P voice call with controls
8. Connected muted + video + speaker
9. Connected group call with pending peers
10. Connected with Bluetooth audio
11. Call ended
12. Large group (5+ members, +N badge)
13. PiP calling
14. PiP connected

https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR
2026-04-03 01:07:14 +00:00
Claude 0c7aa67dd8 feat: start group call on first answer, show pending peers
The call now starts immediately when the first peer answers instead
of waiting for all participants. Remaining peers are tracked as
pending and shown in the UI with a "Waiting for others to join..."
indicator. When a pending peer answers, they move to the connected
set. If a pending peer rejects or hangs up, they're silently removed.

Changes:
- Add pendingPeerPubKeys to Connecting and Connected states
- Add allPeerPubKeys helper on Connected for UI rendering
- Split peers into connected/pending on first answer in onCallAnswered
- Handle subsequent answers in Connecting and Connected states
- Carry pending peers through Connecting -> Connected transition
- Show all peers (connected + pending) in call UI avatars
- Display "Waiting for others to join..." when pending peers exist

https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR
2026-04-03 00:58:50 +00:00
Claude 3962f75583 feat: keep group call alive when a peer leaves until only 2 remain
When a peer hangs up or rejects in a group call, remove them from the
participant set instead of ending the entire call. The call only ends
when no peers remain (all others left). Similarly, if a peer rejects
during the Offering phase of a group call, remove them but continue
ringing remaining peers.

For incoming group calls, if the original caller hangs up the call
ends immediately. If another group member leaves, the call continues
as long as at least 2 members (including self) remain.

https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR
2026-04-03 00:29:24 +00:00
Claude f25c1df0e2 feat: enable group calls via gift wraps to each group member
Extends the NIP-AC P2P call signaling to support group calls by
sending individual gift-wrapped offers to each member of the group,
all sharing the same call-id.

- CallOfferEvent: add multi-pubkey build overload, groupMembers(),
  isGroupCall(), and recipientPubKeys() helpers
- WebRtcCallFactory: add GroupResult type, createGroupCallOffer(),
  createGroupHangup(), and createGroupReject() methods
- CallState: change peerPubKey to peerPubKeys (Set<HexKey>) across
  Offering, Connecting, Connected, and Ended states; add groupMembers
  field to IncomingCall
- CallManager: add initiateGroupCall() that creates a single signed
  offer with p tags for every callee and gift-wraps it individually;
  hangup sends to all peers in a group call
- CallController: add initiateGroupCall() entry point
- Tests: add group call offer tests for p tags, call-id, call-type,
  and expiration

https://claude.ai/code/session_01WafqMofFQfWCQA5TcLvHRb
2026-04-02 22:13:48 +00:00
Claude 500928f1b3 fix: stop ringing on other devices when call is answered or rejected
When a user is logged in on multiple devices, all devices ring on an
incoming call. Previously, accepting or rejecting on one device left the
others still ringing.

Now, acceptCall() and rejectCall() also publish a self-addressed
gift-wrapped event (to the user's own pubkey) so other devices receive
it and transition out of the IncomingCall state. Added ANSWERED_ELSEWHERE
end reason and handling in onCallAnswered/onCallRejected for IncomingCall
state.

https://claude.ai/code/session_01X9juzyPYWqxRMCgDWEw8R1
2026-04-02 18:09:33 +00:00
Claude fe1a009fb4 fix: wire SDP renegotiation so video toggle works for remote peer
When toggling video mid-call, addVideoTrack triggers
onRenegotiationNeeded but the callback was empty — the remote peer
never received the updated SDP, so video never appeared on their side.

This wires up the full renegotiation flow using the existing
CallRenegotiateEvent (kind 25055):
- WebRtcCallSession: forward onRenegotiationNeeded to a callback
- CallController: create and send renegotiation offers, handle
  incoming renegotiation offers by creating answers
- CallManager: route CallRenegotiateEvent, allow CallAnswerEvent
  during Connected state for renegotiation answers

https://claude.ai/code/session_01WdmqktFBjXKB6PYRZ5b4FD
2026-04-02 17:05:10 +00:00
Vitor Pamplona 90288a1c78 Merge pull request #2068 from vitorpamplona/claude/add-webrtc-calls-4kBSR
Add WebRTC-based peer-to-peer voice and video calling via NIP-AC
2026-04-02 11:28:23 -04:00
Claude a524890a36 feat: NIP-53 full compliance - render all live activity kinds
- Add case-insensitive role matching in ParticipantTag (accepts both
  "host" and "Host" from other clients per NIP-53 spec)
- Add PARTICIPANT role to ROLE enum per NIP-53 spec
- Add MeetingSpaceEvent (30312), MeetingRoomEvent (30313), and
  MeetingRoomPresenceEvent (10312) consumption in LocalCache
- Create rendering composables for MeetingSpaceEvent and MeetingRoomEvent
  with status flags (OPEN/PRIVATE/CLOSED for spaces, LIVE/PLANNED/ENDED
  for rooms) and participant display
- Add meeting kinds to all discovery feed relay filters (global, authors,
  hashtag, geohash, community)
- Add meeting kinds to ChannelCardCompose and LiveActivityCard thumb
  rendering
- Add meeting kinds to KindRegistry search aliases

https://claude.ai/code/session_01CJ6xfY9gRA1c5r4T1SrUuv
2026-04-02 03:42:31 +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 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 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 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 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 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 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 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
Claude b960a4692a feat: migrate BookmarkListEvent from kind 30001 to 10003
Rename the existing BookmarkListEvent (kind 30001) to OldBookmarkListEvent
and create a new BookmarkListEvent with kind 10003 as per the updated spec.

- OldBookmarkListEvent (kind 30001): Kept as-is for backward compatibility,
  displayed as "Old Bookmarks" in the Bookmark Lists screen
- BookmarkListEvent (kind 10003): New replaceable event, all new bookmark
  operations (save/check/remove) use this kind
- Both kinds are displayed as separate items in the Bookmark Lists screen
- Old Bookmarks screen includes a "Move All to New Bookmarks" button that
  migrates public bookmarks to public and private to private
- Created PrivateReplaceableTagArrayEvent base class for replaceable events
  with encrypted private tags (kind 10003 is in the 10000-19999 range)
- Updated all relay filters, search queries, and profile views to fetch
  both kinds
- Updated Android and Desktop modules

https://claude.ai/code/session_01U9sjQHQMVVHxYiesoXjqop
2026-03-31 13:24:27 +00:00
Vitor Pamplona 26798e2b57 Fixes assertion 2026-03-31 08:59:38 -04:00
Vitor Pamplona 3dbd79d879 Merge pull request #2052 from vitorpamplona/claude/fix-sorting-order-p23ZY
Improve sorting stability by adding secondary sort keys
2026-03-31 08:42:46 -04:00
Vitor Pamplona 126d58e2d1 Merge pull request #2051 from vitorpamplona/claude/fix-quote-cursor-position-QOw9L
Fix quote placement to begin at start of message
2026-03-31 08:41:54 -04:00
davotoula 4d1e9cb8d5 Merge branch 'main-upstream' into chess-enhancements-and-bug-fixes 2026-03-31 07:26:39 +02:00
Claude 3a850ba26c fix: place cursor at beginning of text field when quoting a note
When a user hits Quote, the new post screen opens with the nostr URI
pre-filled but the cursor was at the end (after the URI). This moves
the cursor to the beginning so the user can immediately start typing
their commentary.

https://claude.ai/code/session_01RPseKC9GJ8hs3GGBR85ezw
2026-03-31 04:01:05 +00:00
Claude 96c4092138 fix: ensure secondary sort by id ascending when sorting by createdAt descending
When events share the same createdAt timestamp, the tiebreaker sort by id
must be ascending to produce a stable, deterministic order. Fixed several
locations that either had no secondary sort, used descending id sort, or
used .reversed() which incorrectly flipped both sort directions.

https://claude.ai/code/session_01RvuyPf1x9wLf2DCgsSXLGz
2026-03-31 03:45:24 +00:00
Vitor Pamplona f9d231063d Merge pull request #2040 from vitorpamplona/claude/migrate-textfield-state-6ezEZ
Migrate text fields from TextFieldValue to TextFieldState
2026-03-30 15:58:19 -04:00
Nguyen Van Nam bc491b154b docs: undocumented identity-based comparison in equalimmutablelists
`equalImmutableLists` is public and uses referential equality (`===`) per element rather than structural equality (`==`). Without documentation, callers may assume normal list equality and get false negatives for value-equal but distinct instances. This is a non-obvious contract and should be documented explicitly to prevent subtle logic bugs.


Affected files: ListUtils.kt

Signed-off-by: Nguyen Van Nam <nam.nv205106@gmail.com>
2026-03-30 22:48:41 +07:00
davotoula a7259b21ee Merge branch 'main-upstream' into chess-enhancements-and-bug-fixes 2026-03-30 17:40:40 +02:00
davotoula 40ce80786e update logging to new style 2026-03-30 14:50:42 +02:00
davotoula 17f071b0f4 Merge branch 'main' into chess-enhancements-and-bug-fixes
# Conflicts:
#	commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/chess/ChessEventBroadcaster.kt
2026-03-30 13:14:33 +02:00
nrobi144 2b3005797e feat(desktop): render reposts and quoted notes in feed
- Extract GenericRepostLayout + BoostedMark to commons for cross-platform use
- Feed filters accept kind 6/16 reposts with deduplication by original note
- Relay subscriptions request kinds 1, 6, 16
- Cache consumes GenericRepostEvent (kind 16) and uses getOrCreateNote for
  repost originals to handle out-of-order arrival
- FeedNoteCard renders reposts with overlapping avatars + "Boosted" label
- QuotedNoteEmbed renders nostr:nevent/note references as embedded NoteCards
  with reactive metadata observation
- Direct relay subscriptions fetch missing referenced notes and author metadata
- FeedMetadataCoordinator routes all events (not just metadata) back to cache
- consumeMetadata invalidates note flows when author metadata arrives

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:44:24 +03:00
Vitor Pamplona 03c692a8cf Merge pull request #2005 from davotoula/Introduce-log-level-filtering
Introduce log level filtering
2026-03-29 09:56:21 -04:00