From 938f1d57810c5ca47159d2285cf2800fa3a0aac5 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 20 May 2026 15:11:17 +0200 Subject: [PATCH 1/6] feat: Restrict access to upload for Viewers profile --- .../android/ui/common/AttachmentButton.kt | 33 +++++++++++-- .../conversations/MessageComposerViewState.kt | 1 + .../composer/MessageComposerViewModel.kt | 45 ++++++++++++++++- .../home/messagecomposer/AdditionalOptions.kt | 2 + .../home/messagecomposer/AttachmentOptions.kt | 48 ++++++++++++------- .../messagecomposer/EnabledMessageComposer.kt | 1 + .../MessageComposerViewModelArrangement.kt | 42 ++++++++++++++-- .../composer/MessageComposerViewModelTest.kt | 44 +++++++++++++++++ kalium | 2 +- 9 files changed, 190 insertions(+), 28 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/AttachmentButton.kt b/app/src/main/kotlin/com/wire/android/ui/common/AttachmentButton.kt index 6c9f5d403c2..65c81021f9e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/AttachmentButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/AttachmentButton.kt @@ -48,19 +48,42 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography @Composable -fun AttachmentButton(@DrawableRes icon: Int, labelStyle: TextStyle, modifier: Modifier = Modifier, text: String = "", onClick: () -> Unit) { +fun AttachmentButton( + @DrawableRes icon: Int, + labelStyle: TextStyle, + modifier: Modifier = Modifier, + text: String = "", + enabled: Boolean = true, + onClick: () -> Unit, +) { + val buttonBackgroundColor = if (enabled) { + MaterialTheme.wireColorScheme.primaryButtonEnabled + } else { + MaterialTheme.wireColorScheme.primaryButtonDisabled + } + val buttonContentColor = if (enabled) { + MaterialTheme.wireColorScheme.onPrimaryButtonEnabled + } else { + MaterialTheme.wireColorScheme.onPrimaryButtonDisabled + } + val labelColor = if (enabled) { + MaterialTheme.wireColorScheme.onBackground + } else { + MaterialTheme.wireColorScheme.secondaryText + } + Column( modifier = modifier .padding(dimensions().spacing4x) .clip(RoundedCornerShape(size = MaterialTheme.wireDimensions.buttonSmallCornerSize)) - .clickable { onClick() } + .clickable(enabled = enabled) { onClick() } .padding(dimensions().spacing8x), horizontalAlignment = Alignment.CenterHorizontally ) { Box( modifier = Modifier .size(dimensions().attachmentButtonSize) - .background(MaterialTheme.wireColorScheme.primaryButtonEnabled, CircleShape) + .background(buttonBackgroundColor, CircleShape) .padding(dimensions().spacing2x) ) { Image( @@ -70,7 +93,7 @@ fun AttachmentButton(@DrawableRes icon: Int, labelStyle: TextStyle, modifier: Mo modifier = Modifier .padding(dimensions().spacing8x) .align(Alignment.Center), - colorFilter = ColorFilter.tint(MaterialTheme.wireColorScheme.onPrimaryButtonEnabled) + colorFilter = ColorFilter.tint(buttonContentColor) ) } VerticalSpace.x4() @@ -80,7 +103,7 @@ fun AttachmentButton(@DrawableRes icon: Int, labelStyle: TextStyle, modifier: Mo maxLines = 2, textAlign = TextAlign.Center, style = labelStyle, - color = MaterialTheme.wireColorScheme.onBackground, + color = labelColor, ) Spacer(modifier = Modifier.weight(1F)) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt index c6c15311abd..25bc058efbf 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.data.id.MessageId data class MessageComposerViewState( val isFileSharingEnabled: Boolean = true, + val areAttachmentOptionsEnabled: Boolean = true, val interactionAvailability: InteractionAvailability = InteractionAvailability.ENABLED, val mentionSearchResult: List = listOf(), val mentionSearchQuery: String = String.EMPTY, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt index db9ade80fb7..f949096b1fe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.datastore.GlobalDataStore import com.wire.android.mapper.ContactMapper import com.wire.android.ui.home.conversations.ConversationNavArgs @@ -32,22 +33,24 @@ import com.wire.android.ui.home.conversations.InvalidLinkDialogState import com.wire.android.ui.home.conversations.MessageComposerViewState import com.wire.android.ui.home.conversations.VisitLinkDialogState import com.wire.android.ui.home.conversations.model.UIMessage -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode +import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult +import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase -import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase @@ -55,10 +58,13 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletion import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -74,6 +80,8 @@ class MessageComposerViewModel @Inject constructor( val savedStateHandle: SavedStateHandle, private val dispatchers: DispatcherProvider, private val isFileSharingEnabled: IsFileSharingEnabledUseCase, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val observeSelfUser: ObserveSelfUserUseCase, private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase, private val updateConversationReadDate: UpdateConversationReadDateUseCase, private val markConversationAsReadLocally: MarkConversationAsReadLocallyUseCase, @@ -116,6 +124,7 @@ class MessageComposerViewModel @Inject constructor( initTempWritableImageUri() observeIsTypingAvailable() setFileSharingStatus() + observeAttachmentOptionsAvailability() getEnterToSendState() observeCallState() } @@ -199,6 +208,38 @@ class MessageComposerViewModel @Inject constructor( } } + private fun observeAttachmentOptionsAvailability() { + viewModelScope.launch { + + combine( + observeSelfUser().distinctUntilChanged(), + observeConversationDetails(conversationId) + .filterIsInstance() + .map { it.conversationDetails } + .distinctUntilChanged(), + ) { selfUser, conversationDetails -> + canUseAttachmentOptions( + selfUser = selfUser, + conversationDetails = conversationDetails, + ) + }.collectLatest { areAttachmentOptionsEnabled -> + messageComposerViewState.value = messageComposerViewState.value.copy( + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled + ) + } + } + } + + private fun canUseAttachmentOptions( + selfUser: SelfUser, + conversationDetails: ConversationDetails, + ): Boolean { + val isCellsConversation = (conversationDetails as? ConversationDetails.Group)?.wireCell != null + val isConversationOwnedBySelfTeam = conversationDetails.conversation.teamId == selfUser.teamId + + return !(isCellsConversation && !isConversationOwnedBySelfTeam) + } + fun updateConversationReadDate(utcISO: String) { val instant = Instant.parse(utcISO) lastReadInstant = instant diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt index 9d16458a4e0..9ae13d5aa06 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt @@ -94,6 +94,7 @@ fun AdditionalOptionsMenu( @Composable fun AdditionalOptionSubMenu( isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, optionsVisible: Boolean, onPermissionPermanentlyDenied: (type: ConversationActionPermissionType) -> Unit, onLocationPickerClicked: () -> Unit, @@ -115,6 +116,7 @@ fun AdditionalOptionSubMenu( tempWritableImageUri = tempWritableImageUri, tempWritableVideoUri = tempWritableVideoUri, isFileSharingEnabled = isFileSharingEnabled, + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled, onRecordAudioMessageClicked = onRecordAudioMessageClicked, onLocationPickerClicked = onLocationPickerClicked, onPermissionPermanentlyDenied = onPermissionPermanentlyDenied, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt index a9ea0fb8e8b..786527e0a6d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt @@ -71,6 +71,7 @@ fun AttachmentOptionsComponent( tempWritableImageUri: Uri?, tempWritableVideoUri: Uri?, isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, onLocationPickerClicked: () -> Unit, onPermissionPermanentlyDenied: (type: ConversationActionPermissionType) -> Unit, modifier: Modifier = Modifier, @@ -80,6 +81,7 @@ fun AttachmentOptionsComponent( val attachmentOptions = buildAttachmentOptionItems( isFileSharingEnabled = isFileSharingEnabled, + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled, tempWritableImageUri = tempWritableImageUri, tempWritableVideoUri = tempWritableVideoUri, onImagesPicked = onImagesPicked, @@ -163,7 +165,8 @@ fun AttachmentOptionsComponent( icon = option.icon, labelStyle = labelStyle, modifier = Modifier.scale(animatedScale), - text = stringResource(option.text) + text = stringResource(option.text), + enabled = option.isEnabled, ) { option.onClick() } } } @@ -278,6 +281,7 @@ private fun rememberCaptureVideoFlow( @Composable private fun buildAttachmentOptionItems( isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, tempWritableImageUri: Uri?, tempWritableVideoUri: Uri?, onImagesPicked: (List) -> Unit, @@ -311,39 +315,44 @@ private fun buildAttachmentOptionItems( with(localFeatureVisibilityFlags) { add( AttachmentOptionItem( - isFileSharingEnabled, - R.string.attachment_share_file, - R.drawable.ic_attach_file + shouldShow = isFileSharingEnabled, + isEnabled = areAttachmentOptionsEnabled, + text = R.string.attachment_share_file, + icon = R.drawable.ic_attach_file, ) { fileFlow.launch() } ) add( AttachmentOptionItem( - isFileSharingEnabled, - R.string.attachment_share_image, - R.drawable.ic_gallery + shouldShow = isFileSharingEnabled, + isEnabled = areAttachmentOptionsEnabled, + text = R.string.attachment_share_image, + icon = R.drawable.ic_gallery, ) { galleryFlow.launch() } ) add( AttachmentOptionItem( - isFileSharingEnabled, - R.string.attachment_take_photo, - R.drawable.ic_camera + shouldShow = isFileSharingEnabled, + isEnabled = areAttachmentOptionsEnabled, + text = R.string.attachment_take_photo, + icon = R.drawable.ic_camera, ) { takePictureFlow?.launch() } ) add( AttachmentOptionItem( - isFileSharingEnabled, - R.string.attachment_record_video, - R.drawable.ic_video + shouldShow = isFileSharingEnabled, + isEnabled = areAttachmentOptionsEnabled, + text = R.string.attachment_record_video, + icon = R.drawable.ic_video, ) { captureVideoFlow?.launch() } ) if (AudioMessagesIcon) { add( AttachmentOptionItem( - isFileSharingEnabled, - R.string.attachment_voice_message, - R.drawable.ic_mic_on, - onRecordAudioMessageClicked + shouldShow = isFileSharingEnabled, + isEnabled = areAttachmentOptionsEnabled, + text = R.string.attachment_voice_message, + icon = R.drawable.ic_mic_on, + onClick = onRecordAudioMessageClicked, ) ) } @@ -363,6 +372,7 @@ private fun buildAttachmentOptionItems( private data class AttachmentOptionItem( val shouldShow: Boolean = true, + val isEnabled: Boolean = true, @StringRes val text: Int, @DrawableRes val icon: Int, val onClick: () -> Unit @@ -377,6 +387,7 @@ fun PreviewAttachmentComponents() { onImagesPicked = {}, onAttachmentPicked = {}, isFileSharingEnabled = true, + areAttachmentOptionsEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, onRecordAudioMessageClicked = {}, @@ -398,6 +409,7 @@ fun PreviewAttachmentOptionsComponentSmallScreen() { onAttachmentPicked = {}, onImagesPicked = {}, isFileSharingEnabled = true, + areAttachmentOptionsEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, onRecordAudioMessageClicked = {}, @@ -420,6 +432,7 @@ fun PreviewAttachmentOptionsComponentNormalScreen() { onAttachmentPicked = {}, onImagesPicked = {}, isFileSharingEnabled = true, + areAttachmentOptionsEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, onRecordAudioMessageClicked = {}, @@ -442,6 +455,7 @@ fun PreviewAttachmentOptionsComponentTabledScreen() { onAttachmentPicked = {}, onImagesPicked = {}, isFileSharingEnabled = true, + areAttachmentOptionsEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, onRecordAudioMessageClicked = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt index 93d95467d34..b1409350162 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt @@ -453,6 +453,7 @@ fun EnabledMessageComposer( AdditionalOptionSubMenu( optionsVisible = inputStateHolder.optionsVisible, isFileSharingEnabled = messageComposerViewState.value.isFileSharingEnabled, + areAttachmentOptionsEnabled = messageComposerViewState.value.areAttachmentOptionsEnabled, additionalOptionsState = additionalOptionStateHolder.additionalOptionsSubMenuState, onRecordAudioMessageClicked = { if (!messageComposerViewState.value.isCallOngoing) { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index d598fedad15..1f7d1b4a238 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -46,10 +46,12 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.AssetId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId @@ -57,10 +59,12 @@ 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.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadResult import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase @@ -69,6 +73,7 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletion import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -83,6 +88,11 @@ import kotlinx.datetime.Instant internal class MessageComposerViewModelArrangement { val conversationId: ConversationId = ConversationId("some-dummy-value", "some.dummy.domain") + private var arrangedSelfUser: SelfUser = TestUser.SELF_USER + private var arrangedConversationDetails: ConversationDetails = mockConversationDetailsGroup( + conversationName = "GROUP Name", + mockedConversationId = conversationId, + ) init { // Tests setup @@ -100,6 +110,10 @@ internal class MessageComposerViewModelArrangement { coEvery { currentSessionFlowUseCase() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) + coEvery { observeSelfUserUseCase() } returns flowOf(arrangedSelfUser) + coEvery { observeConversationDetailsUseCase(any()) } returns flowOf( + ObserveConversationDetailsUseCase.Result.Success(arrangedConversationDetails) + ) coEvery { globalDataStore.enterToSendFlow() } returns flowOf(false) coEvery { observeEstablishedCalls() } returns emptyFlow() coEvery { markConversationAsReadLocallyUseCase(any(), any()) } returns MarkConversationAsReadResult.Success(false) @@ -120,6 +134,12 @@ internal class MessageComposerViewModelArrangement { @MockK private lateinit var observeConversationInteractionAvailabilityUseCase: ObserveConversationInteractionAvailabilityUseCase + @MockK + private lateinit var observeConversationDetailsUseCase: ObserveConversationDetailsUseCase + + @MockK + private lateinit var observeSelfUserUseCase: ObserveSelfUserUseCase + @MockK private lateinit var updateConversationReadDateUseCase: UpdateConversationReadDateUseCase @@ -163,6 +183,8 @@ internal class MessageComposerViewModelArrangement { savedStateHandle = savedStateHandle, dispatchers = TestDispatcherProvider(), isFileSharingEnabled = isFileSharingEnabledUseCase, + observeConversationDetails = observeConversationDetailsUseCase, + observeSelfUser = observeSelfUserUseCase, updateConversationReadDate = updateConversationReadDateUseCase, markConversationAsReadLocally = markConversationAsReadLocallyUseCase, observeConversationInteractionAvailability = observeConversationInteractionAvailabilityUseCase, @@ -197,6 +219,18 @@ internal class MessageComposerViewModelArrangement { coEvery { currentSessionFlowUseCase() } returns resultFlow } + fun withSelfUser(selfUser: SelfUser) = apply { + arrangedSelfUser = selfUser + coEvery { observeSelfUserUseCase() } returns flowOf(arrangedSelfUser) + } + + fun withConversationDetails(conversationDetails: ConversationDetails) = apply { + arrangedConversationDetails = conversationDetails + coEvery { observeConversationDetailsUseCase(any()) } returns flowOf( + ObserveConversationDetailsUseCase.Result.Success(arrangedConversationDetails) + ) + } + fun arrange() = this to viewModel } @@ -223,14 +257,16 @@ internal fun withMockConversationDetailsOneOnOne( internal fun mockConversationDetailsGroup( conversationName: String, - mockedConversationId: ConversationId = ConversationId("someId", "someDomain") + mockedConversationId: ConversationId = ConversationId("someId", "someDomain"), + teamId: TeamId? = TestConversation.GROUP().teamId, + wireCell: String? = null, ) = ConversationDetails.Group.Regular( conversation = TestConversation.GROUP() - .copy(name = conversationName, id = mockedConversationId), + .copy(name = conversationName, id = mockedConversationId, teamId = teamId), hasOngoingCall = false, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member, - wireCell = null, + wireCell = wireCell, ) internal fun mockUITextMessage(id: String = "someId", userName: String = "mockUserName"): UIMessage { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt index a65c98da7ae..1864bb9a9d6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt @@ -20,8 +20,12 @@ package com.wire.android.ui.home.conversations.composer import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension +import com.wire.android.framework.TestUser import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.InteractionAvailability +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.data.user.type.UserTypeInfo import com.wire.kalium.logic.feature.session.CurrentSessionResult import io.mockk.coVerify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -40,6 +44,46 @@ import org.junit.jupiter.api.extension.ExtendWith @Suppress("LargeClass") class MessageComposerViewModelTest { + @Test + fun `given guest in foreign-team cells conversation when init then attachment options are disabled`() = runTest { + val foreignTeamId = TeamId("foreign-team") + + val (_, viewModel) = MessageComposerViewModelArrangement() + .withSuccessfulViewModelInit() + .withSelfUser(TestUser.SELF_USER.copy(userType = UserTypeInfo.Regular(UserType.GUEST))) + .withConversationDetails( + mockConversationDetailsGroup( + conversationName = "Foreign team cells", + teamId = foreignTeamId, + wireCell = "wire-cell-id", + ) + ) + .arrange() + + advanceUntilIdle() + + assertEquals(false, viewModel.messageComposerViewState.value.areAttachmentOptionsEnabled) + } + + @Test + fun `given guest in self-team cells conversation when init then attachment options stay enabled`() = runTest { + val (_, viewModel) = MessageComposerViewModelArrangement() + .withSuccessfulViewModelInit() + .withSelfUser(TestUser.SELF_USER.copy(userType = UserTypeInfo.Regular(UserType.GUEST))) + .withConversationDetails( + mockConversationDetailsGroup( + conversationName = "Own team cells", + teamId = TestUser.SELF_USER.teamId, + wireCell = "wire-cell-id", + ) + ) + .arrange() + + advanceUntilIdle() + + assertTrue(viewModel.messageComposerViewState.value.areAttachmentOptionsEnabled) + } + @Test fun `given that user types a text message, when invoked typing invoked, then send typing event is called`() = runTest { // given diff --git a/kalium b/kalium index cc41a919dac..5e2f51636f4 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit cc41a919dacdfbc4c82ceff0493a1e09f0d7a46a +Subproject commit 5e2f51636f4ab310b8bb328cb3ef5b8a731b677e From 63ba58aaaf6a1c9e9dc01af711c4c5ea7b964aff Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 20 May 2026 17:29:23 +0200 Subject: [PATCH 2/6] feat: color for disabled secondary button --- .../home/messagecomposer/AdditionalOptions.kt | 8 +- .../messagecomposer/EnabledMessageComposer.kt | 9 ++- .../messagecomposer/MessageComposeActions.kt | 16 ++-- .../common/button/WireSecondaryIconButton.kt | 74 ++++++++++++------- .../wire/android/ui/theme/WireColorScheme.kt | 2 +- 5 files changed, 74 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt index 9ae13d5aa06..726e21592b4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt @@ -48,6 +48,7 @@ fun AdditionalOptionsMenu( isEditing: Boolean, isMentionActive: Boolean, isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, onAdditionalOptionsMenuClicked: () -> Unit, onMentionButtonClicked: (() -> Unit), onPingOptionClicked: () -> Unit, @@ -75,7 +76,8 @@ fun AdditionalOptionsMenu( onRichEditingButtonClicked = onRichEditingButtonClicked, onPingClicked = onPingOptionClicked, onDrawingModeClicked = onDrawingModeClicked, - isFileSharingEnabled = isFileSharingEnabled + isFileSharingEnabled = isFileSharingEnabled, + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled, ) } @@ -141,6 +143,7 @@ fun AttachmentAndAdditionalOptionsMenuItems( attachmentsVisible: Boolean, isMentionActive: Boolean, isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, onMentionButtonClicked: () -> Unit, onSelfDeletionOptionButtonClicked: (SelfDeletionTimer) -> Unit, modifier: Modifier = Modifier, @@ -165,7 +168,8 @@ fun AttachmentAndAdditionalOptionsMenuItems( onGifButtonClicked = onGifButtonClicked, onRichEditingButtonClicked = onRichEditingButtonClicked, onDrawingModeClicked = onDrawingModeClicked, - isFileSharingEnabled = isFileSharingEnabled + isFileSharingEnabled = isFileSharingEnabled, + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled, ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt index b1409350162..8c116c6f509 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt @@ -388,8 +388,13 @@ fun EnabledMessageComposer( additionalOptionStateHolder.toRichTextEditing() }, onCloseRichEditingButtonClicked = additionalOptionStateHolder::toAttachmentAndAdditionalOptionsMenu, - onDrawingModeClicked = openDrawingCanvas, - isFileSharingEnabled = messageComposerViewState.value.isFileSharingEnabled + onDrawingModeClicked = { + if (messageComposerViewState.value.areAttachmentOptionsEnabled) { + openDrawingCanvas() + } + }, + isFileSharingEnabled = messageComposerViewState.value.isFileSharingEnabled, + areAttachmentOptionsEnabled = messageComposerViewState.value.areAttachmentOptionsEnabled, ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt index 0f00dd74f6f..8e743b874ef 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt @@ -59,6 +59,7 @@ fun MessageComposeActions( onGifButtonClicked: () -> Unit, onRichEditingButtonClicked: () -> Unit, isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, isMentionActive: Boolean = true, onDrawingModeClicked: () -> Unit ) { @@ -82,7 +83,8 @@ fun MessageComposeActions( onPingButtonClicked = onPingButtonClicked, onMentionButtonClicked = onMentionButtonClicked, onDrawingModeClicked = onDrawingModeClicked, - isFileSharingEnabled = isFileSharingEnabled + isFileSharingEnabled = isFileSharingEnabled, + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled, ) } } @@ -92,6 +94,7 @@ private fun ComposingActions( conversationId: ConversationId, selectedOption: AdditionalOptionSelectItem, isFileSharingEnabled: Boolean, + areAttachmentOptionsEnabled: Boolean, attachmentsVisible: Boolean, isMentionActive: Boolean, onAdditionalOptionButtonClicked: () -> Unit, @@ -121,7 +124,10 @@ private fun ComposingActions( onRichEditingButtonClicked ) if (DrawingIcon && isFileSharingEnabled) { - DrawingModeAction(onDrawingModeClicked) + DrawingModeAction( + onButtonClicked = onDrawingModeClicked, + isEnabled = areAttachmentOptionsEnabled, + ) } if (EmojiIcon) AddEmojiAction({}) if (GifIcon) AddGifAction(onGifButtonClicked) @@ -176,12 +182,12 @@ private fun RichTextEditingAction(isSelected: Boolean, onButtonClicked: () -> Un } @Composable -private fun DrawingModeAction(onButtonClicked: () -> Unit) { +private fun DrawingModeAction(onButtonClicked: () -> Unit, isEnabled: Boolean) { WireSecondaryIconButton( onButtonClicked = onButtonClicked, clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true), iconResource = R.drawable.ic_drawing, - state = WireButtonState.Default, + state = if (isEnabled) WireButtonState.Default else WireButtonState.Disabled, contentDescription = R.string.content_description_conversation_enable_drawing_mode ) } @@ -299,7 +305,7 @@ fun PreviewMessageActionsBox() { .height(dimensions().spacing56x) ) { AdditionalOptionButton(isSelected = false, onClick = {}) - DrawingModeAction {} + DrawingModeAction(onButtonClicked = {}, isEnabled = true) RichTextEditingAction(true) { } AddEmojiAction {} AddGifAction {} diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryIconButton.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryIconButton.kt index 8b0cfd1a91b..9b1455e4885 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryIconButton.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/button/WireSecondaryIconButton.kt @@ -31,13 +31,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.wire.android.model.ClickBlockParams import com.wire.android.ui.common.R import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions @Composable @@ -79,38 +80,61 @@ fun WireSecondaryIconButton( ) } -@Preview +@MultipleThemePreviews @Composable fun PreviewWireSecondaryIconButton() { - WireSecondaryIconButton( - {}, - loading = false, - iconResource = R.drawable.ic_close, - contentDescription = 0 - ) + WireTheme { + WireSecondaryIconButton( + {}, + loading = false, + iconResource = R.drawable.ic_close, + contentDescription = 0 + ) + } } -@Preview +@MultipleThemePreviews @Composable fun PreviewWireSecondaryIconButtonLoading() { - WireSecondaryIconButton( - {}, - loading = true, - iconResource = R.drawable.ic_close, - contentDescription = 0 - ) + WireTheme { + WireSecondaryIconButton( + {}, + loading = true, + iconResource = R.drawable.ic_close, + contentDescription = 0 + ) + } } -@Preview +@MultipleThemePreviews @Composable fun PreviewWireSecondaryIconButtonRound() { - WireSecondaryIconButton( - {}, - loading = false, - iconResource = R.drawable.ic_close, - contentDescription = 0, - shape = CircleShape, - minSize = DpSize(40.dp, 40.dp), - minClickableSize = DpSize(48.dp, 48.dp) - ) + WireTheme { + WireSecondaryIconButton( + {}, + loading = false, + iconResource = R.drawable.ic_close, + contentDescription = 0, + shape = CircleShape, + minSize = DpSize(40.dp, 40.dp), + minClickableSize = DpSize(48.dp, 48.dp) + ) + } +} + +@MultipleThemePreviews +@Composable +fun PreviewWireSecondaryIconButtonRoundDisabled() { + WireTheme { + WireSecondaryIconButton( + {}, + loading = false, + iconResource = R.drawable.ic_close, + contentDescription = 0, + shape = CircleShape, + minSize = DpSize(40.dp, 40.dp), + minClickableSize = DpSize(48.dp, 48.dp), + state = WireButtonState.Disabled + ) + } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt index 664dc71799b..e2eb6da271b 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt @@ -277,7 +277,7 @@ private val DarkWireColorScheme = WireColorScheme( secondaryButtonEnabled = WireColorPalette.Gray90, onSecondaryButtonEnabled = Color.White, secondaryButtonEnabledOutline = WireColorPalette.Gray100, secondaryButtonDisabled = WireColorPalette.Gray95, onSecondaryButtonDisabled = WireColorPalette.Gray50, - secondaryButtonDisabledOutline = WireColorPalette.Gray95, + secondaryButtonDisabledOutline = WireColorPalette.Gray90, secondaryButtonSelected = WireColorPalette.DarkBlue800, onSecondaryButtonSelected = Color.White, secondaryButtonSelectedOutline = WireColorPalette.DarkBlue600, secondaryButtonRipple = Color.White, From 34d3829ca02bc5bb46cd1321014cfcec464cbdca Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 21 May 2026 11:01:24 +0200 Subject: [PATCH 3/6] feat: cleanup --- .../composer/MessageComposerViewModelArrangement.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index 1f7d1b4a238..567b350b749 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.conversations.composer import android.net.Uri import androidx.lifecycle.SavedStateHandle +import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.datastore.GlobalDataStore @@ -37,7 +38,6 @@ import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.MessageTime import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.FileManager import com.wire.android.util.ui.UIText import com.wire.kalium.logic.configuration.FileSharingStatus @@ -59,7 +59,6 @@ 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.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase -import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadResult From abaea7765ae5c57d2dca606d3247673a218ff33b Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 4 Jun 2026 16:10:21 +0100 Subject: [PATCH 4/6] chore: address comments --- .../di/accountScoped/ConversationModule.kt | 8 ++++ .../composer/MessageComposerViewModel.kt | 42 ++++--------------- .../MessageComposerViewModelArrangement.kt | 37 ++++------------ .../composer/MessageComposerViewModelTest.kt | 24 +---------- kalium | 2 +- 5 files changed, 27 insertions(+), 86 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt index f0379b34b56..1cdf214d021 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt @@ -39,6 +39,7 @@ import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase @@ -419,4 +420,11 @@ class ConversationModule { conversationScope: ConversationScope ): ChangeAccessForAppsInConversationUseCase = conversationScope.changeAccessForAppsInConversation + + @ViewModelScoped + @Provides + fun provideObserveSelfUserHasViewerAccessUseCase( + conversationScope: ConversationScope + ): ObserveSelfUserHasViewerAccessUseCase = + conversationScope.observeSelfUserHasViewerAccess } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt index f949096b1fe..b86aa462ec9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt @@ -39,17 +39,15 @@ import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode -import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.user.OtherUser -import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase @@ -58,13 +56,10 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletion import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase -import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -80,8 +75,6 @@ class MessageComposerViewModel @Inject constructor( val savedStateHandle: SavedStateHandle, private val dispatchers: DispatcherProvider, private val isFileSharingEnabled: IsFileSharingEnabledUseCase, - private val observeConversationDetails: ObserveConversationDetailsUseCase, - private val observeSelfUser: ObserveSelfUserUseCase, private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase, private val updateConversationReadDate: UpdateConversationReadDateUseCase, private val markConversationAsReadLocally: MarkConversationAsReadLocallyUseCase, @@ -95,6 +88,7 @@ class MessageComposerViewModel @Inject constructor( private val currentSessionFlowUseCase: CurrentSessionFlowUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val globalDataStore: GlobalDataStore, + private val observeSelfUserHasViewerAccess: ObserveSelfUserHasViewerAccessUseCase, ) : ViewModel() { var messageComposerViewState = mutableStateOf(MessageComposerViewState()) @@ -210,35 +204,15 @@ class MessageComposerViewModel @Inject constructor( private fun observeAttachmentOptionsAvailability() { viewModelScope.launch { - - combine( - observeSelfUser().distinctUntilChanged(), - observeConversationDetails(conversationId) - .filterIsInstance() - .map { it.conversationDetails } - .distinctUntilChanged(), - ) { selfUser, conversationDetails -> - canUseAttachmentOptions( - selfUser = selfUser, - conversationDetails = conversationDetails, - ) - }.collectLatest { areAttachmentOptionsEnabled -> - messageComposerViewState.value = messageComposerViewState.value.copy( - areAttachmentOptionsEnabled = areAttachmentOptionsEnabled - ) - } + observeSelfUserHasViewerAccess(conversationId) + .collectLatest { areAttachmentOptionsEnabled -> + messageComposerViewState.value = messageComposerViewState.value.copy( + areAttachmentOptionsEnabled = areAttachmentOptionsEnabled + ) + } } } - private fun canUseAttachmentOptions( - selfUser: SelfUser, - conversationDetails: ConversationDetails, - ): Boolean { - val isCellsConversation = (conversationDetails as? ConversationDetails.Group)?.wireCell != null - val isConversationOwnedBySelfTeam = conversationDetails.conversation.teamId == selfUser.teamId - - return !(isCellsConversation && !isConversationOwnedBySelfTeam) - } fun updateConversationReadDate(utcISO: String) { val instant = Instant.parse(utcISO) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index 567b350b749..eb73ba896f8 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -51,7 +51,6 @@ import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.AssetId import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser -import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId @@ -63,7 +62,7 @@ import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadResult import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase @@ -72,7 +71,6 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletion import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase -import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -87,11 +85,6 @@ import kotlinx.datetime.Instant internal class MessageComposerViewModelArrangement { val conversationId: ConversationId = ConversationId("some-dummy-value", "some.dummy.domain") - private var arrangedSelfUser: SelfUser = TestUser.SELF_USER - private var arrangedConversationDetails: ConversationDetails = mockConversationDetailsGroup( - conversationName = "GROUP Name", - mockedConversationId = conversationId, - ) init { // Tests setup @@ -109,10 +102,7 @@ internal class MessageComposerViewModelArrangement { coEvery { currentSessionFlowUseCase() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) - coEvery { observeSelfUserUseCase() } returns flowOf(arrangedSelfUser) - coEvery { observeConversationDetailsUseCase(any()) } returns flowOf( - ObserveConversationDetailsUseCase.Result.Success(arrangedConversationDetails) - ) + coEvery { observeSelfUserHasViewerAccessUseCase(any()) } returns flowOf(true) coEvery { globalDataStore.enterToSendFlow() } returns flowOf(false) coEvery { observeEstablishedCalls() } returns emptyFlow() coEvery { markConversationAsReadLocallyUseCase(any(), any()) } returns MarkConversationAsReadResult.Success(false) @@ -134,10 +124,7 @@ internal class MessageComposerViewModelArrangement { private lateinit var observeConversationInteractionAvailabilityUseCase: ObserveConversationInteractionAvailabilityUseCase @MockK - private lateinit var observeConversationDetailsUseCase: ObserveConversationDetailsUseCase - - @MockK - private lateinit var observeSelfUserUseCase: ObserveSelfUserUseCase + private lateinit var observeSelfUserHasViewerAccessUseCase: ObserveSelfUserHasViewerAccessUseCase @MockK private lateinit var updateConversationReadDateUseCase: UpdateConversationReadDateUseCase @@ -182,8 +169,6 @@ internal class MessageComposerViewModelArrangement { savedStateHandle = savedStateHandle, dispatchers = TestDispatcherProvider(), isFileSharingEnabled = isFileSharingEnabledUseCase, - observeConversationDetails = observeConversationDetailsUseCase, - observeSelfUser = observeSelfUserUseCase, updateConversationReadDate = updateConversationReadDateUseCase, markConversationAsReadLocally = markConversationAsReadLocallyUseCase, observeConversationInteractionAvailability = observeConversationInteractionAvailabilityUseCase, @@ -197,7 +182,7 @@ internal class MessageComposerViewModelArrangement { currentSessionFlowUseCase = currentSessionFlowUseCase, observeEstablishedCalls = observeEstablishedCalls, globalDataStore = globalDataStore, - ) + observeSelfUserHasViewerAccess = observeSelfUserHasViewerAccessUseCase, ) } fun withSuccessfulViewModelInit( @@ -218,16 +203,8 @@ internal class MessageComposerViewModelArrangement { coEvery { currentSessionFlowUseCase() } returns resultFlow } - fun withSelfUser(selfUser: SelfUser) = apply { - arrangedSelfUser = selfUser - coEvery { observeSelfUserUseCase() } returns flowOf(arrangedSelfUser) - } - - fun withConversationDetails(conversationDetails: ConversationDetails) = apply { - arrangedConversationDetails = conversationDetails - coEvery { observeConversationDetailsUseCase(any()) } returns flowOf( - ObserveConversationDetailsUseCase.Result.Success(arrangedConversationDetails) - ) + fun withAttachmentOptionsAvailability(available: Boolean) = apply { + coEvery { observeSelfUserHasViewerAccessUseCase(any()) } returns flowOf(available) } fun arrange() = this to viewModel @@ -309,3 +286,5 @@ internal fun mockUIAudioMessage(id: String = "someId", userName: String = "mockU ) } } + + diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt index 1864bb9a9d6..cf8471b6fea 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt @@ -20,12 +20,8 @@ package com.wire.android.ui.home.conversations.composer import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension -import com.wire.android.framework.TestUser import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.InteractionAvailability -import com.wire.kalium.logic.data.id.TeamId -import com.wire.kalium.logic.data.user.type.UserType -import com.wire.kalium.logic.data.user.type.UserTypeInfo import com.wire.kalium.logic.feature.session.CurrentSessionResult import io.mockk.coVerify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,18 +42,9 @@ class MessageComposerViewModelTest { @Test fun `given guest in foreign-team cells conversation when init then attachment options are disabled`() = runTest { - val foreignTeamId = TeamId("foreign-team") - val (_, viewModel) = MessageComposerViewModelArrangement() .withSuccessfulViewModelInit() - .withSelfUser(TestUser.SELF_USER.copy(userType = UserTypeInfo.Regular(UserType.GUEST))) - .withConversationDetails( - mockConversationDetailsGroup( - conversationName = "Foreign team cells", - teamId = foreignTeamId, - wireCell = "wire-cell-id", - ) - ) + .withAttachmentOptionsAvailability(false) .arrange() advanceUntilIdle() @@ -69,14 +56,7 @@ class MessageComposerViewModelTest { fun `given guest in self-team cells conversation when init then attachment options stay enabled`() = runTest { val (_, viewModel) = MessageComposerViewModelArrangement() .withSuccessfulViewModelInit() - .withSelfUser(TestUser.SELF_USER.copy(userType = UserTypeInfo.Regular(UserType.GUEST))) - .withConversationDetails( - mockConversationDetailsGroup( - conversationName = "Own team cells", - teamId = TestUser.SELF_USER.teamId, - wireCell = "wire-cell-id", - ) - ) + .withAttachmentOptionsAvailability(true) .arrange() advanceUntilIdle() diff --git a/kalium b/kalium index 122f2bd1c5e..e996a3aced8 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 122f2bd1c5e31459d5d090aeeee0a808064a5ef1 +Subproject commit e996a3aced8de5a826c8dc7f071fccf5d35f6ef2 From f11cbc9dbcb266e6fd5370d70965da07e170e6b9 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 4 Jun 2026 16:16:39 +0100 Subject: [PATCH 5/6] chore: cleanup --- .../ui/home/conversations/ConversationCoreViewModelFactory.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt index 354b41216df..68965e67ca2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt @@ -85,6 +85,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.SetNotifiedAboutConversationUnderLegalHoldUseCase import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase @@ -192,6 +193,7 @@ class ConversationCoreViewModelFactory @Inject constructor( private val onlineEditor: OnlineEditor, private val featureFlags: KaliumConfigs, private val getWireCellsConfig: GetWireCellConfigurationUseCase, + private val observeSelfUserHasViewerAccess: ObserveSelfUserHasViewerAccessUseCase, private val networkStateObserver: NetworkStateObserver, @CurrentAccount private val selfUserId: UserId, ) { @@ -234,6 +236,7 @@ class ConversationCoreViewModelFactory @Inject constructor( currentSessionFlowUseCase = currentSessionFlowUseCase, observeEstablishedCalls = observeEstablishedCalls, globalDataStore = globalDataStore, + observeSelfUserHasViewerAccess = observeSelfUserHasViewerAccess ) fun sendMessageViewModel(savedStateHandle: SavedStateHandle) = SendMessageViewModel( From d7d788a2f3eb088360e3a9fea9a3e87cf5be5e8e Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 4 Jun 2026 16:52:01 +0100 Subject: [PATCH 6/6] chore: rename use case --- .../android/di/accountScoped/ConversationModule.kt | 4 ++-- .../conversations/ConversationCoreViewModelFactory.kt | 4 ++-- .../conversations/composer/MessageComposerViewModel.kt | 4 ++-- .../composer/MessageComposerViewModelArrangement.kt | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt index 1cdf214d021..04967b9d58f 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt @@ -39,7 +39,7 @@ import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase -import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessOnConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase @@ -425,6 +425,6 @@ class ConversationModule { @Provides fun provideObserveSelfUserHasViewerAccessUseCase( conversationScope: ConversationScope - ): ObserveSelfUserHasViewerAccessUseCase = + ): ObserveSelfUserHasViewerAccessOnConversationUseCase = conversationScope.observeSelfUserHasViewerAccess } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt index 68965e67ca2..9f4bd62d862 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt @@ -85,7 +85,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase -import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessOnConversationUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.SetNotifiedAboutConversationUnderLegalHoldUseCase import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase @@ -193,7 +193,7 @@ class ConversationCoreViewModelFactory @Inject constructor( private val onlineEditor: OnlineEditor, private val featureFlags: KaliumConfigs, private val getWireCellsConfig: GetWireCellConfigurationUseCase, - private val observeSelfUserHasViewerAccess: ObserveSelfUserHasViewerAccessUseCase, + private val observeSelfUserHasViewerAccess: ObserveSelfUserHasViewerAccessOnConversationUseCase, private val networkStateObserver: NetworkStateObserver, @CurrentAccount private val selfUserId: UserId, ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt index e7e916724d3..90f52d8b813 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt @@ -47,7 +47,7 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase -import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessOnConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase @@ -85,7 +85,7 @@ class MessageComposerViewModel( private val currentSessionFlowUseCase: CurrentSessionFlowUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val globalDataStore: GlobalDataStore, - private val observeSelfUserHasViewerAccess: ObserveSelfUserHasViewerAccessUseCase, + private val observeSelfUserHasViewerAccess: ObserveSelfUserHasViewerAccessOnConversationUseCase, ) : ViewModel() { var messageComposerViewState = mutableStateOf(MessageComposerViewState()) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index 29133744439..7df3d59bcfa 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -62,7 +62,7 @@ import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadResult import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase -import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessUseCase +import com.wire.kalium.logic.feature.conversation.ObserveSelfUserHasViewerAccessOnConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase @@ -102,7 +102,7 @@ internal class MessageComposerViewModelArrangement { coEvery { currentSessionFlowUseCase() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) - coEvery { observeSelfUserHasViewerAccessUseCase(any()) } returns flowOf(true) + coEvery { observeSelfUserHasViewerAccessOnConversationUseCase(any()) } returns flowOf(true) coEvery { globalDataStore.enterToSendFlow() } returns flowOf(false) coEvery { observeEstablishedCalls() } returns emptyFlow() coEvery { markConversationAsReadLocallyUseCase(any(), any()) } returns MarkConversationAsReadResult.Success(false) @@ -124,7 +124,7 @@ internal class MessageComposerViewModelArrangement { private lateinit var observeConversationInteractionAvailabilityUseCase: ObserveConversationInteractionAvailabilityUseCase @MockK - private lateinit var observeSelfUserHasViewerAccessUseCase: ObserveSelfUserHasViewerAccessUseCase + private lateinit var observeSelfUserHasViewerAccessOnConversationUseCase: ObserveSelfUserHasViewerAccessOnConversationUseCase @MockK private lateinit var updateConversationReadDateUseCase: UpdateConversationReadDateUseCase @@ -182,7 +182,7 @@ internal class MessageComposerViewModelArrangement { currentSessionFlowUseCase = currentSessionFlowUseCase, observeEstablishedCalls = observeEstablishedCalls, globalDataStore = globalDataStore, - observeSelfUserHasViewerAccess = observeSelfUserHasViewerAccessUseCase, ) + observeSelfUserHasViewerAccess = observeSelfUserHasViewerAccessOnConversationUseCase, ) } fun withSuccessfulViewModelInit( @@ -204,7 +204,7 @@ internal class MessageComposerViewModelArrangement { } fun withAttachmentOptionsAvailability(available: Boolean) = apply { - coEvery { observeSelfUserHasViewerAccessUseCase(any()) } returns flowOf(available) + coEvery { observeSelfUserHasViewerAccessOnConversationUseCase(any()) } returns flowOf(available) } fun arrange() = this to viewModel