Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.wire.android.di.KaliumCoreLogic
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.meeting.CreateNewMeetingUseCase
import com.wire.kalium.logic.feature.meeting.EnsureMeetingIsMLSEstablishedUseCase
import com.wire.kalium.logic.feature.meeting.GetPaginatedMeetingOccurrencesUseCase
import com.wire.kalium.logic.feature.meeting.MeetingScope
import com.wire.kalium.logic.feature.meeting.ObserveMeetingOccurrenceUseCase
Expand Down Expand Up @@ -49,6 +50,10 @@ class MeetingModule {
fun provideDeleteMeetingUseCase(meetingScope: MeetingScope): com.wire.kalium.logic.feature.meeting.DeleteMeetingUseCase =
meetingScope.deleteMeeting

@Provides
fun ensureMeetingIsMLSEstablishedUseCase(meetingScope: MeetingScope): EnsureMeetingIsMLSEstablishedUseCase =
meetingScope.ensureMeetingIsMLSEstablished

@Provides
fun provideCreateNewMeetingUseCase(meetingScope: MeetingScope): CreateNewMeetingUseCase =
meetingScope.createNewMeeting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase
import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase
import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase
import com.wire.kalium.logic.feature.meeting.EnsureMeetingIsMLSEstablishedUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase
Expand Down Expand Up @@ -140,6 +141,7 @@
private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase,
private val observeConferenceCallingEnabled: ObserveConferenceCallingEnabledUseCase,
private val observeSelf: ObserveSelfUserUseCase,
private val ensureMeetingIsMLSEstablished: EnsureMeetingIsMLSEstablishedUseCase,

Check warning on line 144 in app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt#L144

Added line #L144 was not covered by tests
) {
fun callActivityViewModel() = CallActivityViewModel(
dispatchers = dispatchers,
Expand Down Expand Up @@ -244,6 +246,7 @@
setUserInformedAboutVerification = setUserInformedAboutVerification,
observeDegradedConversationNotified = observeDegradedConversationNotified,
observeSelf = observeSelf,
ensureMeetingIsMLSEstablished = ensureMeetingIsMLSEstablished,

Check warning on line 249 in app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt#L249

Added line #L249 was not covered by tests
)

fun conversationListCallViewModel() = ConversationListCallViewModelImpl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,22 @@ package com.wire.android.ui.home.meetings

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.ui.common.visbility.VisibilityState
import com.wire.android.ui.home.conversations.call.JoinOrStartCallManager
import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase
import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase
import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase
import com.wire.kalium.logic.feature.meeting.EnsureMeetingIsMLSEstablishedUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase
import com.wire.kalium.logic.sync.ObserveSyncStateUseCase
import kotlinx.coroutines.launch

@Suppress("LongParameterList", "TooManyFunctions")
class MeetingsCallViewModel(
Expand All @@ -43,8 +48,11 @@ class MeetingsCallViewModel(
private val isConferenceCallingEnabled: IsEligibleToStartCallUseCase,
private val setUserInformedAboutVerification: SetUserInformedAboutVerificationUseCase,
private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase,
private val observeSelf: ObserveSelfUserUseCase
private val observeSelf: ObserveSelfUserUseCase,
private val ensureMeetingIsMLSEstablished: EnsureMeetingIsMLSEstablishedUseCase,
) : ViewModel() {
val notEstablishedDialogState: VisibilityState<Unit> = VisibilityState()

val callManager = JoinOrStartCallManager(
scope = viewModelScope,
currentAccount = currentAccount,
Expand All @@ -58,4 +66,31 @@ class MeetingsCallViewModel(
observeDegradedConversationNotified = observeDegradedConversationNotified,
observeSelf = observeSelf,
)

fun joinOngoingCall(conversationId: ConversationId) {
viewModelScope.launch {
ensureMLSEstablished(conversationId) {
callManager.joinOngoingCall(conversationId)
}
}
}

fun startCallIfPossible(conversationId: ConversationId) {
viewModelScope.launch {
ensureMLSEstablished(conversationId) {
callManager.startCallIfPossible(
conversationId = conversationId,
conversationType = Conversation.Type.Group.Meeting,
shouldCheckParticipantCount = false, // since this is a meeting, we don't need to check participant count
)
}
}
}

private suspend fun ensureMLSEstablished(conversationId: ConversationId, actionIfEstablished: suspend () -> Unit) =
if (ensureMeetingIsMLSEstablished(conversationId)) {
actionIfEstablished()
} else {
notEstablishedDialogState.show(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,28 @@ package com.wire.android.ui.home.meetings
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.ramcosta.composedestinations.generated.meetings.destinations.NewMeetingScreenDestination
import com.wire.android.feature.meetings.ui.AllMeetingsScreen
import com.wire.android.feature.meetings.ui.NewMeetingBottomSheet
import com.wire.android.navigation.HomeDestination
import com.wire.android.navigation.annotation.app.WireHomeDestination
import com.wire.android.ui.common.dimensions
import com.wire.android.feature.meetings.ui.create.NewMeetingType
import com.wire.android.navigation.HomeDestination
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.annotation.app.WireHomeDestination
import com.wire.android.ui.calling.meetingsCallViewModel
import com.wire.android.ui.calling.ongoing.getOngoingCallIntent
import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.common.VisibilityState
import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.visbility.VisibilityState
import com.wire.android.ui.home.HomeStateHolder
import com.wire.android.ui.home.conversations.call.HandleActions
import com.wire.android.ui.home.conversations.call.HandleJoinOrStartCallScreenDialogs
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.android.feature.meetings.R as meetingsR
import com.wire.android.ui.common.R as commonR

@WireHomeDestination
@Composable
Expand All @@ -46,14 +54,10 @@ fun MeetingsScreen(
lazyListState = homeStateHolder.lazyListStateFor(HomeDestination.Meetings),
contentPadding = PaddingValues(bottom = dimensions().spacing80x), // to ensure last item is not obscured by FAB
startCall = { conversationId ->
viewModel.callManager.startCallIfPossible(
conversationId = conversationId,
conversationType = Conversation.Type.Group.Regular,
shouldCheckParticipantCount = false, // since this is a meeting, we don't need to check participant count
)
viewModel.startCallIfPossible(conversationId = conversationId)
},
joinCall = { conversationId ->
viewModel.callManager.joinOngoingCall(conversationId = conversationId)
viewModel.joinOngoingCall(conversationId = conversationId)
},
returnToCall = { conversationId ->
context.startActivity(
Expand All @@ -69,6 +73,8 @@ fun MeetingsScreen(
viewModel.callManager.actions.HandleActions()
viewModel.callManager.HandleJoinOrStartCallScreenDialogs()

NotEstablishedDialog(dialogState = viewModel.notEstablishedDialogState)

NewMeetingBottomSheet(
sheetState = homeStateHolder.newMeetingBottomSheetState,
onMeetNowClick = {
Expand All @@ -83,3 +89,21 @@ fun MeetingsScreen(
}
)
}

@Composable
private fun NotEstablishedDialog(dialogState: VisibilityState<Unit>) {
VisibilityState(dialogState) {
WireDialog(
title = stringResource(meetingsR.string.meeting_join_failure_title),
text = stringResource(id = meetingsR.string.meeting_join_failure_description),
buttonsHorizontalAlignment = true,
onDismiss = dialogState::dismiss,
optionButton1Properties = WireDialogButtonProperties(
onClick = dialogState::dismiss,
text = stringResource(id = commonR.string.label_ok),
type = WireDialogButtonType.Primary,
state = WireButtonState.Default,
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/

package com.wire.android.ui.home.meetings

import app.cash.turbine.test
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.framework.TestConversation
import com.wire.android.framework.TestUser
import com.wire.android.ui.home.conversations.call.JoinOrStartCallViewActions
import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData
import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.sync.SyncState
import com.wire.kalium.logic.data.user.type.UserType
import com.wire.kalium.logic.data.user.type.UserTypeInfo
import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase
import com.wire.kalium.logic.feature.call.usecase.ConferenceCallingResult
import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase
import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase
import com.wire.kalium.logic.feature.meeting.EnsureMeetingIsMLSEstablishedUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase
import com.wire.kalium.logic.sync.ObserveSyncStateUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
class MeetingsCallViewModelTest {

@Suppress("UnusedFlow")
@Test
fun givenMeetingMLSIsEstablished_whenStartingCall_thenEnsureMLSAndInitiateMeetingCall() = runTest {
val (arrangement, viewModel) = Arrangement()
.withMeetingMLSEstablished()
.arrange()

viewModel.callManager.actions.test {
viewModel.startCallIfPossible(TestConversation.ID)
advanceUntilIdle()

assertEquals(JoinOrStartCallViewActions.InitiatedCall(TestConversation.ID, TestUser.SELF_USER_ID), awaitItem())
assertFalse(viewModel.notEstablishedDialogState.isVisible)
coVerify(exactly = 1) { arrangement.ensureMeetingIsMLSEstablished(TestConversation.ID) }
coVerify(exactly = 1) { arrangement.isConferenceCallingEnabled(TestConversation.ID, Conversation.Type.Group.Meeting) }
coVerify(exactly = 0) { arrangement.observeParticipantsForConversation(any()) }
}
}

@Test
fun givenMeetingMLSIsNotEstablished_whenStartingCall_thenShowFailureDialogAndDoNotInitiateCall() = runTest {
val (arrangement, viewModel) = Arrangement()
.withMeetingMLSNotEstablished()
.arrange()

viewModel.callManager.actions.test {
viewModel.startCallIfPossible(TestConversation.ID)
advanceUntilIdle()

expectNoEvents()
assertTrue(viewModel.notEstablishedDialogState.isVisible)
coVerify(exactly = 1) { arrangement.ensureMeetingIsMLSEstablished(TestConversation.ID) }
coVerify(exactly = 0) { arrangement.isConferenceCallingEnabled(any(), any()) }
}
}

@Test
fun givenMeetingMLSIsEstablished_whenJoiningCall_thenEnsureMLSAndJoinCall() = runTest {
val (arrangement, viewModel) = Arrangement()
.withMeetingMLSEstablished()
.arrange()

viewModel.callManager.actions.test {
viewModel.joinOngoingCall(TestConversation.ID)
advanceUntilIdle()

assertEquals(JoinOrStartCallViewActions.JoinedCall(TestConversation.ID, TestUser.SELF_USER_ID), awaitItem())
assertFalse(viewModel.notEstablishedDialogState.isVisible)
coVerify(exactly = 1) { arrangement.ensureMeetingIsMLSEstablished(TestConversation.ID) }
coVerify(exactly = 1) { arrangement.answerCall(conversationId = TestConversation.ID) }
}
}

@Test
fun givenMeetingMLSIsNotEstablished_whenJoiningCall_thenShowFailureDialogAndDoNotJoinCall() = runTest {
val (arrangement, viewModel) = Arrangement()
.withMeetingMLSNotEstablished()
.arrange()

viewModel.callManager.actions.test {
viewModel.joinOngoingCall(TestConversation.ID)
advanceUntilIdle()

expectNoEvents()
assertTrue(viewModel.notEstablishedDialogState.isVisible)
coVerify(exactly = 1) { arrangement.ensureMeetingIsMLSEstablished(TestConversation.ID) }
coVerify(exactly = 0) { arrangement.answerCall(conversationId = any()) }
}
}

inner class Arrangement {
@MockK
lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase

@MockK
lateinit var observeParticipantsForConversation: ObserveParticipantsForConversationUseCase

@MockK
lateinit var answerCall: AnswerCallUseCase

@MockK
lateinit var endCall: EndCallUseCase

@MockK
lateinit var observeSyncState: ObserveSyncStateUseCase

@MockK
lateinit var isConferenceCallingEnabled: IsEligibleToStartCallUseCase

@MockK
lateinit var setUserInformedAboutVerification: SetUserInformedAboutVerificationUseCase

@MockK
lateinit var observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase

@MockK
lateinit var observeSelfUser: ObserveSelfUserUseCase

@MockK
lateinit var ensureMeetingIsMLSEstablished: EnsureMeetingIsMLSEstablishedUseCase

init {
MockKAnnotations.init(this, relaxUnitFun = true)
every { observeEstablishedCalls() } returns emptyFlow()
coEvery { observeParticipantsForConversation(any()) } returns flowOf(ConversationParticipantsData())
coEvery { answerCall(conversationId = any()) } returns Unit
coEvery { endCall(any()) } returns Unit
every { observeSyncState() } returns flowOf(SyncState.Live)
coEvery { isConferenceCallingEnabled(any(), any()) } returns ConferenceCallingResult.Enabled
coEvery { setUserInformedAboutVerification(any()) } returns Unit
every { observeDegradedConversationNotified(any()) } returns flowOf(true)
coEvery { observeSelfUser() } returns flowOf(TestUser.SELF_USER.copy(userType = UserTypeInfo.Regular(UserType.GUEST)))
coEvery { ensureMeetingIsMLSEstablished(any()) } returns true
}

fun withMeetingMLSEstablished() = apply {
coEvery { ensureMeetingIsMLSEstablished(any()) } returns true
}
fun withMeetingMLSNotEstablished() = apply {
coEvery { ensureMeetingIsMLSEstablished(any()) } returns false
}
fun arrange() = this to MeetingsCallViewModel(
currentAccount = TestUser.SELF_USER_ID,
observeEstablishedCalls = observeEstablishedCalls,
observeParticipantsForConversation = observeParticipantsForConversation,
answerCall = answerCall,
endCall = endCall,
observeSyncState = observeSyncState,
isConferenceCallingEnabled = isConferenceCallingEnabled,
setUserInformedAboutVerification = setUserInformedAboutVerification,
observeDegradedConversationNotified = observeDegradedConversationNotified,
observeSelf = observeSelfUser,
ensureMeetingIsMLSEstablished = ensureMeetingIsMLSEstablished,
)
}
}
Loading
Loading