From ac72f4e08a9173f2d95da6eb0f457be7a188e55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 30 Jul 2026 12:50:00 +0200 Subject: [PATCH 1/4] feat: ensure MLS is established when joining meeting [WPB-25677] --- .../android/di/accountScoped/MeetingModule.kt | 5 + .../ui/calling/CallingViewModelFactory.kt | 3 + .../ui/home/meetings/MeetingsCallViewModel.kt | 37 +++- .../ui/home/meetings/MeetingsScreen.kt | 44 ++++- .../meetings/MeetingsCallViewModelTest.kt | 186 ++++++++++++++++++ .../meetings/src/main/res/values/strings.xml | 2 + kalium | 2 +- 7 files changed, 267 insertions(+), 12 deletions(-) create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/MeetingModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/MeetingModule.kt index c6eaf011d19..6c2f6d56e20 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/MeetingModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/MeetingModule.kt @@ -21,6 +21,7 @@ import com.wire.android.di.CurrentAccount 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.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 @@ -47,4 +48,8 @@ class MeetingModule { @Provides fun provideDeleteMeetingUseCase(meetingScope: MeetingScope): com.wire.kalium.logic.feature.meeting.DeleteMeetingUseCase = meetingScope.deleteMeeting + + @Provides + fun ensureMeetingIsMLSEstablishedUseCase(meetingScope: MeetingScope): EnsureMeetingIsMLSEstablishedUseCase = + meetingScope.ensureMeetingIsMLSEstablished } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt index 09567bd9b73..0772d9e804b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt @@ -77,6 +77,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC 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 @@ -140,6 +141,7 @@ class CallingViewModelFactory @Inject constructor( private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase, private val observeConferenceCallingEnabled: ObserveConferenceCallingEnabledUseCase, private val observeSelf: ObserveSelfUserUseCase, + private val ensureMeetingIsMLSEstablished: EnsureMeetingIsMLSEstablishedUseCase, ) { fun callActivityViewModel() = CallActivityViewModel( dispatchers = dispatchers, @@ -244,6 +246,7 @@ class CallingViewModelFactory @Inject constructor( setUserInformedAboutVerification = setUserInformedAboutVerification, observeDegradedConversationNotified = observeDegradedConversationNotified, observeSelf = observeSelf, + ensureMeetingIsMLSEstablished = ensureMeetingIsMLSEstablished, ) fun conversationListCallViewModel() = ConversationListCallViewModelImpl( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModel.kt index aee02cde020..bed2b36e2d7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModel.kt @@ -20,8 +20,11 @@ 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 @@ -29,8 +32,10 @@ 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( @@ -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 = VisibilityState() + val callManager = JoinOrStartCallManager( scope = viewModelScope, currentAccount = currentAccount, @@ -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) + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsScreen.kt index 31b3c849f60..33914eec030 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/meetings/MeetingsScreen.kt @@ -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 @@ -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( @@ -69,6 +73,8 @@ fun MeetingsScreen( viewModel.callManager.actions.HandleActions() viewModel.callManager.HandleJoinOrStartCallScreenDialogs() + NotEstablishedDialog(dialogState = viewModel.notEstablishedDialogState) + NewMeetingBottomSheet( sheetState = homeStateHolder.newMeetingBottomSheetState, onMeetNowClick = { @@ -83,3 +89,21 @@ fun MeetingsScreen( } ) } + +@Composable +private fun NotEstablishedDialog(dialogState: VisibilityState) { + 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, + ) + ) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt new file mode 100644 index 00000000000..440dcc4af85 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt @@ -0,0 +1,186 @@ +/* + * 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, + ) + } +} diff --git a/features/meetings/src/main/res/values/strings.xml b/features/meetings/src/main/res/values/strings.xml index 016bc575b73..86ca97d002d 100644 --- a/features/meetings/src/main/res/values/strings.xml +++ b/features/meetings/src/main/res/values/strings.xml @@ -79,6 +79,8 @@ The meeting will be deleted for you and everybody else. All participants will be notified. ā€œ%sā€ deleted There was an error while deleting meeting ā€œ%sā€ + Could not join meeting + Something went wrong while joining the meeting. Please try again. +%1$d more diff --git a/kalium b/kalium index e68500602d0..29739311b88 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e68500602d0914e1263045aadeb69a414517f896 +Subproject commit 29739311b888f6fbad77d771eb8135a48ac50be5 From 77c8b9f8badd0769f96a913aa2d2a10b9fc5d06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 30 Jul 2026 12:58:54 +0200 Subject: [PATCH 2/4] clean-up --- .../meetings/MeetingsCallViewModelTest.kt | 9 ++++ app/stability/app-devDebug.stability | 42 ++++++++++--------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt index 440dcc4af85..9f1f5930cbe 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/meetings/MeetingsCallViewModelTest.kt @@ -130,22 +130,31 @@ class MeetingsCallViewModelTest { 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 diff --git a/app/stability/app-devDebug.stability b/app/stability/app-devDebug.stability index 38306685fb8..17363e04805 100644 --- a/app/stability/app-devDebug.stability +++ b/app/stability/app-devDebug.stability @@ -848,11 +848,11 @@ private fun com.wire.android.ui.WireActivity.rememberWireActivityGraphContext(ap - isSessionTransitionInProgress: STABLE (primitive type) @Composable -private fun com.wire.android.ui.WireActivity.wireActivityScopedViewModels(graph: com.wire.android.di.metro.AppSessionViewModelGraph): com.wire.android.ui.WireActivityScopedViewModels - skippable: false +private fun com.wire.android.ui.WireActivity.wireActivityScopedViewModels(retainedSessionGraph: com.wire.android.ui.RetainedSessionGraph): com.wire.android.ui.WireActivityScopedViewModels + skippable: true restartable: true params: - - graph: RUNTIME (requires runtime check) + - retainedSessionGraph: STABLE (marked @Stable or @Immutable) @Composable public fun com.wire.android.ui.analyticsUsageViewModel(): com.wire.android.ui.analytics.AnalyticsUsageViewModel @@ -2785,16 +2785,6 @@ public fun com.wire.android.ui.common.UnderConstructionScreen(screenName: kotlin - screenName: STABLE (String is immutable) - modifier: STABLE (marked @Stable or @Immutable) -@Composable -public fun com.wire.android.ui.common.WireRadioButton(checked: kotlin.Boolean, modifier: androidx.compose.ui.Modifier, onButtonChecked: kotlin.Function0?, enabled: kotlin.Boolean): kotlin.Unit - skippable: true - restartable: true - params: - - checked: STABLE (primitive type) - - modifier: STABLE (marked @Stable or @Immutable) - - onButtonChecked: STABLE (function type) - - enabled: STABLE (primitive type) - @Composable public fun com.wire.android.ui.common.animateAsStateRotationToRight(isOpen: kotlin.Boolean): androidx.compose.runtime.State skippable: true @@ -4537,7 +4527,7 @@ public fun com.wire.android.ui.home.conversations.ConversationScreen(navigator: - messageAttachmentsViewModel: UNSTABLE (has mutable properties or unstable members) @Composable -private fun com.wire.android.ui.home.conversations.ConversationScreen(bannerMessage: com.wire.android.util.ui.UIText?, messageComposerViewState: com.wire.android.ui.home.conversations.MessageComposerViewState, conversationCallViewState: com.wire.android.ui.home.conversations.call.ConversationCallViewState, conversationInfoViewState: com.wire.android.ui.home.conversations.info.ConversationInfoViewState, conversationMessagesViewState: com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState, attachments: kotlin.collections.List, bottomSheetVisible: kotlin.Boolean, onOpenProfile: kotlin.Function1<@[ParameterName(name = \, onMessageDetailsClick: kotlin.Function2<@[ParameterName(name = \, onSendMessage: kotlin.Function1, onPingOptionClicked: kotlin.Function0, onImagesPicked: kotlin.Function2, kotlin.Boolean, kotlin.Unit>, onAttachmentPicked: kotlin.Function1, onAudioRecorded: kotlin.Function1, onDeleteMessage: kotlin.Function2, onAssetItemClicked: kotlin.Function1, onImageFullScreenMode: kotlin.Function3, onVideoClick: kotlin.Function3<@[ParameterName(name = \, onStartCall: kotlin.Function0, onJoinCall: kotlin.Function0, onReactionClick: kotlin.Function2<@[ParameterName(name = \, onResetSessionClick: kotlin.Function2<@[ParameterName(name = \, onUpdateConversationReadDate: kotlin.Function1, onDropDownClick: kotlin.Function0, onBackButtonClick: kotlin.Function0, composerMessages: kotlinx.coroutines.flow.SharedFlow, conversationMessages: kotlinx.coroutines.flow.SharedFlow, shareAsset: kotlin.Function2, onSelfDeletingMessageRead: kotlin.Function1, onNewSelfDeletingMessagesStatus: kotlin.Function1, tempWritableImageUri: android.net.Uri?, tempWritableVideoUri: android.net.Uri?, onFailedMessageRetryClicked: kotlin.Function2, onClearMentionSearchResult: kotlin.Function0, onPermissionPermanentlyDenied: kotlin.Function1<@[ParameterName(name = \, conversationScreenState: com.wire.android.ui.home.conversations.ConversationScreenState, messageComposerStateHolder: com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder, onLinkClick: kotlin.Function1, openDrawingCanvas: kotlin.Function0, onAttachmentClick: kotlin.Function1, onAttachmentMenuClick: kotlin.Function1, currentTimeInMillisFlow: kotlinx.coroutines.flow.Flow, onReachedOldestMessage: kotlin.Function0, isFetchingOlderMessages: kotlin.Boolean, hasMoreRemoteMessages: kotlin.Boolean, isWireCellsEnabled: kotlin.Boolean): kotlin.Unit +private fun com.wire.android.ui.home.conversations.ConversationScreen(bannerMessage: com.wire.android.util.ui.UIText?, messageComposerViewState: com.wire.android.ui.home.conversations.MessageComposerViewState, conversationCallViewState: com.wire.android.ui.home.conversations.call.ConversationCallViewState, conversationInfoViewState: com.wire.android.ui.home.conversations.info.ConversationInfoViewState, conversationMessagesViewState: com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState, attachments: kotlin.collections.List, bottomSheetVisible: kotlin.Boolean, onOpenProfile: kotlin.Function1<@[ParameterName(name = \, onMessageDetailsClick: kotlin.Function2<@[ParameterName(name = \, onSendMessage: kotlin.Function1, onPingOptionClicked: kotlin.Function0, onImagesPicked: kotlin.Function2, kotlin.Boolean, kotlin.Unit>, onAttachmentPicked: kotlin.Function1, onAudioRecorded: kotlin.Function1, onDeleteMessage: kotlin.Function2, onAssetItemClicked: kotlin.Function1, onImageFullScreenMode: kotlin.Function3, onVideoClick: kotlin.Function3<@[ParameterName(name = \, onStartCall: kotlin.Function0, onJoinCall: kotlin.Function0, onReactionClick: kotlin.Function2<@[ParameterName(name = \, onResetSessionClick: kotlin.Function2<@[ParameterName(name = \, onUpdateConversationReadDate: kotlin.Function1, onDropDownClick: kotlin.Function0, onBackButtonClick: kotlin.Function0, composerMessages: kotlinx.coroutines.flow.SharedFlow, conversationMessages: kotlinx.coroutines.flow.SharedFlow, shareAsset: kotlin.Function2, onSelfDeletingMessageRead: kotlin.Function1, onNewSelfDeletingMessagesStatus: kotlin.Function1, tempWritableImageUri: android.net.Uri?, tempWritableVideoUri: android.net.Uri?, onFailedMessageRetryClicked: kotlin.Function2, onClearMentionSearchResult: kotlin.Function0, onPermissionPermanentlyDenied: kotlin.Function1<@[ParameterName(name = \, conversationScreenState: com.wire.android.ui.home.conversations.ConversationScreenState, messageComposerStateHolder: com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder, onLinkClick: kotlin.Function1, openDrawingCanvas: kotlin.Function0, onAttachmentClick: kotlin.Function1, onAttachmentMenuClick: kotlin.Function1, currentTimeInMillisFlow: kotlinx.coroutines.flow.Flow, onReachedOldestMessage: kotlin.Function0, isFetchingOlderMessages: kotlin.Boolean, hasMoreRemoteMessages: kotlin.Boolean, isWireCellsEnabled: kotlin.Boolean): kotlin.Unit skippable: false restartable: true params: @@ -4592,7 +4582,7 @@ private fun com.wire.android.ui.home.conversations.ConversationScreen(bannerMess - isWireCellsEnabled: STABLE (primitive type) @Composable -private fun com.wire.android.ui.home.conversations.ConversationScreenContent(conversationId: com.wire.kalium.logic.data.id.QualifiedID, bottomSheetVisible: kotlin.Boolean, lastUnreadMessageInstant: kotlinx.datetime.Instant?, unreadEventCount: kotlin.Int, playingAudioMessage: com.wire.android.media.audiomessage.PlayingAudioMessage, assetStatuses: kotlinx.collections.immutable.PersistentMap, selectedMessageId: kotlin.String?, messageComposerStateHolder: com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder, attachments: kotlin.collections.List, messages: kotlinx.coroutines.flow.Flow>, onSendMessage: kotlin.Function1, onPingOptionClicked: kotlin.Function0, onImagesPicked: kotlin.Function2, kotlin.Boolean, kotlin.Unit>, onAttachmentPicked: kotlin.Function1, onAudioRecorded: kotlin.Function1, onAssetItemClicked: kotlin.Function1, onImageFullScreenMode: kotlin.Function3, onVideoClick: kotlin.Function3<@[ParameterName(name = \, onReactionClicked: kotlin.Function2, onResetSessionClicked: kotlin.Function2<@[ParameterName(name = \, onOpenProfile: kotlin.Function1<@[ParameterName(name = \, onUpdateConversationReadDate: kotlin.Function1, onShowEditingOptions: kotlin.Function1, onSwipedToReply: kotlin.Function1, onSelfDeletingMessageRead: kotlin.Function1, conversationDetailsData: com.wire.android.ui.home.conversations.info.ConversationDetailsData, onFailedMessageRetryClicked: kotlin.Function2, onFailedMessageCancelClicked: kotlin.Function1, onChangeSelfDeletionClicked: kotlin.Function1, onClearMentionSearchResult: kotlin.Function0, onLocationClicked: kotlin.Function0, onPermissionPermanentlyDenied: kotlin.Function1<@[ParameterName(name = \, tempWritableImageUri: android.net.Uri?, tempWritableVideoUri: android.net.Uri?, onLinkClick: kotlin.Function1, onNavigateToReplyOriginalMessage: kotlin.Function1, openDrawingCanvas: kotlin.Function0, onAttachmentClick: kotlin.Function1, onAttachmentMenuClick: kotlin.Function1, currentTimeInMillisFlow: kotlinx.coroutines.flow.Flow, onReachedOldestMessage: kotlin.Function0, showHistoryLoadingIndicator: kotlin.Boolean, isFetchingOlderMessages: kotlin.Boolean, hasMoreRemoteMessages: kotlin.Boolean, isBubbleUiEnabled: kotlin.Boolean, isWireCellsEnabled: kotlin.Boolean): kotlin.Unit +private fun com.wire.android.ui.home.conversations.ConversationScreenContent(conversationId: com.wire.kalium.logic.data.id.QualifiedID, bottomSheetVisible: kotlin.Boolean, lastUnreadMessageInstant: kotlinx.datetime.Instant?, unreadEventCount: kotlin.Int, playingAudioMessage: com.wire.android.media.audiomessage.PlayingAudioMessage, assetStatuses: kotlinx.collections.immutable.PersistentMap, selectedMessageId: kotlin.String?, messageComposerStateHolder: com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder, attachments: kotlin.collections.List, messages: kotlinx.coroutines.flow.Flow>, onSendMessage: kotlin.Function1, onPingOptionClicked: kotlin.Function0, onImagesPicked: kotlin.Function2, kotlin.Boolean, kotlin.Unit>, onAttachmentPicked: kotlin.Function1, onAudioRecorded: kotlin.Function1, onAssetItemClicked: kotlin.Function1, onImageFullScreenMode: kotlin.Function3, onVideoClick: kotlin.Function3<@[ParameterName(name = \, onReactionClicked: kotlin.Function2, onResetSessionClicked: kotlin.Function2<@[ParameterName(name = \, onOpenProfile: kotlin.Function1<@[ParameterName(name = \, onUpdateConversationReadDate: kotlin.Function1, onShowEditingOptions: kotlin.Function1, onSwipedToReply: kotlin.Function1, onSelfDeletingMessageRead: kotlin.Function1, conversationDetailsData: com.wire.android.ui.home.conversations.info.ConversationDetailsData, onFailedMessageRetryClicked: kotlin.Function2, onFailedMessageCancelClicked: kotlin.Function1, onChangeSelfDeletionClicked: kotlin.Function1, onClearMentionSearchResult: kotlin.Function0, onLocationClicked: kotlin.Function0, onPermissionPermanentlyDenied: kotlin.Function1<@[ParameterName(name = \, tempWritableImageUri: android.net.Uri?, tempWritableVideoUri: android.net.Uri?, onLinkClick: kotlin.Function1, onNavigateToReplyOriginalMessage: kotlin.Function1, openDrawingCanvas: kotlin.Function0, onAttachmentClick: kotlin.Function1, onAttachmentMenuClick: kotlin.Function1, currentTimeInMillisFlow: kotlinx.coroutines.flow.Flow, onReachedOldestMessage: kotlin.Function0, showHistoryLoadingIndicator: kotlin.Boolean, isFetchingOlderMessages: kotlin.Boolean, hasMoreRemoteMessages: kotlin.Boolean, isBubbleUiEnabled: kotlin.Boolean, isWireCellsEnabled: kotlin.Boolean): kotlin.Unit skippable: false restartable: true params: @@ -4738,17 +4728,17 @@ private fun com.wire.android.ui.home.conversations.MenuItem(iconRes: kotlin.Int, - onClick: STABLE (function type) @Composable -private fun com.wire.android.ui.home.conversations.MessageGroupDateTime(now: kotlin.Long, messageDateTime: java.util.Date, messageDateTimeGroup: com.wire.android.mapper.MessageDateTimeGroup?, isBubbleUiEnabled: kotlin.Boolean): kotlin.Unit - skippable: false +private fun com.wire.android.ui.home.conversations.MessageGroupDateTime(now: kotlin.Long, messageDateTime: kotlinx.datetime.Instant, messageDateTimeGroup: com.wire.android.mapper.MessageDateTimeGroup, isBubbleUiEnabled: kotlin.Boolean): kotlin.Unit + skippable: true restartable: true params: - now: STABLE (primitive type) - - messageDateTime: UNSTABLE (mutable Java class) + - messageDateTime: STABLE (matched by stability configuration) - messageDateTimeGroup: STABLE (class with no mutable properties) - isBubbleUiEnabled: STABLE (primitive type) @Composable -public fun com.wire.android.ui.home.conversations.MessageList(lazyPagingMessages: androidx.paging.compose.LazyPagingItems, lazyListState: androidx.compose.foundation.lazy.LazyListState, lastUnreadMessageInstant: kotlinx.datetime.Instant?, playingAudioMessage: com.wire.android.media.audiomessage.PlayingAudioMessage, assetStatuses: kotlinx.collections.immutable.PersistentMap, onUpdateConversationReadDate: kotlin.Function1, onSwipedToReply: kotlin.Function1, onSwipedToReact: kotlin.Function1, onSelfDeletingMessageRead: kotlin.Function1, conversationDetailsData: com.wire.android.ui.home.conversations.info.ConversationDetailsData, selectedMessageId: kotlin.String?, interactionAvailability: com.wire.kalium.logic.data.conversation.InteractionAvailability, clickActions: com.wire.android.ui.home.conversations.messages.item.MessageClickActions.Content, modifier: androidx.compose.ui.Modifier, currentTimeInMillisFlow: kotlinx.coroutines.flow.Flow, showHistoryLoadingIndicator: kotlin.Boolean, isFetchingOlderMessages: kotlin.Boolean, hasMoreRemoteMessages: kotlin.Boolean, isBubbleUiEnabled: kotlin.Boolean, isWireCellsEnabled: kotlin.Boolean, onReachedOldestMessage: kotlin.Function0): kotlin.Unit +public fun com.wire.android.ui.home.conversations.MessageList(lazyPagingMessages: androidx.paging.compose.LazyPagingItems, lazyListState: androidx.compose.foundation.lazy.LazyListState, lastUnreadMessageInstant: kotlinx.datetime.Instant?, playingAudioMessage: com.wire.android.media.audiomessage.PlayingAudioMessage, assetStatuses: kotlinx.collections.immutable.PersistentMap, onUpdateConversationReadDate: kotlin.Function1, onSwipedToReply: kotlin.Function1, onSwipedToReact: kotlin.Function1, onSelfDeletingMessageRead: kotlin.Function1, conversationDetailsData: com.wire.android.ui.home.conversations.info.ConversationDetailsData, selectedMessageId: kotlin.String?, interactionAvailability: com.wire.kalium.logic.data.conversation.InteractionAvailability, clickActions: com.wire.android.ui.home.conversations.messages.item.MessageClickActions.Content, modifier: androidx.compose.ui.Modifier, currentTimeInMillisFlow: kotlinx.coroutines.flow.Flow, showHistoryLoadingIndicator: kotlin.Boolean, isFetchingOlderMessages: kotlin.Boolean, hasMoreRemoteMessages: kotlin.Boolean, isBubbleUiEnabled: kotlin.Boolean, isWireCellsEnabled: kotlin.Boolean, onReachedOldestMessage: kotlin.Function0): kotlin.Unit skippable: false restartable: true params: @@ -8033,6 +8023,13 @@ public fun com.wire.android.ui.home.meetings.MeetingsScreen(homeStateHolder: com - homeStateHolder: UNSTABLE (has mutable properties or unstable members) - viewModel: UNSTABLE (has mutable properties or unstable members) +@Composable +private fun com.wire.android.ui.home.meetings.NotEstablishedDialog(dialogState: com.wire.android.ui.common.visbility.VisibilityState): kotlin.Unit + skippable: false + restartable: true + params: + - dialogState: UNSTABLE (has mutable properties or unstable members) + @Composable public fun com.wire.android.ui.home.messagecomposer.ActiveMessageComposerInput(conversationId: com.wire.kalium.logic.data.id.QualifiedID, messageComposition: com.wire.android.ui.home.messagecomposer.model.MessageComposition, messageTextState: androidx.compose.foundation.text.input.TextFieldState, isTextExpanded: kotlin.Boolean, inputType: com.wire.android.ui.home.messagecomposer.state.InputType, focusRequester: androidx.compose.ui.focus.FocusRequester, keyboardOptions: androidx.compose.foundation.text.KeyboardOptions, onKeyboardAction: androidx.compose.foundation.text.input.KeyboardActionHandler?, canSendMessage: kotlin.Boolean, onSendButtonClicked: kotlin.Function0, onEditButtonClicked: kotlin.Function0, onChangeSelfDeletionClicked: kotlin.Function1<@[ParameterName(name = \, onToggleInputSize: kotlin.Function0, onCancelReply: kotlin.Function0, onCancelEdit: kotlin.Function0, onFocused: kotlin.Function0, onSelectedLineIndexChanged: kotlin.Function1, onLineBottomYCoordinateChanged: kotlin.Function1, showOptions: kotlin.Boolean, optionsSelected: kotlin.Boolean, onPlusClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: false @@ -11786,6 +11783,13 @@ public fun com.wire.android.ui.userprofile.teammigration.step4.TeamMigrationDone - navigator: STABLE (marked @Stable or @Immutable) - teamMigrationViewModel: UNSTABLE (has mutable properties or unstable members) +@Composable +internal fun com.wire.android.ui.wireActivityCurrentBackStackEntryAsState(navigator: com.wire.android.navigation.Navigator): androidx.compose.runtime.State + skippable: true + restartable: true + params: + - navigator: STABLE (marked @Stable or @Immutable) + @Composable public fun com.wire.android.util.lifecycle.rememberLifecycleEvent(lifecycleOwner: androidx.lifecycle.LifecycleOwner): androidx.lifecycle.Lifecycle.Event skippable: false From 312a80dc035c116aae937c437dd57a982b204e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 31 Jul 2026 15:14:48 +0200 Subject: [PATCH 3/4] update kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 29739311b88..f13da7ae506 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 29739311b888f6fbad77d771eb8135a48ac50be5 +Subproject commit f13da7ae506c317df6a37567e84f69e4609f4b7f From 44055ddc9c3737f8405c43b7173947b6d2b35c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 31 Jul 2026 16:52:59 +0200 Subject: [PATCH 4/4] update kalium ref --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index f13da7ae506..24033ed5fcd 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit f13da7ae506c317df6a37567e84f69e4609f4b7f +Subproject commit 24033ed5fcdcda7e4dbecb3fed9a0ff5e7aab815