In chess note cards (NoteCompose), player hex keys were displayed as
raw strings. Now uses LoadUser + ClickableUserPicture + UsernameDisplay
to show proper profile pictures and display names for:
- Challenge cards (incoming/outgoing)
- Game end cards (both players)
- PGN metadata in game viewers (white/black players)
Added playerContent composable slot to PGNMetadata and ChessGameViewer
so callers can inject platform-specific user rendering.
https://claude.ai/code/session_0171mKrVEfQnNRabmT7Kv4gf
- Clear discoveredCalleePeers on transitionToEnded to prevent stale
peers from triggering mesh setup in subsequent calls
- Cap processedEventIds and completedCallIds with LRU eviction to
prevent unbounded memory growth over long app sessions
- Store and release SurfaceTextureHelper in startCamera/stopCamera
to prevent GL thread and texture leaks
- Wrap stopCamera's stopCapture in try-catch for InterruptedException
to ensure camera resources are always released
- Add Mutex to CallManager to serialize state mutations, preventing
races when signaling events arrive concurrently from multiple relays
- Add @Synchronized to CallController's toggleAudioMute, toggleVideo,
and cleanup to prevent races with WebRTC callbacks
https://claude.ai/code/session_01TTPrYjcz1eYEzdSV5N6rHi
When the app restarts and reconnects to relays, old call offer events
from completed calls could be replayed, causing the phone to ring for
calls that already ended. This was especially noticeable when the user
killed and restarted the app shortly after a call.
Three protections added to CallManager:
1. Track completed call IDs: hangup/reject events mark their call-id as
completed. Subsequent offer events for the same call-id are ignored.
2. Init timestamp guard: events created before the CallManager was
initialized (minus a grace period) are rejected, preventing stale
events from a previous app session from triggering ringing.
3. Completed call IDs survive reset(): the set is intentionally not
cleared when the call state machine resets to Idle, ensuring that
stale offers remain blocked for the lifetime of the CallManager.
https://claude.ai/code/session_0145VKiG8yZMqcMsaBEjNPxv
Move transitionToEnded() before the signing + relay publish in hangup()
so the UI stops ringing/ringback immediately, matching the pattern
already used by rejectCall(). Add onDestroy safety net in CallActivity
to hang up if the Activity is destroyed while a call is active. Wrap
audio stop methods in try-catch to prevent one failure from blocking
the others.
https://claude.ai/code/session_01Rip2HPCbF48PPFDiB2X5ik
The publish KeyPackage button was always active because the app didn't
track whether a key package had already been published. This adds:
- hasActiveKeyPackages() to KeyPackageRotationManager and MarmotManager
- hasPublishedKeyPackage() to Account, checking both in-memory bundles
and the local cache for existing kind:30443 events
- Own key package filter in MarmotSubscriptionManager and the EOSE
manager so previously published key packages are downloaded from
relays on app restart
- UI feedback: primary-colored key icon when published, contextual
empty-state message, and a spinner during publishing
https://claude.ai/code/session_01BVe7aSEWd2KLi5Ks6RZkcc
Implement end-to-end reading of MarmotGroupData (extension 0xF2EE)
from the MLS GroupContext.extensions and sync to the UI layer.
Quartz (protocol):
- MarmotGroupData.decodeTls(): deserialize from TLS wire format
(version, nostrGroupId, name, description, admin pubkeys, relays,
image fields)
- MarmotGroupData.fromExtensions(): find and decode from extension list
- MlsGroup.extensions: expose groupContext.extensions publicly
Commons (shared logic):
- MarmotGroupChatroom: add description, adminPubkeys, relays fields
as MutableStateFlow for reactive UI
- MarmotManager.groupMetadata(): extract MIP-01 data from group
- MarmotManager.syncMetadataTo(): sync metadata + member count to
a MarmotGroupChatroom
Sync points (amethyst):
- Account init: sync metadata after restoreAll() for all restored groups
- GiftWrapEventHandler: sync after Welcome processing (group join)
- GroupEventHandler: sync after CommitProcessed (epoch advances may
update extensions via GroupContextExtensions proposals)
UI (GroupInfoScreen):
- Display group description from MIP-01 metadata
- Display relay URLs from MIP-01 metadata
- Mark admin members with "- admin" suffix (from adminPubkeys)
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Based on MIP spec review, add missing UI screens:
MarmotManager additions (commons):
- memberPubkeys(): extract Nostr pubkeys from MLS ratchet tree
leaf nodes via Credential.Basic identity
- memberCount(): get group member count
- groupEpoch(): expose current MLS epoch
- GroupMemberInfo data class for leaf index + pubkey pairs
Account & AccountViewModel:
- leaveMarmotGroup(): publish SelfRemove proposal and clean up
- marmotGroupMembers(): expose member list to UI
MarmotGroupInfoScreen (new):
- Group metadata header (name, ID, epoch, member count)
- Member list with leaf indices, clickable to view profiles
- "Leave Group" with confirmation dialog
- "Add Member" access from group info
Navigation:
- Route.MarmotGroupInfo added and wired into nav graph
- Chat screen title now clickable to navigate to group info
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Add complete UI layer for Marmot Protocol MLS group messaging:
State models (commons/commonMain/):
- MarmotGroupChatroom: tracks decrypted inner messages per group
- MarmotGroupList: manages all group chatrooms with change notifications
- MarmotGroupFeedFilter: feed filter for group message feeds
- MarmotGroupFeedViewModel: ViewModel exposing group messages
Account integration:
- Add marmotGroupList to IAccount interface and implementations
- GroupEventHandler indexes inner events into MarmotGroupList
- AccountViewModel gains sendMarmotGroupMessage, createMarmotGroup,
publishMarmotKeyPackage helper methods
Android screens (amethyst/):
- MarmotGroupListScreen: lists all groups with last message preview
- MarmotGroupChatScreen: group conversation with message composer
- MarmotGroupChatView: message feed + simple text composer
- CreateGroupDialog: create new MLS group with display name
- AddMemberDialog: add member by npub/hex (KeyPackage fetch TBD)
Navigation & integration:
- Route.MarmotGroupList and Route.MarmotGroupChat routes
- Wired into AppNavigation nav graph
- "MLS Groups" button added to ChannelFabColumn on Messages screen
- Marmot group newest messages appear in ChatroomListKnownFeedFilter
https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM
Wire Marmot MLS group messaging into the app's Account lifecycle,
event processing pipeline, and relay subscription system.
New files:
- AndroidMlsGroupStateStore: encrypted file-based MLS state storage
using KeyStoreEncryption (AES/GCM backed by Android KeyStore)
- MarmotManager: central coordinator holding all Marmot components
(MlsGroupManager, KeyPackageRotationManager, subscription/inbound/
outbound processors, WelcomeSender)
- MarmotGroupEventsEoseManager: relay subscription manager for
kind:445 GroupEvent filters via AccountFilterAssembler
Account integration:
- MarmotManager initialized during Account startup with restoreAll()
- Outbound methods: sendMarmotGroupMessage, addMarmotGroupMember,
publishMarmotKeyPackage(s), createMarmotGroup
- GroupEvent (kind:445) and KeyPackageEvent (kind:30443) added to
LocalCache.justConsumeInnerInner() dispatch
Event routing:
- GroupEventHandler: processes inbound kind:445 events through
MarmotInboundProcessor, indexes decrypted inner events in LocalCache
- GiftWrapEventHandler extended: detects kind:444 WelcomeEvent after
NIP-59 unwrap, routes to MarmotManager.processWelcome(), triggers
KeyPackage rotation when needed
- WelcomeEvent enhanced with optional "h" tag for group ID routing
https://claude.ai/code/session_01W2LHazEt4E3W4hn8f7gWVW
- Split PeerSession.kt out of PeerSessionManager.kt (types, interface,
manager are now in separate files)
- Remove webRtcSessions duplication in CallController — PeerSessionManager
is now the single source of truth for session tracking; WebRtcCallSession
is retrieved via the adapter cast when WebRTC-specific APIs are needed
- Initialize PeerSessionManager eagerly with localPubKey (passed to
CallController constructor) instead of lazy suspend init — fixes early
ICE candidates being silently dropped before first suspend call
- Extract FakePeerSession into its own file for reuse across test files
- Remove assertion-only glare tiebreaker tests from NipACStateMachineTest
(now properly tested with real logic in PeerSessionManagerTest)
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
- ThumbnailDiskCache: combine get()+decodeThumbnail() into single load()
method. Move bitmap resize logic into generateFromFile() so callers
just pass a source file and the cache handles decode+resize+save.
- ProfilePictureFetcher: remove unused `options` field, remove @Stable
annotation, delegate all resize logic to ThumbnailDiskCache.
- UserAvatar: fix stale doc comment.
https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
Replace profilepic:// string scheme with a ProfilePictureUrl data class.
Coil routes to the fetcher by type via Fetcher.Factory<ProfilePictureUrl>
instead of parsing a URI scheme string on every load. This avoids string
concatenation at the call site and string parsing in the fetcher/keyer.
https://claude.ai/code/session_01PBJS3HDrurLP3i5n4tMsCv
Extract the ICE candidate buffering, answer routing, and renegotiation
glare logic from CallController into a platform-independent
PeerSessionManager in commons/. This makes the three most critical
NIP-AC spec requirements testable as JVM unit tests with FakePeerSession:
1. Two-layer ICE candidate buffering (global + per-session)
- Global buffer: candidates arriving before any PeerConnection exists
- Per-session buffer: candidates arriving before setRemoteDescription
- Candidates buffered while ringing are preserved when accepting
2. Renegotiation glare handling (pubkey comparison tiebreaker)
- Higher pubkey wins, lower pubkey rolls back local offer
- Full glare scenario test with two PeerSessionManagers
3. Callee-to-callee mesh initiation (lower pubkey initiates)
CallController now delegates to PeerSessionManager via a PeerSession
interface, with WebRtcPeerSessionAdapter bridging to WebRtcCallSession.
https://claude.ai/code/session_01AfRYTRCvtKqqDxeKQujUrx
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
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
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
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
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
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
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
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
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
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
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
- 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
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
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
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
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
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
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
- 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