From e26a74fe67632bcd1f2896d0f32cde85df207b52 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 21 May 2024 13:27:51 +0200 Subject: [PATCH 1/3] feat: image preview improvements --- .../com/wire/android/navigation/Extras.kt | 23 --- .../android/navigation/NavigationUtils.kt | 18 ++ .../home/conversations/AssetTooLargeDialog.kt | 34 ++-- .../home/conversations/ConversationNavArgs.kt | 6 +- .../home/conversations/ConversationScreen.kt | 21 +++ .../conversations/MessageComposerViewState.kt | 8 +- .../media/CheckAssetRestrictionsViewModel.kt | 52 ++++++ .../media/preview/ImagesPreviewNavArgs.kt | 5 + .../media/preview/ImagesPreviewScreen.kt | 172 +++++------------- .../media/preview/ImagesPreviewState.kt | 3 +- .../media/preview/ImagesPreviewViewModel.kt | 4 +- .../home/conversations/model/AssetBundle.kt | 22 +++ .../sendmessage/SendMessageViewModel.kt | 62 +++---- .../ImportMediaAuthenticatedViewModel.kt | 8 +- .../android/ui/sharing/ImportMediaScreen.kt | 107 ++++++----- .../com/wire/android/util/DeviceUtil.kt | 8 +- app/src/main/res/values/strings.xml | 6 +- .../SendMessageViewModelArrangement.kt | 12 +- .../sendmessage/SendMessageViewModelTest.kt | 4 +- .../wire/android/ui/common/error/ErrorIcon.kt | 47 +++++ .../android/ui/common/remove/RemoveIcon.kt | 54 ++++++ .../src/main/res/drawable/ic_attention.xml | 10 + .../src/main/res/drawable/ic_close.xml | 27 +++ 23 files changed, 447 insertions(+), 266 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/navigation/Extras.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/ui/common/error/ErrorIcon.kt create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/ui/common/remove/RemoveIcon.kt create mode 100644 core/ui-common/src/main/res/drawable/ic_attention.xml create mode 100644 core/ui-common/src/main/res/drawable/ic_close.xml diff --git a/app/src/main/kotlin/com/wire/android/navigation/Extras.kt b/app/src/main/kotlin/com/wire/android/navigation/Extras.kt deleted file mode 100644 index b13f530dd62..00000000000 --- a/app/src/main/kotlin/com/wire/android/navigation/Extras.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.android.navigation - -const val EXTRA_USER_ID = "extra_user_id" -const val EXTRA_USER_NAME = "extra_user_name" -const val EXTRA_MESSAGE_ID = "extra_message_id" diff --git a/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt b/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt index eb62eb12fe2..c0af17321c2 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt @@ -35,6 +35,8 @@ import com.wire.android.ui.NavGraphs import com.wire.android.ui.destinations.Destination import com.wire.android.util.CustomTabsHelper import com.wire.kalium.logger.obfuscateId +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json @SuppressLint("RestrictedApi") internal fun NavController.navigateToItem(command: NavigationCommand) { @@ -44,6 +46,7 @@ internal fun NavController.navigateToItem(command: NavigationCommand) { fun lastNestedGraph() = lastDestination()?.takeIf { it.navGraph() != navGraph }?.navGraph() fun firstDestinationWithRoute(route: String) = currentBackStack.value.firstOrNull { it.destination.route?.getBaseRoute() == route.getBaseRoute() } + fun lastDestinationFromOtherGraph(graph: NavGraphSpec) = currentBackStack.value.lastOrNull { it.navGraph() != graph } appLogger.d("[$TAG] -> command: ${command.destination.route.obfuscateId()}") @@ -115,4 +118,19 @@ fun Direction.handleNavigation(context: Context, handleOtherDirection: (Directio else -> handleOtherDirection(this) } +object ArgsSerializer { + @OptIn(ExperimentalSerializationApi::class) + private val instance: Json by lazy { + Json { + encodeDefaults = true + explicitNulls = false + // to enable the serialization of maps with complex keys + // e.g. Map + allowStructuredMapKeys = true + } + } + + operator fun invoke() = instance +} + private const val TAG = "NavigationUtils" diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/AssetTooLargeDialog.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/AssetTooLargeDialog.kt index 416326aab21..086a338489c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/AssetTooLargeDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/AssetTooLargeDialog.kt @@ -30,8 +30,7 @@ import com.wire.kalium.logic.data.asset.AttachmentType @Composable fun AssetTooLargeDialog(dialogState: AssetTooLargeDialogState, hideDialog: () -> Unit) { when (dialogState) { - is AssetTooLargeDialogState.SingleVisible, - is AssetTooLargeDialogState.MultipleVisible -> { + is AssetTooLargeDialogState.Visible -> { WireDialog( title = getTitle(dialogState), text = getLabel(dialogState), @@ -52,38 +51,43 @@ fun AssetTooLargeDialog(dialogState: AssetTooLargeDialogState, hideDialog: () -> @Composable private fun getTitle(dialogState: AssetTooLargeDialogState) = when (dialogState) { AssetTooLargeDialogState.Hidden -> "" - is AssetTooLargeDialogState.MultipleVisible -> stringResource(id = R.string.title_assets_could_not_be_sent) - is AssetTooLargeDialogState.SingleVisible -> when (dialogState.assetType) { - AttachmentType.IMAGE -> stringResource(R.string.title_image_could_not_be_sent) - AttachmentType.VIDEO -> stringResource(R.string.title_video_could_not_be_sent) - AttachmentType.AUDIO, // TODO - AttachmentType.GENERIC_FILE -> stringResource(R.string.title_file_could_not_be_sent) - } + is AssetTooLargeDialogState.Visible -> + if (dialogState.multipleAssets) { + stringResource(id = R.string.title_assets_could_not_be_sent) + } else { + when (dialogState.assetType) { + AttachmentType.IMAGE -> stringResource(R.string.title_image_could_not_be_sent) + AttachmentType.VIDEO -> stringResource(R.string.title_video_could_not_be_sent) + AttachmentType.AUDIO, // TODO + AttachmentType.GENERIC_FILE -> stringResource(R.string.title_file_could_not_be_sent) + } + } } @Composable private fun getLabel(dialogState: AssetTooLargeDialogState) = when (dialogState) { AssetTooLargeDialogState.Hidden -> "" - is AssetTooLargeDialogState.MultipleVisible -> stringResource(id = R.string.label_shared_asset_too_large, dialogState.maxLimitInMB) - is AssetTooLargeDialogState.SingleVisible -> when (dialogState.assetType) { + is AssetTooLargeDialogState.Visible -> when (dialogState.assetType) { AttachmentType.IMAGE -> stringResource(R.string.label_shared_image_too_large, dialogState.maxLimitInMB) AttachmentType.VIDEO -> stringResource(R.string.label_shared_video_too_large, dialogState.maxLimitInMB) AttachmentType.AUDIO, // TODO AttachmentType.GENERIC_FILE -> stringResource(R.string.label_shared_file_too_large, dialogState.maxLimitInMB) }.let { - if (dialogState.savedToDevice) it + "\n" + stringResource(R.string.label_file_saved_to_device) - else it + var label = it + if (dialogState.multipleAssets) label = label + "\n" + stringResource(R.string.label_shared_multiple_assets_error) + if (dialogState.savedToDevice) label = label + "\n" + stringResource(R.string.label_file_saved_to_device) + label } } @Preview @Composable fun PreviewAssetTooLargeDialog() { - AssetTooLargeDialog(AssetTooLargeDialogState.SingleVisible(AttachmentType.VIDEO, 100, true)) {} + AssetTooLargeDialog(AssetTooLargeDialogState.Visible(AttachmentType.VIDEO, 100, true)) {} } @Preview @Composable fun PreviewMultipleAssetTooLargeDialog() { - AssetTooLargeDialog(AssetTooLargeDialogState.MultipleVisible(20)) {} + AssetTooLargeDialog(AssetTooLargeDialogState.Visible(AttachmentType.VIDEO, 100, false, true)) {} } 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 2f3f7f65241..764c9fe3a83 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 @@ -17,9 +17,13 @@ */ package com.wire.android.ui.home.conversations +import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.serialization.Serializable +@Serializable data class ConversationNavArgs( val conversationId: ConversationId, - val searchedMessageId: String? = null + val searchedMessageId: String? = null, + val pendingBundles: ArrayList? = null ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index ca35c6c66ac..dd2aaaa9ca6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -82,6 +82,7 @@ import com.wire.android.appLogger import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.Clickable import com.wire.android.model.SnackBarMessage +import com.wire.android.navigation.ArgsSerializer import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -127,6 +128,7 @@ import com.wire.android.ui.home.conversations.edit.EditMessageMenuItems import com.wire.android.ui.home.conversations.info.ConversationDetailsData import com.wire.android.ui.home.conversations.info.ConversationInfoViewModel import com.wire.android.ui.home.conversations.info.ConversationInfoViewState +import com.wire.android.ui.home.conversations.media.preview.ImagesPreviewNavBackArgs import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModel import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState import com.wire.android.ui.home.conversations.messages.draft.MessageDraftViewModel @@ -141,6 +143,7 @@ import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel import com.wire.android.ui.home.gallery.MediaGalleryActionType import com.wire.android.ui.home.gallery.MediaGalleryNavBackArgs import com.wire.android.ui.home.messagecomposer.MessageComposer +import com.wire.android.ui.home.messagecomposer.model.ComposableMessageBundle import com.wire.android.ui.home.messagecomposer.model.MessageBundle import com.wire.android.ui.home.messagecomposer.model.MessageComposition import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder @@ -207,6 +210,7 @@ fun ConversationScreen( messageDraftViewModel: MessageDraftViewModel = hiltViewModel(), groupDetailsScreenResultRecipient: ResultRecipient, mediaGalleryScreenResultRecipient: ResultRecipient, + imagePreviewScreenResultRecipient: ResultRecipient, resultNavigator: ResultBackNavigator, ) { val coroutineScope = rememberCoroutineScope() @@ -615,6 +619,23 @@ fun ConversationScreen( } } } + + imagePreviewScreenResultRecipient.onNavResult { result -> + when (result) { + Canceled -> {} + is Value -> { + val pendingBundles = ArgsSerializer().decodeFromString(result.value).pendingBundles + sendMessageViewModel.trySendMessages( + pendingBundles.map { assetBundle -> + ComposableMessageBundle.AttachmentPickedBundle( + conversationId = conversationMessagesViewModel.conversationId, + assetBundle = assetBundle + ) + } + ) + } + } + } } private fun conversationScreenOnBackButtonClick( 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 2f7b04605e2..91c052fb95c 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 @@ -36,8 +36,12 @@ data class MessageComposerViewState( sealed class AssetTooLargeDialogState { data object Hidden : AssetTooLargeDialogState() - data class SingleVisible(val assetType: AttachmentType, val maxLimitInMB: Int, val savedToDevice: Boolean) : AssetTooLargeDialogState() - data class MultipleVisible(val maxLimitInMB: Int) : AssetTooLargeDialogState() + data class Visible( + val assetType: AttachmentType, + val maxLimitInMB: Int, + val savedToDevice: Boolean, + val multipleAssets: Boolean = false + ) : AssetTooLargeDialogState() } sealed class VisitLinkDialogState { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt new file mode 100644 index 00000000000..3937efcf2fd --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.media + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.wire.android.ui.home.conversations.AssetTooLargeDialogState +import com.wire.android.ui.home.conversations.model.AssetBundle +import com.wire.android.ui.sharing.ImportedMediaAsset +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class CheckAssetRestrictionsViewModel @Inject constructor() : ViewModel() { + + var assetTooLargeDialogState: AssetTooLargeDialogState by mutableStateOf( + AssetTooLargeDialogState.Hidden + ) + private set + + fun checkRestrictions(importedMediaList: List, onSuccess: (bundleList: List) -> Unit) { + importedMediaList.firstOrNull { it.assetSizeExceeded != null }?.let { + assetTooLargeDialogState = AssetTooLargeDialogState.Visible( + assetType = it.assetBundle.assetType, + maxLimitInMB = it.assetSizeExceeded!!, + savedToDevice = false, + multipleAssets = true + ) + }?: onSuccess(importedMediaList.map { it.assetBundle }) + } + + fun hideDialog() { + assetTooLargeDialogState = AssetTooLargeDialogState.Hidden + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewNavArgs.kt index 30bab97843b..c09555c9b4e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewNavArgs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewNavArgs.kt @@ -18,10 +18,15 @@ package com.wire.android.ui.home.conversations.media.preview import android.net.Uri +import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.serialization.Serializable data class ImagesPreviewNavArgs( val conversationId: ConversationId, val conversationName: String, val assetUriList: ArrayList ) + +@Serializable +data class ImagesPreviewNavBackArgs(val pendingBundles: List) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt index f2b4da7e660..3608995ec76 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -36,9 +35,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -46,45 +42,35 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R -import com.wire.android.model.SnackBarMessage -import com.wire.android.navigation.BackStackMode -import com.wire.android.navigation.NavigationCommand +import com.wire.android.navigation.ArgsSerializer import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.colorsScheme -import com.wire.android.ui.common.dialogs.SureAboutMessagingInDegradedConversationDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.divider.WireDivider +import com.wire.android.ui.common.error.ErrorIcon import com.wire.android.ui.common.image.WireImage +import com.wire.android.ui.common.progress.WireCircularProgressIndicator +import com.wire.android.ui.common.remove.RemoveIcon import com.wire.android.ui.common.scaffold.WireScaffold -import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.spacers.HorizontalSpace import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar -import com.wire.android.ui.destinations.ConversationScreenDestination -import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.home.conversations.AssetTooLargeDialog -import com.wire.android.ui.home.conversations.SureAboutMessagingDialogState +import com.wire.android.ui.home.conversations.media.CheckAssetRestrictionsViewModel import com.wire.android.ui.home.conversations.model.AssetBundle -import com.wire.android.ui.home.conversations.sendmessage.SendMessageAction -import com.wire.android.ui.home.conversations.sendmessage.SendMessageState -import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel -import com.wire.android.ui.home.messagecomposer.model.ComposableMessageBundle -import com.wire.android.ui.home.messagecomposer.model.MessageBundle -import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectMessageDialog import com.wire.android.ui.sharing.ImportedMediaAsset import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme @@ -92,7 +78,6 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.id.ConversationId import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import okio.Path.Companion.toPath @@ -105,66 +90,42 @@ import okio.Path.Companion.toPath fun ImagesPreviewScreen( navigator: Navigator, imagesPreviewViewModel: ImagesPreviewViewModel = hiltViewModel(), - sendMessageViewModel: SendMessageViewModel = hiltViewModel() -) { - LaunchedEffect(sendMessageViewModel.viewState.afterMessageSendAction) { - when (val action = sendMessageViewModel.viewState.afterMessageSendAction) { - SendMessageAction.NavigateBack -> navigator.navigateBack() - is SendMessageAction.NavigateToConversation -> navigator.navigate( - NavigationCommand( - ConversationScreenDestination(action.conversationId), - BackStackMode.REMOVE_CURRENT - ) - ) - - SendMessageAction.NavigateToHome -> navigator.navigate( - NavigationCommand( - HomeScreenDestination(), - BackStackMode.REMOVE_CURRENT - ) - ) - - SendMessageAction.None -> {} - } - } + checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel(), + resultNavigator: ResultBackNavigator +) { Content( previewState = imagesPreviewViewModel.viewState, - sendState = sendMessageViewModel.viewState, onNavigationPressed = { navigator.navigateBack() }, - onSendMessages = sendMessageViewModel::trySendMessages, + onSendMessages = { mediaAssets -> + checkAssetRestrictionsViewModel.checkRestrictions( + importedMediaList = mediaAssets, + onSuccess = { + val result = ArgsSerializer().encodeToString( + serializer = ImagesPreviewNavBackArgs.serializer(), + value = ImagesPreviewNavBackArgs(pendingBundles = ArrayList(it)) + ) + resultNavigator.setResult(result) + resultNavigator.navigateBack() + } + ) + }, onSelected = imagesPreviewViewModel::onSelected, onRemoveAsset = imagesPreviewViewModel::onRemove ) AssetTooLargeDialog( - dialogState = sendMessageViewModel.assetTooLargeDialogState, - hideDialog = sendMessageViewModel::hideAssetTooLargeError - ) - - SureAboutMessagingInDegradedConversationDialog( - dialogState = sendMessageViewModel.sureAboutMessagingDialogState, - sendAnyway = sendMessageViewModel::acceptSureAboutSendingMessage, - hideDialog = sendMessageViewModel::dismissSureAboutSendingMessage + dialogState = checkAssetRestrictionsViewModel.assetTooLargeDialogState, + hideDialog = checkAssetRestrictionsViewModel::hideDialog ) - - (sendMessageViewModel.sureAboutMessagingDialogState as? SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold)?.let { - LegalHoldSubjectMessageDialog( - dialogDismissed = sendMessageViewModel::dismissSureAboutSendingMessage, - sendAnywayClicked = sendMessageViewModel::acceptSureAboutSendingMessage, - ) - } - - SnackBarMessage(sendMessageViewModel.infoMessage) } @OptIn(ExperimentalFoundationApi::class) @Composable private fun Content( previewState: ImagesPreviewState, - sendState: SendMessageState, onNavigationPressed: () -> Unit = {}, - onSendMessages: (List) -> Unit, + onSendMessages: (List) -> Unit, onSelected: (index: Int) -> Unit, onRemoveAsset: (index: Int) -> Unit ) { @@ -214,7 +175,6 @@ private fun Content( ) HorizontalSpace.x16() WirePrimaryButton( - loading = sendState.inProgress, modifier = Modifier.weight(1F), text = stringResource(id = R.string.import_media_send_button_title), leadingIcon = { @@ -226,15 +186,7 @@ private fun Content( ) }, onClick = { - onSendMessages( - previewState.assetBundleList.map { - ComposableMessageBundle.AttachmentPickedBundle( - previewState.conversationId, - it.assetBundle - ) - } - - ) + onSendMessages(previewState.assetBundleList) } ) HorizontalSpace.x16() @@ -276,6 +228,15 @@ private fun Content( } } + if (previewState.isLoading) { + WireCircularProgressIndicator( + progressColor = MaterialTheme.wireColorScheme.onBackground, modifier = Modifier.align( + Alignment.Center + ), + size = dimensions().spacing24x + ) + } + LazyRow( modifier = Modifier .padding(bottom = dimensions().spacing8x) @@ -303,12 +264,18 @@ private fun Content( ) if (previewState.assetBundleList.size > 1) { - RemoveAssetButton(modifier = Modifier.align(Alignment.TopEnd), onClick = { - onRemoveAsset(index) - }) + RemoveIcon( + modifier = Modifier.align(Alignment.TopEnd), onClick = { + onRemoveAsset(index) + }, + contentDescription = stringResource(id = R.string.remove_asset_description) + ) } if (previewState.assetBundleList[index].assetSizeExceeded != null) { - ErrorIcon(modifier = Modifier.align(Alignment.Center)) + ErrorIcon( + modifier = Modifier.align(Alignment.Center), + stringResource(id = R.string.asset_attention_description) + ) } } } @@ -317,54 +284,6 @@ private fun Content( } } -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun RemoveAssetButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, -) { - Icon( - modifier = modifier - .size(dimensions().spacing24x) - .combinedClickable(onClick = onClick) - .clip(shape = CircleShape) - .background(color = MaterialTheme.wireColorScheme.inverseSurface) - .padding(dimensions().spacing6x), - painter = painterResource(id = R.drawable.ic_close), - contentDescription = stringResource(id = R.string.asset_preview_remove_asset_description), - tint = MaterialTheme.wireColorScheme.inverseOnSurface - ) -} - -@Composable -private fun ErrorIcon( - modifier: Modifier = Modifier, -) { - Icon( - modifier = modifier - .clip(shape = RoundedCornerShape(dimensions().spacing4x)) - .background(color = MaterialTheme.wireColorScheme.error) - .padding(dimensions().spacing6x), - painter = painterResource(id = R.drawable.ic_attention), - contentDescription = stringResource(id = R.string.asset_preview_asset_attention_description), - tint = MaterialTheme.wireColorScheme.onError - ) -} - -@Composable -private fun SnackBarMessage(infoMessages: SharedFlow) { - val context = LocalContext.current - val snackbarHostState = LocalSnackbarHostState.current - - LaunchedEffect(Unit) { - infoMessages.collect { - snackbarHostState.showSnackbar( - message = it.uiText.asString(context.resources) - ) - } - } -} - @PreviewMultipleThemes @Composable fun PreviewImagesPreviewScreen() { @@ -421,7 +340,6 @@ fun PreviewImagesPreviewScreen() { ) ), ), - sendState = SendMessageState(inProgress = true), onNavigationPressed = {}, onSendMessages = {}, onSelected = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewState.kt index b6c777cf64c..161e12f11bb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewState.kt @@ -26,5 +26,6 @@ data class ImagesPreviewState( val conversationId: ConversationId, val conversationName: String, val assetBundleList: PersistentList = persistentListOf(), - val selectedIndex: Int = 0 + val selectedIndex: Int = 0, + val isLoading: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt index 337101f6f6f..d432206bf3e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt @@ -63,10 +63,12 @@ class ImagesPreviewViewModel @Inject constructor( } private fun handleAssets() { + viewState = viewState.copy(isLoading = true) viewModelScope.launch { val assets = navArgs.assetUriList.map { handleImportedAsset(it) } viewState = viewState.copy( - assetBundleList = assets.filterNotNull().toPersistentList() + assetBundleList = assets.filterNotNull().toPersistentList(), + isLoading = false ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/AssetBundle.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/AssetBundle.kt index 8f667381946..b6ee61b96d7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/AssetBundle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/AssetBundle.kt @@ -21,15 +21,25 @@ package com.wire.android.ui.home.conversations.model import android.net.Uri import androidx.compose.runtime.Stable import com.wire.kalium.logic.data.asset.AttachmentType +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import okio.Path +import okio.Path.Companion.toPath import kotlin.math.roundToInt /** * Represents a set of metadata information of an asset message */ +@Serializable data class AssetBundle( val key: String, val mimeType: String, + @Serializable(with = PathAsStringSerializer::class) val dataPath: Path, val dataSize: Long, val fileName: String, @@ -61,3 +71,15 @@ data class UriAsset( val uri: Uri, val saveToDeviceIfInvalid: Boolean = false ) + +private object PathAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Path) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): Path { + return decoder.decodeString().toPath() + } +} 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 41223c0df6a..6343eeeb9c7 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 @@ -21,13 +21,15 @@ package com.wire.android.ui.home.conversations.sendmessage import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger import com.wire.android.media.PingRinger import com.wire.android.model.SnackBarMessage +import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.home.conversations.AssetTooLargeDialogState +import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.ConversationSnackbarMessages import com.wire.android.ui.home.conversations.SureAboutMessagingDialogState import com.wire.android.ui.home.conversations.model.AssetBundle @@ -36,6 +38,7 @@ import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.messagecomposer.model.ComposableMessageBundle import com.wire.android.ui.home.messagecomposer.model.MessageBundle import com.wire.android.ui.home.messagecomposer.model.Ping +import com.wire.android.ui.navArgs import com.wire.android.ui.sharing.SendMessagesSnackbarMessages import com.wire.android.util.ImageUtil import com.wire.android.util.dispatchers.DispatcherProvider @@ -45,6 +48,7 @@ import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.failure.LegalHoldEnabledForConversationFailure import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageResult import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase @@ -60,7 +64,6 @@ import com.wire.kalium.logic.feature.message.SendLocationUseCase import com.wire.kalium.logic.feature.message.SendTextMessageUseCase import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.isRight import com.wire.kalium.logic.functional.onFailure import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -77,6 +80,7 @@ import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") @HiltViewModel class SendMessageViewModel @Inject constructor( + override val savedStateHandle: SavedStateHandle, private val sendAssetMessage: ScheduleNewAssetMessageUseCase, private val sendTextMessage: SendTextMessageUseCase, private val sendEditTextMessage: SendEditTextMessageUseCase, @@ -94,7 +98,10 @@ class SendMessageViewModel @Inject constructor( private val observeConversationUnderLegalHoldNotified: ObserveConversationUnderLegalHoldNotifiedUseCase, private val sendLocation: SendLocationUseCase, private val removeMessageDraft: RemoveMessageDraftUseCase, -) : ViewModel() { +) : SavedStateViewModel(savedStateHandle) { + + private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() + val conversationId: QualifiedID = conversationNavArgs.conversationId private val _infoMessage = MutableSharedFlow() val infoMessage = _infoMessage.asSharedFlow() @@ -107,7 +114,18 @@ class SendMessageViewModel @Inject constructor( SureAboutMessagingDialogState.Hidden ) - var viewState: SendMessageState by mutableStateOf(SendMessageState()) + init { + conversationNavArgs.pendingBundles?.let { assetBundles -> + trySendMessages( + assetBundles.map { assetBundle -> + ComposableMessageBundle.AttachmentPickedBundle( + conversationId, + assetBundle + ) + } + ) + } + } private fun onSnackbarMessage(type: SnackBarMessage) = viewModelScope.launch { _infoMessage.emit(type) @@ -151,7 +169,6 @@ class SendMessageViewModel @Inject constructor( private suspend fun sendMessages(messageBundleList: List) { val jobs: MutableCollection = mutableListOf() - beforeSendingMessage() messageBundleList.forEach { val job = viewModelScope.launch { sendMessage(it) @@ -159,20 +176,12 @@ class SendMessageViewModel @Inject constructor( jobs.add(job) } jobs.joinAll() - withContext(dispatchers.main()) { - val action = messageBundleList.firstOrNull()?.let { - SendMessageAction.NavigateToConversation(it.conversationId) - } ?: SendMessageAction.NavigateToHome - - viewState = viewState.copy(inProgress = false, afterMessageSendAction = action) - } } @Suppress("LongMethod") private suspend fun sendMessage(messageBundle: MessageBundle) { when (messageBundle) { is ComposableMessageBundle.EditMessageBundle -> { - beforeSendingMessage() removeMessageDraft(messageBundle.conversationId) sendTypingEvent(messageBundle.conversationId, TypingIndicatorMode.STOPPED) with(messageBundle) { @@ -183,7 +192,6 @@ class SendMessageViewModel @Inject constructor( mentions = newMentions.map { it.intoMessageMention() }, ) .handleLegalHoldFailureAfterSendingMessage(conversationId) - .handleAfterMessageResult() } } @@ -207,7 +215,6 @@ class SendMessageViewModel @Inject constructor( } is ComposableMessageBundle.SendTextMessageBundle -> { - beforeSendingMessage() removeMessageDraft(messageBundle.conversationId) sendTypingEvent(messageBundle.conversationId, TypingIndicatorMode.STOPPED) with(messageBundle) { @@ -218,25 +225,20 @@ class SendMessageViewModel @Inject constructor( quotedMessageId = quotedMessageId ) .handleLegalHoldFailureAfterSendingMessage(conversationId) - .handleAfterMessageResult() } } is ComposableMessageBundle.LocationBundle -> { - beforeSendingMessage() with(messageBundle) { sendLocation(conversationId, location.latitude.toFloat(), location.longitude.toFloat(), locationName, zoom) .handleLegalHoldFailureAfterSendingMessage(conversationId) - .handleAfterMessageResult() } } is Ping -> { - beforeSendingMessage() pingRinger.ping(R.raw.ping_from_me, isReceivingPing = false) sendKnockUseCase(conversationId = messageBundle.conversationId, hotKnock = false) .handleLegalHoldFailureAfterSendingMessage(messageBundle.conversationId) - .handleAfterMessageResult() } } } @@ -252,7 +254,7 @@ class SendMessageViewModel @Inject constructor( audioPath = audioPath )) { is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> { - assetTooLargeDialogState = AssetTooLargeDialogState.SingleVisible( + assetTooLargeDialogState = AssetTooLargeDialogState.Visible( assetType = result.assetBundle.assetType, maxLimitInMB = result.maxLimitInMB, savedToDevice = attachmentUri.saveToDeviceIfInvalid @@ -270,7 +272,6 @@ class SendMessageViewModel @Inject constructor( } internal fun sendAttachment(attachmentBundle: AssetBundle?, conversationId: ConversationId) { - beforeSendingMessage() viewModelScope.launch { withContext(dispatchers.io()) { attachmentBundle?.run { @@ -291,7 +292,6 @@ class SendMessageViewModel @Inject constructor( audioLengthInMs = 0L ) .handleLegalHoldFailureAfterSendingMessage(conversationId) - .handleAfterMessageResult() } AttachmentType.VIDEO, @@ -312,7 +312,6 @@ class SendMessageViewModel @Inject constructor( ) ) .handleLegalHoldFailureAfterSendingMessage(conversationId) - .handleAfterMessageResult() } catch (e: OutOfMemoryError) { appLogger.e("There was an OutOfMemory error while uploading the asset") onSnackbarMessage(ConversationSnackbarMessages.ErrorSendingAsset) @@ -405,21 +404,6 @@ class SendMessageViewModel @Inject constructor( sureAboutMessagingDialogState = SureAboutMessagingDialogState.Hidden } - private fun beforeSendingMessage() { - viewState = viewState.copy(inProgress = true) - } - - private fun Either.handleAfterMessageResult() { - viewState = viewState.copy( - afterMessageSendAction = if (this.isRight()) { - SendMessageAction.None // TODO KBX pass action - } else { - SendMessageAction.None - }, - inProgress = false - ) - } - private companion object { const val MAX_LIMIT_MESSAGE_SEND = 20 } 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 51dfec7955f..a5480068b98 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 @@ -106,6 +106,10 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( } } + fun onRemove(index: Int) { + importMediaState = importMediaState.copy(importedAssets = importMediaState.importedAssets.removeAt(index)) + } + private fun loadUserAvatar() = viewModelScope.launch(dispatchers.io()) { getSelf().collect { selfUser -> withContext(dispatchers.main()) { @@ -355,10 +359,6 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( fun onSnackbarMessage(type: SnackBarMessage) = viewModelScope.launch { _infoMessage.emit(type) } - - private companion object { - const val MAX_LIMIT_MEDIA_IMPORT = 20 - } } @Stable 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 cee097f6cd9..f1fc9145d5a 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 @@ -63,25 +63,26 @@ import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.error.ErrorIcon import com.wire.android.ui.common.progress.WireCircularProgressIndicator +import com.wire.android.ui.common.remove.RemoveIcon import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.destinations.ConversationScreenDestination -import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.home.FeatureFlagState +import com.wire.android.ui.home.conversations.AssetTooLargeDialog +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.media.CheckAssetRestrictionsViewModel import com.wire.android.ui.home.conversations.media.preview.AssetTilePreview import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMenuItems -import com.wire.android.ui.home.conversations.sendmessage.SendMessageAction -import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel import com.wire.android.ui.home.conversationslist.common.ConversationList import com.wire.android.ui.home.conversationslist.model.ConversationFolder import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration -import com.wire.android.ui.home.messagecomposer.model.ComposableMessageBundle import com.wire.android.ui.home.newconversation.common.SendContentButton import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel import com.wire.android.ui.theme.WireTheme @@ -127,46 +128,42 @@ fun ImportMediaScreen( FeatureFlagState.SharingRestrictedState.NONE -> { val importMediaViewModel: ImportMediaAuthenticatedViewModel = hiltViewModel() - val sendMessageViewModel: SendMessageViewModel = hiltViewModel() - - LaunchedEffect(sendMessageViewModel.viewState.afterMessageSendAction) { - when (val action = sendMessageViewModel.viewState.afterMessageSendAction) { - SendMessageAction.NavigateBack -> navigator.navigateBack() - is SendMessageAction.NavigateToConversation -> navigator.navigate( - NavigationCommand( - ConversationScreenDestination(action.conversationId), - BackStackMode.REMOVE_CURRENT - ) - ) - - SendMessageAction.NavigateToHome -> navigator.navigate( - NavigationCommand( - HomeScreenDestination(), - BackStackMode.REMOVE_CURRENT - ) - ) - - SendMessageAction.None -> { - } - } - } + val checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel() ImportMediaRegularContent( importMediaAuthenticatedState = importMediaViewModel.importMediaState, onSearchQueryChanged = importMediaViewModel::onSearchQueryChanged, onConversationClicked = importMediaViewModel::onConversationClicked, checkRestrictionsAndSendImportedMedia = { - sendMessageViewModel.trySendMessages(importMediaViewModel.importMediaState.importedAssets.map { - ComposableMessageBundle.AttachmentPickedBundle( - importMediaViewModel.importMediaState.selectedConversationItem.first().conversationId, - it.assetBundle + importMediaViewModel.importMediaState.selectedConversationItem.firstOrNull()?.let { conversationItem -> + checkAssetRestrictionsViewModel.checkRestrictions( + importedMediaList = importMediaViewModel.importMediaState.importedAssets, + onSuccess = { + navigator.navigate( + NavigationCommand( + ConversationScreenDestination( + ConversationNavArgs( + conversationId = conversationItem.conversationId, + pendingBundles = ArrayList(it) + ) + ), + BackStackMode.UPDATE_EXISTED + ), + ) + } ) - }) + } }, onNewSelfDeletionTimerPicked = importMediaViewModel::onNewSelfDeletionTimerPicked, infoMessage = importMediaViewModel.infoMessage, navigateBack = navigator.finish, + onRemoveAsset = importMediaViewModel::onRemove ) + AssetTooLargeDialog( + dialogState = checkAssetRestrictionsViewModel.assetTooLargeDialogState, + hideDialog = checkAssetRestrictionsViewModel::hideDialog + ) + val context = LocalContext.current LaunchedEffect(importMediaViewModel.importMediaState.importedAssets) { if (importMediaViewModel.importMediaState.importedAssets.isEmpty()) { @@ -227,6 +224,7 @@ fun ImportMediaRegularContent( onNewSelfDeletionTimerPicked: (selfDeletionDuration: SelfDeletionDuration) -> Unit, infoMessage: SharedFlow, navigateBack: () -> Unit, + onRemoveAsset: (index: Int) -> Unit, ) { val importMediaScreenState = rememberImportMediaScreenState() @@ -254,7 +252,8 @@ fun ImportMediaRegularContent( internalPadding = internalPadding, onSearchQueryChanged = onSearchQueryChanged, onConversationClicked = onConversationClicked, - searchBarState = importMediaScreenState.searchBarState + searchBarState = importMediaScreenState.searchBarState, + onRemoveAsset = onRemoveAsset ) }, bottomBar = { @@ -385,6 +384,7 @@ private fun ImportMediaContent( internalPadding: PaddingValues, onSearchQueryChanged: (searchQuery: TextFieldValue) -> Unit, onConversationClicked: (conversationId: ConversationId) -> Unit, + onRemoveAsset: (index: Int) -> Unit, searchBarState: SearchBarState ) { val importedItemsList: PersistentList = state.importedAssets @@ -429,20 +429,42 @@ private fun ImportMediaContent( modifier = Modifier .fillMaxWidth() .height(dimensions().spacing120x), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), - contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) + contentPadding = PaddingValues(start = dimensions().spacing8x, end = dimensions().spacing8x) ) { items( count = importedItemsList.size, ) { index -> - AssetTilePreview( + Box( modifier = Modifier .width(dimensions().spacing120x) - .fillMaxHeight(), - assetBundle = importedItemsList[index].assetBundle, - showOnlyExtension = false, - onClick = {} - ) + .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( + modifier = Modifier.align(Alignment.Center), + stringResource(id = R.string.asset_attention_description) + ) + } + } } } } @@ -577,7 +599,8 @@ fun PreviewImportMediaScreenRegular() { {}, {}, {}, - MutableSharedFlow() + MutableSharedFlow(), + {} ) {} } } diff --git a/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt index c1ad2791e24..ac8a2a99cfe 100644 --- a/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt @@ -21,7 +21,7 @@ import android.os.Environment import android.os.StatFs object DeviceUtil { - private const val BYTES_IN_KILOBYTE = 1024 + private const val BYTES_IN_KILOBYTE = 1024L private const val BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024 private const val BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024 @@ -48,9 +48,9 @@ object DeviceUtil { fun formatSize(sizeInBytes: Long): String { return when { sizeInBytes < BYTES_IN_KILOBYTE -> "$sizeInBytes B" - sizeInBytes < BYTES_IN_MEGABYTE -> String.format("%.2f KB", sizeInBytes / BYTES_IN_KILOBYTE) - sizeInBytes < BYTES_IN_GIGABYTE -> String.format("%.2f MB", sizeInBytes / BYTES_IN_MEGABYTE) - else -> String.format("%.2f GB", sizeInBytes / BYTES_IN_GIGABYTE) + sizeInBytes < BYTES_IN_MEGABYTE -> String.format("%.2f KB", sizeInBytes.toDouble() / BYTES_IN_KILOBYTE) + sizeInBytes < BYTES_IN_GIGABYTE -> String.format("%.2f MB", sizeInBytes.toDouble() / BYTES_IN_MEGABYTE) + else -> String.format("%.2f GB", sizeInBytes.toDouble() / BYTES_IN_GIGABYTE) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4a76be99e0a..8f82e97a5a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1230,7 +1230,7 @@ You can only share a video up to %d MB. You can only share an image up to %d MB. You can only share a file up to %d MB. - You can only share a single asset up to %d MB. \n\nRemove all marked assets and try again + Remove all marked assets and try again The file was saved to your device. Self-deleting message • %1$s @@ -1435,7 +1435,7 @@ Location could not be shared - Remove asset - Asset attention + Remove asset + Asset attention 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 87f0bb281a0..0c4cd6c357e 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 @@ -23,9 +23,12 @@ import com.wire.android.config.TestDispatcherProvider 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.usecase.HandleUriAssetUseCase +import com.wire.android.ui.navArgs import com.wire.android.util.ImageUtil import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageResult import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase @@ -55,11 +58,15 @@ import okio.buffer internal class SendMessageViewModelArrangement { + val conversationId = ConversationId("some-dummy-value", "some.dummy.domain") + init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - + every { savedStateHandle.navArgs() } returns ConversationNavArgs( + conversationId = conversationId + ) // Default empty values coEvery { observeOngoingCallsUseCase() } returns flowOf(listOf()) coEvery { observeEstablishedCallsUseCase() } returns flowOf(listOf()) @@ -149,7 +156,8 @@ internal class SendMessageViewModelArrangement { setNotifiedAboutConversationUnderLegalHold = setNotifiedAboutConversationUnderLegalHold, observeConversationUnderLegalHoldNotified = observeConversationUnderLegalHoldNotified, sendLocation = sendLocation, - removeMessageDraft = removeMessageDraftUseCase + removeMessageDraft = removeMessageDraftUseCase, + savedStateHandle = savedStateHandle ) } 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 0826316a526..b3694b310f5 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 @@ -190,7 +190,7 @@ class SendMessageViewModelTest { any() ) } - assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.SingleVisible) + assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.Visible) } @Test @@ -232,7 +232,7 @@ class SendMessageViewModelTest { any() ) } - assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.SingleVisible) + assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.Visible) } @Test diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/error/ErrorIcon.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/error/ErrorIcon.kt new file mode 100644 index 00000000000..0cd40cf4dd9 --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/error/ErrorIcon.kt @@ -0,0 +1,47 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.error + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import com.wire.android.ui.common.R +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.wireColorScheme + +@Composable +fun ErrorIcon( + modifier: Modifier = Modifier, + contentDescription: String +) { + Icon( + modifier = modifier + .clip(shape = RoundedCornerShape(dimensions().spacing4x)) + .background(color = MaterialTheme.wireColorScheme.error) + .padding(dimensions().spacing6x), + painter = painterResource(id = R.drawable.ic_attention), + contentDescription = contentDescription, + tint = MaterialTheme.wireColorScheme.onError + ) +} diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/remove/RemoveIcon.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/remove/RemoveIcon.kt new file mode 100644 index 00000000000..5d9259ba7f9 --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/remove/RemoveIcon.kt @@ -0,0 +1,54 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.remove + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import com.wire.android.ui.common.R +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.wireColorScheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RemoveIcon( + contentDescription: String, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Icon( + modifier = modifier + .size(dimensions().spacing24x) + .combinedClickable(onClick = onClick) + .clip(shape = CircleShape) + .background(color = MaterialTheme.wireColorScheme.inverseSurface) + .padding(dimensions().spacing6x), + painter = painterResource(id = R.drawable.ic_close), + contentDescription = contentDescription, + tint = MaterialTheme.wireColorScheme.inverseOnSurface + ) +} diff --git a/core/ui-common/src/main/res/drawable/ic_attention.xml b/core/ui-common/src/main/res/drawable/ic_attention.xml new file mode 100644 index 00000000000..7fb97ada215 --- /dev/null +++ b/core/ui-common/src/main/res/drawable/ic_attention.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui-common/src/main/res/drawable/ic_close.xml b/core/ui-common/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000000..7c4f2a4289e --- /dev/null +++ b/core/ui-common/src/main/res/drawable/ic_close.xml @@ -0,0 +1,27 @@ + + + + + From b81ddaeec07c0a582540680021adc425c2fb93ec Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 21 May 2024 14:51:47 +0200 Subject: [PATCH 2/3] detekt fix --- .../conversations/media/CheckAssetRestrictionsViewModel.kt | 2 +- .../home/conversations/media/preview/ImagesPreviewScreen.kt | 5 ++--- .../kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt index 3937efcf2fd..a8445d77da1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt @@ -43,7 +43,7 @@ class CheckAssetRestrictionsViewModel @Inject constructor() : ViewModel() { savedToDevice = false, multipleAssets = true ) - }?: onSuccess(importedMediaList.map { it.assetBundle }) + } ?: onSuccess(importedMediaList.map { it.assetBundle }) } fun hideDialog() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt index 3608995ec76..0a055fb03e7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt @@ -230,9 +230,8 @@ private fun Content( if (previewState.isLoading) { WireCircularProgressIndicator( - progressColor = MaterialTheme.wireColorScheme.onBackground, modifier = Modifier.align( - Alignment.Center - ), + progressColor = MaterialTheme.wireColorScheme.onBackground, + modifier = Modifier.align(Alignment.Center), size = dimensions().spacing24x ) } 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 f1fc9145d5a..d4fc6d6789c 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 @@ -452,9 +452,8 @@ private fun ImportMediaContent( if (importedItemsList.size > 1) { RemoveIcon( - modifier = Modifier.align(Alignment.TopEnd), onClick = { - onRemoveAsset(index) - }, + modifier = Modifier.align(Alignment.TopEnd), + onClick = { onRemoveAsset(index) }, contentDescription = stringResource(id = R.string.remove_asset_description) ) } From 9860c0e2f10f661fdbe86a8cee1c9a82a816418a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 21 May 2024 16:51:15 +0200 Subject: [PATCH 3/3] detekt fix --- .../ui/home/conversations/media/preview/ImagesPreviewScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt index 0a055fb03e7..6e1575016a9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt @@ -264,7 +264,8 @@ private fun Content( if (previewState.assetBundleList.size > 1) { RemoveIcon( - modifier = Modifier.align(Alignment.TopEnd), onClick = { + modifier = Modifier.align(Alignment.TopEnd), + onClick = { onRemoveAsset(index) }, contentDescription = stringResource(id = R.string.remove_asset_description)