From f1d68bad27f86b7289d958b34050c2489b9e99ec Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Tue, 6 Aug 2024 14:02:53 +0200 Subject: [PATCH] fix: sharing extension for text content (WPB-10466) (#3291) --- .../home/conversations/ConversationNavArgs.kt | 3 +- .../sendmessage/SendMessageViewModel.kt | 9 + .../ImportMediaAuthenticatedViewModel.kt | 9 +- .../android/ui/sharing/ImportMediaScreen.kt | 180 ++++++++++++------ .../SendMessageViewModelArrangement.kt | 15 ++ .../sendmessage/SendMessageViewModelTest.kt | 55 ++++++ 6 files changed, 208 insertions(+), 63 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationNavArgs.kt index 764c9fe3a83..4e5520f7c92 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationNavArgs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationNavArgs.kt @@ -25,5 +25,6 @@ import kotlinx.serialization.Serializable data class ConversationNavArgs( val conversationId: ConversationId, val searchedMessageId: String? = null, - val pendingBundles: ArrayList? = null + val pendingBundles: ArrayList? = null, + val pendingTextBundle: String? = null ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt index ff6a7a24fea..b1a1fad0478 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt @@ -113,6 +113,9 @@ class SendMessageViewModel @Inject constructor( ) init { + conversationNavArgs.pendingTextBundle?.let { text -> + trySendPendingMessageBundle(text) + } conversationNavArgs.pendingBundles?.let { assetBundles -> trySendMessages( assetBundles.map { assetBundle -> @@ -135,6 +138,12 @@ class SendMessageViewModel @Inject constructor( private suspend fun shouldInformAboutUnderLegalHoldBeforeSendingMessage(conversationId: ConversationId) = observeConversationUnderLegalHoldNotified(conversationId).first().let { !it } + private fun trySendPendingMessageBundle(pendingMessage: String) { + viewModelScope.launch { + sendMessage(ComposableMessageBundle.SendTextMessageBundle(conversationId, pendingMessage, emptyList())) + } + } + fun trySendMessage(messageBundle: MessageBundle) { trySendMessages(listOf(messageBundle)) } diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt index 6140c874f3b..4c97c8cc57a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt @@ -36,8 +36,8 @@ import com.wire.android.mapper.toUIPreview import com.wire.android.model.ImageAsset import com.wire.android.model.SnackBarMessage import com.wire.android.model.UserAvatarData -import com.wire.android.ui.home.conversations.ConversationSnackbarMessages import com.wire.android.ui.common.textfield.textAsFlow +import com.wire.android.ui.home.conversations.ConversationSnackbarMessages import com.wire.android.ui.home.conversations.search.DEFAULT_SEARCH_QUERY_DEBOUNCE import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.conversationslist.model.BlockState @@ -379,4 +379,9 @@ data class ImportMediaAuthenticatedState( val shareableConversationListState: ShareableConversationListState = ShareableConversationListState(), val selectedConversationItem: List = emptyList(), val selfDeletingTimer: SelfDeletionTimer = SelfDeletionTimer.Enabled(null) -) +) { + @Stable + fun isImportingData() { + importedText?.isNotEmpty() == true || importedAssets.isNotEmpty() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index 537405a1fe3..41bd65d98bd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -33,10 +33,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState @@ -120,6 +123,7 @@ fun ImportMediaScreen( navigateBack = navigator.finish ) } + FeatureFlagState.FileSharingState.NoUser -> { ImportMediaLoggedOutContent( fileSharingRestrictedState = fileSharingRestrictedState, @@ -192,23 +196,26 @@ private fun ImportMediaAuthenticatedContent( searchQueryTextState = importMediaViewModel.searchQueryTextState, onConversationClicked = importMediaViewModel::onConversationClicked, checkRestrictionsAndSendImportedMedia = { - importMediaViewModel.importMediaState.selectedConversationItem.firstOrNull()?.let { conversationItem -> - checkAssetRestrictionsViewModel.checkRestrictions( - importedMediaList = importMediaViewModel.importMediaState.importedAssets, - onSuccess = { - navigator.navigate( - NavigationCommand( - ConversationScreenDestination( - ConversationNavArgs( - conversationId = conversationItem.conversationId, - pendingBundles = ArrayList(it) - ) + with(importMediaViewModel.importMediaState) { + selectedConversationItem.firstOrNull()?.let { conversationItem -> + checkAssetRestrictionsViewModel.checkRestrictions( + importedMediaList = importedAssets, + onSuccess = { + navigator.navigate( + NavigationCommand( + ConversationScreenDestination( + ConversationNavArgs( + conversationId = conversationItem.conversationId, + pendingBundles = ArrayList(it), + pendingTextBundle = importedText + ) + ), + BackStackMode.REMOVE_CURRENT_AND_REPLACE ), - BackStackMode.REMOVE_CURRENT_AND_REPLACE - ), - ) - } - ) + ) + } + ) + } } }, onNewSelfDeletionTimerPicked = importMediaViewModel::onNewSelfDeletionTimerPicked, @@ -222,10 +229,12 @@ private fun ImportMediaAuthenticatedContent( ) val context = LocalContext.current - LaunchedEffect(importMediaViewModel.importMediaState.importedAssets) { - if (importMediaViewModel.importMediaState.importedAssets.isEmpty()) { - context.getActivity() - ?.let { importMediaViewModel.handleReceivedDataFromSharingIntent(it) } + with(importMediaViewModel.importMediaState) { + LaunchedEffect(isImportingData()) { + if (importedAssets.isEmpty() || importedText.isNullOrEmpty()) { + context.getActivity() + ?.let { activity -> importMediaViewModel.handleReceivedDataFromSharingIntent(activity) } + } } } } @@ -479,46 +488,9 @@ private fun ImportMediaContent( ) } } else { - LazyRow( - modifier = Modifier - .fillMaxWidth() - .height(dimensions().spacing120x), - contentPadding = PaddingValues(start = dimensions().spacing8x, end = dimensions().spacing8x) - ) { - items( - count = importedItemsList.size, - ) { index -> - Box( - modifier = Modifier - .width(dimensions().spacing120x) - .fillMaxHeight() - ) { - val assetSize = dimensions().spacing120x - dimensions().spacing16x - AssetTilePreview( - modifier = Modifier - .width(assetSize) - .height(assetSize) - .align(Alignment.Center), - assetBundle = importedItemsList[index].assetBundle, - showOnlyExtension = false, - onClick = {} - ) - - if (importedItemsList.size > 1) { - RemoveIcon( - modifier = Modifier.align(Alignment.TopEnd), - onClick = { onRemoveAsset(index) }, - contentDescription = stringResource(id = R.string.remove_asset_description) - ) - } - if (importedItemsList[index].assetSizeExceeded != null) { - ErrorIcon( - stringResource(id = R.string.asset_attention_description), - modifier = Modifier.align(Alignment.Center) - ) - } - } - } + when (state.importedText.isNullOrBlank()) { + true -> ImportAssetsCarrousel(importedItemsList, onRemoveAsset) + false -> ImportText(state.importedText) } } HorizontalDivider( @@ -559,6 +531,73 @@ private fun ImportMediaContent( } } +@Composable +private fun ImportText(importedText: String) { + val scrollState = rememberScrollState() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentSize() + .height(dimensions().spacing120x) + .verticalScroll(scrollState) + .padding(vertical = dimensions().spacing8x, horizontal = dimensions().spacing16x), + ) { + Text( + text = importedText, + textAlign = TextAlign.Start, + style = MaterialTheme.wireTypography.body01, + ) + } +} + +@Composable +private fun ImportAssetsCarrousel( + importedItemsList: PersistentList, + onRemoveAsset: (index: Int) -> Unit +) { + LazyRow( + modifier = Modifier + .fillMaxWidth() + .height(dimensions().spacing120x), + contentPadding = PaddingValues(start = dimensions().spacing8x, end = dimensions().spacing8x) + ) { + items( + count = importedItemsList.size, + ) { index -> + Box( + modifier = Modifier + .width(dimensions().spacing120x) + .fillMaxHeight() + ) { + val assetSize = dimensions().spacing120x - dimensions().spacing16x + AssetTilePreview( + modifier = Modifier + .width(assetSize) + .height(assetSize) + .align(Alignment.Center), + assetBundle = importedItemsList[index].assetBundle, + showOnlyExtension = false, + onClick = {} + ) + + if (importedItemsList.size > 1) { + RemoveIcon( + modifier = Modifier.align(Alignment.TopEnd), + onClick = { onRemoveAsset(index) }, + contentDescription = stringResource(id = R.string.remove_asset_description) + ) + } + if (importedItemsList[index].assetSizeExceeded != null) { + ErrorIcon( + stringResource(id = R.string.asset_attention_description), + modifier = Modifier.align(Alignment.Center) + ) + } + } + } + } +} + @Composable private fun SnackBarMessage( infoMessages: SharedFlow, @@ -659,6 +698,27 @@ fun PreviewImportMediaScreenRegular() { } } +@PreviewMultipleThemes +@Composable +fun PreviewImportMediaTextScreenRegular() { + WireTheme { + ImportMediaRegularContent( + importMediaAuthenticatedState = ImportMediaAuthenticatedState( + importedAssets = persistentListOf(), + importedText = "This is a shared text message \n" + + "This is a second line with a veeeeeeeeeeeeeeeeeeeeeeeeeeery long shared text message" + ), + searchQueryTextState = rememberTextFieldState(), + onConversationClicked = {}, + checkRestrictionsAndSendImportedMedia = {}, + onNewSelfDeletionTimerPicked = {}, + infoMessage = MutableSharedFlow(), + onRemoveAsset = { _ -> }, + navigateBack = {} + ) + } +} + @PreviewMultipleThemes @Composable fun PreviewImportMediaBottomBar() { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt index 5f52a44519c..1d036e2e9e5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt @@ -24,6 +24,7 @@ import com.wire.android.config.mockUri import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.media.PingRinger import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.navArgs import com.wire.android.util.ImageUtil @@ -250,5 +251,19 @@ internal class SendMessageViewModelArrangement { coEvery { retryFailedMessageUseCase(any(), any()) } returns Either.Right(Unit) } + fun withPendingTextBundle(textToShare: String = "some text") = apply { + every { savedStateHandle.navArgs() } returns ConversationNavArgs( + conversationId = conversationId, + pendingTextBundle = textToShare + ) + } + + fun withPendingAssetBundle(vararg assetBundle: AssetBundle) = apply { + every { savedStateHandle.navArgs() } returns ConversationNavArgs( + conversationId = conversationId, + pendingBundles = arrayListOf(*assetBundle) + ) + } + fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelTest.kt index 90b62d05ceb..a962760a65b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelTest.kt @@ -649,6 +649,61 @@ class SendMessageViewModelTest { } } + @Test + fun `given text is being shared, when initializing the viewmodel, then message is sent to use the case`() = runTest { + val textToShare = "my nice text to share" + val (arrangement, _) = SendMessageViewModelArrangement() + .withSuccessfulViewModelInit() + .withPendingTextBundle(textToShare) + .withSuccessfulSendTextMessage() + .arrange() + + coVerify { arrangement.sendTextMessage(any(), eq(textToShare), any(), any()) } + } + + @Test + fun `given an asset is being shared, when initializing the viewmodel, then message is sent to use the case`() = runTest { + val assetBundles = arrayOf( + AssetBundle( + "key1", + "application/pdf", + "some-data-path1".toPath(), + 1L, + "mocked_file1.pdf", + AttachmentType.GENERIC_FILE + ), + AssetBundle( + "key2", + "application/pdf", + "some-data-path2".toPath(), + 1L, + "mocked_file2.pdf", + AttachmentType.GENERIC_FILE + ) + ) + val (arrangement, _) = SendMessageViewModelArrangement() + .withSuccessfulViewModelInit() + .withPendingAssetBundle(*assetBundles) + .withSendAttachmentMessageResult(ScheduleNewAssetMessageResult.Success("some-message-id1")) + .withSendAttachmentMessageResult(ScheduleNewAssetMessageResult.Success("some-message-id2")) + .arrange() + + assetBundles.forEach { bundle -> + coVerify { + arrangement.sendAssetMessage( + any(), + eq(bundle.dataPath), + eq(bundle.dataSize), + eq(bundle.fileName), + eq(bundle.mimeType), + any(), + any(), + any() + ) + } + } + } + companion object { val conversationId: ConversationId = ConversationId("some-dummy-value", "some.dummy.domain") }