@capgo/capacitor-stream-call 0.0.6 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,8 @@ import android.app.Application
6
6
  import android.app.KeyguardManager
7
7
  import android.content.Context
8
8
  import android.graphics.Color
9
+ import android.media.RingtoneManager
10
+ import android.net.Uri
9
11
  import android.os.Bundle
10
12
  import android.os.Handler
11
13
  import android.os.Looper
@@ -20,13 +22,11 @@ import com.getcapacitor.Plugin
20
22
  import com.getcapacitor.PluginCall
21
23
  import com.getcapacitor.PluginMethod
22
24
  import com.getcapacitor.annotation.CapacitorPlugin
23
- import io.getstream.android.push.firebase.FirebasePushDeviceGenerator
24
25
  import io.getstream.android.push.permissions.ActivityLifecycleCallbacks
25
26
  import io.getstream.video.android.core.Call
26
27
  import io.getstream.video.android.core.GEO
27
28
  import io.getstream.video.android.core.StreamVideo
28
29
  import io.getstream.video.android.core.StreamVideoBuilder
29
- import io.getstream.video.android.core.model.RejectReason
30
30
  import io.getstream.video.android.core.notifications.NotificationConfig
31
31
  import io.getstream.video.android.core.notifications.NotificationHandler
32
32
  import io.getstream.video.android.core.sounds.emptyRingingConfig
@@ -35,16 +35,21 @@ import io.getstream.video.android.model.StreamCallId
35
35
  import io.getstream.video.android.model.User
36
36
  import io.getstream.video.android.model.streamCallId
37
37
  import kotlinx.coroutines.DelicateCoroutinesApi
38
- import kotlinx.coroutines.flow.Flow
39
38
  import kotlinx.coroutines.launch
40
- import org.openapitools.client.models.CallAcceptedEvent
41
- import org.openapitools.client.models.CallEndedEvent
42
- import org.openapitools.client.models.CallMissedEvent
43
- import org.openapitools.client.models.CallRejectedEvent
44
- import org.openapitools.client.models.CallSessionEndedEvent
45
- import org.openapitools.client.models.VideoEvent
46
39
  import io.getstream.video.android.model.Device
47
- import kotlinx.coroutines.flow.first
40
+ import kotlinx.coroutines.tasks.await
41
+ import com.google.firebase.messaging.FirebaseMessaging
42
+ import io.getstream.android.push.PushProvider
43
+ import io.getstream.android.push.firebase.FirebasePushDeviceGenerator
44
+ import io.getstream.android.video.generated.models.CallAcceptedEvent
45
+ import io.getstream.android.video.generated.models.CallCreatedEvent
46
+ import io.getstream.android.video.generated.models.CallEndedEvent
47
+ import io.getstream.android.video.generated.models.CallMissedEvent
48
+ import io.getstream.android.video.generated.models.CallRejectedEvent
49
+ import io.getstream.android.video.generated.models.CallSessionEndedEvent
50
+ import io.getstream.android.video.generated.models.CallSessionStartedEvent
51
+ import io.getstream.android.video.generated.models.VideoEvent
52
+ import io.getstream.video.android.core.sounds.RingingConfig
48
53
 
49
54
  // I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
50
55
  // It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
@@ -63,7 +68,12 @@ public class StreamCallPlugin : Plugin() {
63
68
  private var savedActivity: Activity? = null
64
69
  private var savedActivityPaused = false
65
70
  private var savedCallsToEndOnResume = mutableListOf<Call>()
66
- private val callStates: MutableMap<String, CallState> = mutableMapOf()
71
+ private val callStates: MutableMap<String, LocalCallState> = mutableMapOf()
72
+
73
+ // Store current call info
74
+ private var currentCallId: String = ""
75
+ private var currentCallType: String = ""
76
+ private var currentCallState: String = ""
67
77
 
68
78
  private enum class State {
69
79
  NOT_INITIALIZED,
@@ -71,6 +81,11 @@ public class StreamCallPlugin : Plugin() {
71
81
  INITIALIZED
72
82
  }
73
83
 
84
+ public fun incomingOnlyRingingConfig(): RingingConfig = object : RingingConfig {
85
+ override val incomingCallSoundUri: Uri? = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
86
+ override val outgoingCallSoundUri: Uri? = null
87
+ }
88
+
74
89
  private fun runOnMainThread(action: () -> Unit) {
75
90
  mainHandler.post { action() }
76
91
  }
@@ -211,12 +226,8 @@ public class StreamCallPlugin : Plugin() {
211
226
  // Stop ringtone
212
227
  ringtonePlayer?.stopRinging()
213
228
 
214
- // Notify that call has ended
215
- val data = JSObject().apply {
216
- put("callId", call.id)
217
- put("state", "rejected")
218
- }
219
- notifyListeners("callEvent", data)
229
+ // Notify that call has ended using our helper
230
+ updateCallStatusAndNotify(call.id, "rejected")
220
231
 
221
232
  hideIncomingCall()
222
233
  } catch (e: Exception) {
@@ -250,14 +261,6 @@ public class StreamCallPlugin : Plugin() {
250
261
  }
251
262
  }
252
263
 
253
- // private fun remoteIncomingCallNotif() {
254
- // CallService.removeIncomingCall(
255
- // context,
256
- // StreamCallId.fromCallCid(call.cid),
257
- // StreamVideo.instance().state.callConfigRegistry.get(call.type),
258
- // )
259
- // }
260
-
261
264
  private fun setupViews() {
262
265
  val context = context
263
266
  val parent = bridge?.webView?.parent as? ViewGroup ?: return
@@ -468,16 +471,17 @@ public class StreamCallPlugin : Plugin() {
468
471
  )
469
472
 
470
473
  val notificationConfig = NotificationConfig(
471
- pushDeviceGenerators = listOf(FirebasePushDeviceGenerator(
474
+ pushDeviceGenerators = listOf(
475
+ FirebasePushDeviceGenerator(
472
476
  providerName = "firebase",
473
477
  context = contextToUse
474
- )),
478
+ )
479
+ ),
475
480
  requestPermissionOnAppLaunch = { true },
476
481
  notificationHandler = notificationHandler,
477
482
  )
478
483
 
479
- val soundsConfig = emptyRingingConfig()
480
- soundsConfig.incomingCallSoundUri
484
+ val soundsConfig = incomingOnlyRingingConfig()
481
485
  // Initialize StreamVideo client
482
486
  streamVideoClient = StreamVideoBuilder(
483
487
  context = contextToUse,
@@ -562,61 +566,37 @@ public class StreamCallPlugin : Plugin() {
562
566
  android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
563
567
  when (event) {
564
568
  // Handle CallCreatedEvent differently - only log it but don't try to access members yet
565
- is org.openapitools.client.models.CallCreatedEvent -> {
566
- val callCid = event.call.cid
569
+ is CallCreatedEvent -> {
570
+ val callCid = event.callCid
567
571
  android.util.Log.d("StreamCallPlugin", "Call created: $callCid")
568
572
 
569
573
  // let's get the members
570
- val callParticipants = event.members.filter{ it.user.id != this@StreamCallPlugin.streamVideoClient?.userId } .map { it.user.id }
574
+ val callParticipants = event.members.filter{ it.user.id != this@StreamCallPlugin.streamVideoClient?.userId }.map { it.user.id }
571
575
  android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} participants")
572
576
 
573
577
  // Start tracking this call now that we have the member list
574
578
  startCallTimeoutMonitor(callCid, callParticipants)
575
579
 
576
- val data = JSObject().apply {
577
- put("callId", callCid)
578
- put("state", "created")
579
- }
580
- notifyListeners("callEvent", data)
580
+ // Use direction from event if available
581
+ val callType = callCid.split(":").firstOrNull() ?: "default"
582
+ updateCallStatusAndNotify(callCid, "created")
581
583
  }
582
584
  // Add handler for CallSessionStartedEvent which contains participant information
583
- is org.openapitools.client.models.CallSessionStartedEvent -> {
584
- val callCid = event.call.cid
585
-
586
- val data = JSObject().apply {
587
- put("callId", callCid)
588
- put("state", "session_started")
589
- }
590
- notifyListeners("callEvent", data)
585
+ is CallSessionStartedEvent -> {
586
+ val callCid = event.callCid
587
+ updateCallStatusAndNotify(callCid, "session_started")
591
588
  }
592
589
 
593
590
  is CallRejectedEvent -> {
594
591
  val userId = event.user.id
595
- val callCid = event.call.cid
592
+ val callCid = event.callCid
596
593
 
597
594
  // Update call state
598
595
  callStates[callCid]?.let { callState ->
599
596
  callState.participantResponses[userId] = "rejected"
600
597
  }
601
598
 
602
- // runOnMainThread {
603
- // android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallRejectedEvent for call ${event.call.cid}")
604
- // overlayView?.setContent {
605
- // CallOverlayView(
606
- // context = context,
607
- // streamVideo = streamVideoClient,
608
- // call = null
609
- // )
610
- // }
611
- // overlayView?.isVisible = false
612
- // }
613
-
614
- val data = JSObject().apply {
615
- put("callId", event.call.cid)
616
- put("state", "rejected")
617
- put("userId", userId)
618
- }
619
- notifyListeners("callEvent", data)
599
+ updateCallStatusAndNotify(callCid, "rejected", userId)
620
600
 
621
601
  // Check if all participants have responded
622
602
  checkAllParticipantsResponded(callCid)
@@ -624,7 +604,7 @@ public class StreamCallPlugin : Plugin() {
624
604
 
625
605
  is CallMissedEvent -> {
626
606
  val userId = event.user.id
627
- val callCid = event.call.cid
607
+ val callCid = event.callCid
628
608
 
629
609
  // Update call state
630
610
  callStates[callCid]?.let { callState ->
@@ -638,12 +618,7 @@ public class StreamCallPlugin : Plugin() {
638
618
  moveAllActivitiesToBackgroundOrKill(context)
639
619
  }
640
620
 
641
- val data = JSObject().apply {
642
- put("callId", callCid)
643
- put("state", "missed")
644
- put("userId", userId)
645
- }
646
- notifyListeners("callEvent", data)
621
+ updateCallStatusAndNotify(callCid, "missed", userId)
647
622
 
648
623
  // Check if all participants have responded
649
624
  checkAllParticipantsResponded(callCid)
@@ -651,7 +626,7 @@ public class StreamCallPlugin : Plugin() {
651
626
 
652
627
  is CallAcceptedEvent -> {
653
628
  val userId = event.user.id
654
- val callCid = event.call.cid
629
+ val callCid = event.callCid
655
630
 
656
631
  // Update call state
657
632
  callStates[callCid]?.let { callState ->
@@ -663,48 +638,34 @@ public class StreamCallPlugin : Plugin() {
663
638
  callState.timer = null
664
639
  }
665
640
 
666
- val data = JSObject().apply {
667
- put("callId", callCid)
668
- put("state", "accepted")
669
- put("userId", userId)
670
- }
671
- notifyListeners("callEvent", data)
641
+ updateCallStatusAndNotify(callCid, "accepted", userId)
672
642
  }
673
643
 
674
644
  is CallEndedEvent -> {
675
645
  runOnMainThread {
676
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.call.cid}")
646
+ android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.callCid}")
677
647
  // Clean up call resources
678
- val callCid = event.call.cid
648
+ val callCid = event.callCid
679
649
  cleanupCall(callCid)
680
650
  }
681
- val data = JSObject().apply {
682
- put("callId", event.call.cid)
683
- put("state", "left")
684
- }
685
- notifyListeners("callEvent", data)
651
+ updateCallStatusAndNotify(event.callCid, "left")
686
652
  }
687
653
 
688
654
  is CallSessionEndedEvent -> {
689
655
  runOnMainThread {
690
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.call.cid}")
656
+ android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.callCid}. Test session: ${event.call.session?.endedAt}")
691
657
  // Clean up call resources
692
- val callCid = event.call.cid
658
+ val callCid = event.callCid
693
659
  cleanupCall(callCid)
694
660
  }
695
- val data = JSObject().apply {
696
- put("callId", event.call.cid)
697
- put("state", "left")
698
- }
699
- notifyListeners("callEvent", data)
661
+ updateCallStatusAndNotify(event.callCid, "left")
700
662
  }
701
663
 
702
664
  else -> {
703
- val data = JSObject().apply {
704
- put("callId", streamVideoClient?.state?.activeCall?.value?.cid)
705
- put("state", event.getEventType())
706
- }
707
- notifyListeners("callEvent", data)
665
+ updateCallStatusAndNotify(
666
+ streamVideoClient?.state?.activeCall?.value?.cid ?: "",
667
+ event.getEventType()
668
+ )
708
669
  }
709
670
  }
710
671
  }
@@ -721,19 +682,11 @@ public class StreamCallPlugin : Plugin() {
721
682
  android.util.Log.d("StreamCallPlugin", "- All participants: ${state.participants}")
722
683
  android.util.Log.d("StreamCallPlugin", "- Remote participants: ${state.remoteParticipants}")
723
684
 
724
- // Notify that a call has started
725
- val data = JSObject().apply {
726
- put("callId", call.cid)
727
- put("state", "joined")
728
- }
729
- notifyListeners("callEvent", data)
685
+ // Notify that a call has started using our helper
686
+ updateCallStatusAndNotify(call.cid, "joined")
730
687
  } ?: run {
731
- // Notify that call has ended
732
- val data = JSObject().apply {
733
- put("callId", "")
734
- put("state", "left")
735
- }
736
- notifyListeners("callEvent", data)
688
+ // Notify that call has ended using our helper
689
+ updateCallStatusAndNotify("", "left")
737
690
  }
738
691
  }
739
692
  }
@@ -807,12 +760,8 @@ public class StreamCallPlugin : Plugin() {
807
760
  // Join the call without affecting others
808
761
  call.accept()
809
762
 
810
- // Notify that call has started
811
- val data = JSObject().apply {
812
- put("callId", call.id)
813
- put("state", "joined")
814
- }
815
- notifyListeners("callEvent", data)
763
+ // Notify that call has started using helper
764
+ updateCallStatusAndNotify(call.id, "joined")
816
765
 
817
766
  // Show overlay view with the active call
818
767
  runOnMainThread {
@@ -935,7 +884,6 @@ public class StreamCallPlugin : Plugin() {
935
884
  val callId = call.id
936
885
  android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
937
886
  call.leave()
938
- call.reject(reason = RejectReason.Cancel)
939
887
 
940
888
  // Capture context from the overlayView
941
889
  val currentContext = overlayView?.context ?: this.savedContext
@@ -988,12 +936,8 @@ public class StreamCallPlugin : Plugin() {
988
936
  incomingCallView?.isVisible = false
989
937
  }
990
938
 
991
- // Notify that call has ended
992
- val data = JSObject().apply {
993
- put("callId", callId)
994
- put("state", "left")
995
- }
996
- notifyListeners("callEvent", data)
939
+ // Notify that call has ended using helper
940
+ updateCallStatusAndNotify(callId, "left")
997
941
  }
998
942
 
999
943
  @OptIn(DelicateCoroutinesApi::class)
@@ -1079,7 +1023,7 @@ public class StreamCallPlugin : Plugin() {
1079
1023
  val callType = call.getString("type") ?: "default"
1080
1024
  val shouldRing = call.getBoolean("ring") ?: true
1081
1025
  val callId = java.util.UUID.randomUUID().toString()
1082
- val callCid = "$callType:$callId"
1026
+ val team = call.getString("team");
1083
1027
 
1084
1028
  android.util.Log.d("StreamCallPlugin", "Creating call:")
1085
1029
  android.util.Log.d("StreamCallPlugin", "- Call ID: $callId")
@@ -1098,15 +1042,23 @@ public class StreamCallPlugin : Plugin() {
1098
1042
 
1099
1043
  android.util.Log.d("StreamCallPlugin", "Creating call with members...")
1100
1044
  // Create the call with all members
1101
- streamCall?.create(
1045
+ val createResult = streamCall?.create(
1102
1046
  memberIds = userIds + selfUserId,
1103
1047
  custom = emptyMap(),
1104
- ring = shouldRing
1048
+ ring = shouldRing,
1049
+ team = team,
1105
1050
  )
1051
+
1052
+ if (createResult?.isFailure == true) {
1053
+ throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
1054
+ }
1106
1055
 
1107
1056
  android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
1108
1057
  // Show overlay view
1109
1058
  activity?.runOnUiThread {
1059
+ streamCall?.microphone?.setEnabled(true)
1060
+ streamCall?.camera?.setEnabled(true)
1061
+
1110
1062
  overlayView?.setContent {
1111
1063
  CallOverlayView(
1112
1064
  context = context,
@@ -1132,7 +1084,7 @@ public class StreamCallPlugin : Plugin() {
1132
1084
  }
1133
1085
 
1134
1086
  private fun startCallTimeoutMonitor(callCid: String, memberIds: List<String>) {
1135
- val callState = CallState(members = memberIds)
1087
+ val callState = LocalCallState(members = memberIds)
1136
1088
 
1137
1089
  val handler = Handler(Looper.getMainLooper())
1138
1090
  val timeoutRunnable = object : Runnable {
@@ -1172,12 +1124,7 @@ public class StreamCallPlugin : Plugin() {
1172
1124
  if (memberId !in callState.participantResponses) {
1173
1125
  callState.participantResponses[memberId] = "missed"
1174
1126
 
1175
- val data = JSObject().apply {
1176
- put("callId", callCid)
1177
- put("state", "missed")
1178
- put("userId", memberId)
1179
- }
1180
- notifyListeners("callEvent", data)
1127
+ updateCallStatusAndNotify(callCid, "missed", memberId)
1181
1128
  }
1182
1129
  }
1183
1130
 
@@ -1195,13 +1142,8 @@ public class StreamCallPlugin : Plugin() {
1195
1142
  // Clean up state - we don't need to do this in endCallRaw because we already did it here
1196
1143
  callStates.remove(callCid)
1197
1144
 
1198
- // Notify that call has ended
1199
- val data = JSObject().apply {
1200
- put("callId", callCid)
1201
- put("state", "ended")
1202
- put("reason", "timeout")
1203
- }
1204
- notifyListeners("callEvent", data)
1145
+ // Notify that call has ended using helper
1146
+ updateCallStatusAndNotify(callCid, "ended", null, "timeout")
1205
1147
  } catch (e: Exception) {
1206
1148
  android.util.Log.e("StreamCallPlugin", "Error ending timed out call", e)
1207
1149
  }
@@ -1271,13 +1213,8 @@ public class StreamCallPlugin : Plugin() {
1271
1213
  // Clean up state - we don't need to do this in endCallRaw because we already did it here
1272
1214
  callStates.remove(callCid)
1273
1215
 
1274
- // Notify that call has ended
1275
- val data = JSObject().apply {
1276
- put("callId", callCid)
1277
- put("state", "ended")
1278
- put("reason", "all_rejected_or_missed")
1279
- }
1280
- notifyListeners("callEvent", data)
1216
+ // Notify that call has ended using helper
1217
+ updateCallStatusAndNotify(callCid, "ended", null, "all_rejected_or_missed")
1281
1218
  } catch (e: Exception) {
1282
1219
  android.util.Log.e("StreamCallPlugin", "Error ending call after all rejected/missed", e)
1283
1220
  }
@@ -1289,68 +1226,68 @@ public class StreamCallPlugin : Plugin() {
1289
1226
 
1290
1227
  private suspend fun magicDeviceDelete(streamVideoClient: StreamVideo) {
1291
1228
  try {
1292
- android.util.Log.d("StreamCallPlugin", "Starting magicDeviceDelete reflection operation")
1293
-
1294
- // Get the streamNotificationManager field from StreamVideo
1295
- val streamVideoClass = streamVideoClient.javaClass
1296
- val notificationManagerField = streamVideoClass.getDeclaredField("streamNotificationManager")
1297
- notificationManagerField.isAccessible = true
1298
- val notificationManager = notificationManagerField.get(streamVideoClient)
1299
-
1300
- if (notificationManager == null) {
1301
- android.util.Log.e("StreamCallPlugin", "streamNotificationManager is null")
1302
- return
1303
- }
1304
-
1305
- android.util.Log.d("StreamCallPlugin", "Successfully accessed streamNotificationManager")
1306
-
1307
- // Get deviceTokenStorage from notification manager
1308
- val notificationManagerClass = notificationManager.javaClass
1309
- val deviceTokenStorageField = notificationManagerClass.getDeclaredField("deviceTokenStorage")
1310
- deviceTokenStorageField.isAccessible = true
1311
- val deviceTokenStorage = deviceTokenStorageField.get(notificationManager)
1312
-
1313
- if (deviceTokenStorage == null) {
1314
- android.util.Log.e("StreamCallPlugin", "deviceTokenStorage is null")
1315
- return
1316
- }
1317
-
1318
- android.util.Log.d("StreamCallPlugin", "Successfully accessed deviceTokenStorage")
1319
-
1320
- // Access the DeviceTokenStorage object dynamically without hardcoding class
1321
- val deviceTokenStorageClass = deviceTokenStorage.javaClass
1322
-
1323
- // Get the userDevice Flow from deviceTokenStorage
1324
- val userDeviceField = deviceTokenStorageClass.getDeclaredField("userDevice")
1325
- userDeviceField.isAccessible = true
1326
- val userDeviceFlow = userDeviceField.get(deviceTokenStorage)
1327
-
1328
- if (userDeviceFlow == null) {
1329
- android.util.Log.e("StreamCallPlugin", "userDevice Flow is null")
1330
- return
1331
- }
1332
-
1333
- android.util.Log.d("StreamCallPlugin", "Successfully accessed userDevice Flow: $userDeviceFlow")
1229
+ android.util.Log.d("StreamCallPlugin", "Starting magicDeviceDelete operation")
1230
+
1231
+ FirebaseMessaging.getInstance().token.await()?.let {
1232
+ android.util.Log.d("StreamCallPlugin", "Found firebase token")
1233
+ val device = Device(
1234
+ id = it,
1235
+ pushProvider = PushProvider.FIREBASE.key,
1236
+ pushProviderName = "firebase",
1237
+ )
1334
1238
 
1335
- val castedUserDeviceFlow = userDeviceFlow as Flow<Device?>
1336
- try {
1337
- castedUserDeviceFlow.first {
1338
- if (it == null) {
1339
- android.util.Log.d("StreamCallPlugin", "Device is null. Nothing to remove")
1340
- return@first true;
1341
- }
1342
- streamVideoClient.deleteDevice(it)
1343
- return@first true;
1344
- }
1345
- } catch (e: Throwable) {
1346
- android.util.Log.e("StreamCallPlugin", "Cannot collect flow in magicDeviceDelete", e)
1239
+ streamVideoClient.deleteDevice(device)
1347
1240
  }
1348
1241
  } catch (e: Exception) {
1349
1242
  android.util.Log.e("StreamCallPlugin", "Error in magicDeviceDelete", e)
1350
1243
  }
1351
1244
  }
1352
1245
 
1353
- data class CallState(
1246
+ @PluginMethod
1247
+ fun getCallStatus(call: PluginCall) {
1248
+ // If not in a call, reject
1249
+ if (currentCallId.isEmpty() || currentCallState == "left") {
1250
+ call.reject("Not in a call")
1251
+ return
1252
+ }
1253
+
1254
+ val result = JSObject()
1255
+ result.put("callId", currentCallId)
1256
+ result.put("state", currentCallState)
1257
+
1258
+ // No additional fields to ensure compatibility with CallEvent interface
1259
+
1260
+ call.resolve(result)
1261
+ }
1262
+
1263
+ // Helper method to update call status and notify listeners
1264
+ private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null) {
1265
+ // Update stored call info
1266
+ currentCallId = callId
1267
+ currentCallState = state
1268
+
1269
+ // Get call type from call ID if available
1270
+ if (callId.contains(":")) {
1271
+ currentCallType = callId.split(":").firstOrNull() ?: ""
1272
+ }
1273
+
1274
+ // Create data object with only the fields in the CallEvent interface
1275
+ val data = JSObject().apply {
1276
+ put("callId", callId)
1277
+ put("state", state)
1278
+ userId?.let {
1279
+ put("userId", it)
1280
+ }
1281
+ reason?.let {
1282
+ put("reason", it)
1283
+ }
1284
+ }
1285
+
1286
+ // Notify listeners
1287
+ notifyListeners("callEvent", data)
1288
+ }
1289
+
1290
+ data class LocalCallState(
1354
1291
  val members: List<String>,
1355
1292
  val participantResponses: MutableMap<String, String> = mutableMapOf(),
1356
1293
  val createdAt: Long = System.currentTimeMillis(),