diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt new file mode 100644 index 0000000000..387d4a98ad --- /dev/null +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCall.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.api + +import io.element.android.libraries.matrix.api.core.RoomId + +/** + * Value for the local current call. + */ +sealed interface CurrentCall { + data object None : CurrentCall + + data class RoomCall( + val roomId: RoomId, + ) : CurrentCall + + data class ExternalUrl( + val url: String, + ) : CurrentCall +} diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt new file mode 100644 index 0000000000..c0f8eb35a8 --- /dev/null +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallObserver.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.api + +import kotlinx.coroutines.flow.StateFlow + +interface CurrentCallObserver { + /** + * The current call state flow, which will be updated when the active call changes. + * This value reflect the local state of the call. It is not updated if the user answers + * a call from another session. + */ + val currentCall: StateFlow +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index 45f1f2be63..ccdbc1b91a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.libraries.di.AppScope @@ -82,6 +83,7 @@ class DefaultActiveCallManager @Inject constructor( private val ringingCallNotificationCreator: RingingCallNotificationCreator, private val notificationManagerCompat: NotificationManagerCompat, private val matrixClientProvider: MatrixClientProvider, + private val defaultCurrentCallObserver: DefaultCurrentCallObserver, ) : ActiveCallManager { private var timedOutCallJob: Job? = null @@ -89,6 +91,7 @@ class DefaultActiveCallManager @Inject constructor( init { observeRingingCall() + observeCurrentCall() } override fun registerIncomingCall(notificationData: CallNotificationData) { @@ -209,6 +212,28 @@ class DefaultActiveCallManager @Inject constructor( } .launchIn(coroutineScope) } + + private fun observeCurrentCall() { + activeCall + .onEach { value -> + if (value == null) { + defaultCurrentCallObserver.onCallEnded() + } else { + when (value.callState) { + is CallState.Ringing -> { + // Nothing to do + } + is CallState.InCall -> { + when (val callType = value.callType) { + is CallType.ExternalUrl -> defaultCurrentCallObserver.onCallStarted(CurrentCall.ExternalUrl(callType.url)) + is CallType.RoomCall -> defaultCurrentCallObserver.onCallStarted(CurrentCall.RoomCall(callType.roomId)) + } + } + } + } + } + .launchIn(coroutineScope) + } } /** diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt new file mode 100644 index 0000000000..4810d31ee6 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCurrentCallObserver.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.call.impl.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.api.CurrentCall +import io.element.android.features.call.api.CurrentCallObserver +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultCurrentCallObserver @Inject constructor() : CurrentCallObserver { + override val currentCall = MutableStateFlow(CurrentCall.None) + + fun onCallStarted(call: CurrentCall) { + currentCall.value = call + } + + fun onCallEnded() { + currentCall.value = CurrentCall.None + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index 45568f2d39..93144ccd50 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -15,6 +15,7 @@ import io.element.android.features.call.impl.notifications.RingingCallNotificati import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.DefaultActiveCallManager +import io.element.android.features.call.impl.utils.DefaultCurrentCallObserver import io.element.android.features.call.test.aCallNotificationData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -299,5 +300,6 @@ class DefaultActiveCallManagerTest { ), notificationManagerCompat = notificationManagerCompat, matrixClientProvider = matrixClientProvider, + defaultCurrentCallObserver = DefaultCurrentCallObserver(), ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt index 58a3940475..2284ffa50c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt @@ -79,7 +79,7 @@ private fun OnGoingCallMenuItem( onJoinCallClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (!roomCallState.isUserInTheCall) { + if (!roomCallState.isUserLocallyInTheCall) { Button( onClick = onJoinCallClick, colors = ButtonDefaults.buttonColors( diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt index 77c58fee2c..e47a623914 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallState.kt @@ -20,6 +20,7 @@ sealed interface RoomCallState { data class OnGoing( val canJoinCall: Boolean, val isUserInTheCall: Boolean, + val isUserLocallyInTheCall: Boolean, ) : RoomCallState } diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt index dce722c2c4..6351c25479 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt @@ -22,9 +22,11 @@ open class RoomCallStateProvider : PreviewParameterProvider { fun anOngoingCallState( canJoinCall: Boolean = true, isUserInTheCall: Boolean = false, + isUserLocallyInTheCall: Boolean = isUserInTheCall, ) = RoomCallState.OnGoing( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, + isUserLocallyInTheCall = isUserLocallyInTheCall, ) fun aStandByCallState( diff --git a/features/roomcall/impl/build.gradle.kts b/features/roomcall/impl/build.gradle.kts index e8f65b160b..adb5b07bf2 100644 --- a/features/roomcall/impl/build.gradle.kts +++ b/features/roomcall/impl/build.gradle.kts @@ -20,6 +20,7 @@ setupAnvil() dependencies { api(projects.features.roomcall.api) implementation(libs.kotlinx.collections.immutable) + implementation(projects.features.call.api) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index 175592af08..b6eb10b342 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -12,6 +12,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import io.element.android.features.call.api.CurrentCall +import io.element.android.features.call.api.CurrentCallObserver import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -20,6 +22,7 @@ import javax.inject.Inject class RoomCallStatePresenter @Inject constructor( private val room: MatrixRoom, + private val currentCallObserver: CurrentCallObserver, ) : Presenter { @Composable override fun present(): RoomCallState { @@ -31,10 +34,17 @@ class RoomCallStatePresenter @Inject constructor( room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() } } + val currentCall by currentCallObserver.currentCall.collectAsState() + val isUserLocallyInTheCall by remember { + derivedStateOf { + (currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId + } + } val callState = when { roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, + isUserLocallyInTheCall = isUserLocallyInTheCall, ) else -> RoomCallState.StandBy(canStartCall = canJoinCall) }