From 938f1d57810c5ca47159d2285cf2800fa3a0aac5 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 20 May 2026 15:11:17 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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