From d93b384f72ee28b5be96705662fb99961f43b31d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 01:12:04 +0000 Subject: [PATCH] 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 --- .../ui/screen/loggedIn/AccountViewModel.kt | 24 +------------------ .../loggedIn/DecryptAndIndexProcessor.kt | 21 ++++++++++++++++ .../chats/privateDM/ChatroomScreen.kt | 11 +++++++-- .../privateDM/header/RenderRoomTopBar.kt | 16 +++++++++++++ 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 7a740b2af..acdea1d56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -146,12 +146,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent -import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @@ -222,6 +216,7 @@ class AccountViewModel( ) callManager.onAnswerReceived = { event -> controller.onCallAnswerReceived(event.sdpAnswer()) } callManager.onIceCandidateReceived = { event -> controller.onIceCandidateReceived(event) } + account.newNotesPreProcessor.callManager = callManager callController = controller } @@ -1415,23 +1410,6 @@ class AccountViewModel( } } } - - viewModelScope.launch(Dispatchers.IO) { - LocalCache.live.newEventBundles.collect { newNotes -> - newNotes.forEach { note -> - val event = note.event ?: return@forEach - when (event) { - is CallOfferEvent, - is CallAnswerEvent, - is CallIceCandidateEvent, - is CallHangupEvent, - is CallRejectEvent, - is CallRenegotiateEvent, - -> callManager.onSignalingEvent(event) - } - } - } - } } override fun onCleared() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index a569b5ac9..8b6c15a4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.commons.call.CallManager import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache @@ -36,6 +37,12 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent +import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException @@ -52,6 +59,8 @@ class EventProcessor( private val zapRequest = LnZapRequestEventHandler(account.privateZapsDecryptionCache) private val zapEvent = LnZapEventHandler(account.privateZapsDecryptionCache) + var callManager: CallManager? = null + suspend fun consume(note: Note) { note.event?.let { event -> try { @@ -68,10 +77,22 @@ class EventProcessor( publicNote: Note, ) { when (event) { + is CallOfferEvent, + is CallAnswerEvent, + is CallIceCandidateEvent, + is CallHangupEvent, + is CallRejectEvent, + is CallRenegotiateEvent, + -> callManager?.onSignalingEvent(event) + is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote) + is DraftWrapEvent -> draftHandler.add(event, eventNote, publicNote) + is GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote) + is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote) + is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt index e338e5747..d41e37ee8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt @@ -48,12 +48,18 @@ fun ChatroomScreen( nav: INav, ) { val context = LocalContext.current - val startCall = + val startVoiceCall = rememberCallWithPermission(context) { val peerPubKey = roomId.users.firstOrNull() ?: return@rememberCallWithPermission accountViewModel.callController?.initiateCall(peerPubKey, CallType.VOICE) nav.nav(Route.ActiveCall(callId = "", peerPubKey = peerPubKey)) } + val startVideoCall = + rememberCallWithPermission(context) { + val peerPubKey = roomId.users.firstOrNull() ?: return@rememberCallWithPermission + accountViewModel.callController?.initiateCall(peerPubKey, CallType.VIDEO) + nav.nav(Route.ActiveCall(callId = "", peerPubKey = peerPubKey)) + } DisappearingScaffold( isInvertedLayout = true, @@ -62,7 +68,8 @@ fun ChatroomScreen( room = roomId, accountViewModel = accountViewModel, nav = nav, - onCallClick = { _ -> startCall() }, + onCallClick = { _ -> startVoiceCall() }, + onVideoCallClick = { _ -> startVideoCall() }, ) }, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt index 8ec60cc3f..739338e76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Call import androidx.compose.material.icons.filled.EditNote +import androidx.compose.material.icons.filled.Videocam import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -74,6 +75,7 @@ fun RenderRoomTopBar( accountViewModel: AccountViewModel, nav: INav, onCallClick: ((String) -> Unit)? = null, + onVideoCallClick: ((String) -> Unit)? = null, ) { if (room.users.size == 1) { TopBarExtensibleWithBackButton( @@ -90,6 +92,20 @@ fun RenderRoomTopBar( UsernameDisplay(baseUser, Modifier.weight(1f), fontWeight = FontWeight.Normal, accountViewModel = accountViewModel) + if (onVideoCallClick != null) { + IconButton( + onClick = { onVideoCallClick(baseUser.pubkeyHex) }, + modifier = Modifier.size(40.dp), + ) { + Icon( + imageVector = Icons.Default.Videocam, + contentDescription = "Video call", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + } + } + if (onCallClick != null) { IconButton( onClick = { onCallClick(baseUser.pubkeyHex) },