From ae90a884a2212516549e55626a9d9108086d813f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Wed, 17 Apr 2024 13:11:45 +0200 Subject: [PATCH 01/12] feat: multiple image preview --- .../conversations/MessageComposerViewState.kt | 14 +++-- .../sendmessage/SendMessageViewModel.kt | 59 +++++++++++++------ 2 files changed, 51 insertions(+), 22 deletions(-) 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 6de10ef75c8..b1c2c4be36d 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 @@ -52,15 +52,19 @@ sealed class InvalidLinkDialogState { sealed class SureAboutMessagingDialogState { data object Hidden : SureAboutMessagingDialogState() sealed class Visible(open val conversationId: ConversationId) : SureAboutMessagingDialogState() { - data class ConversationVerificationDegraded(val messageBundleToSend: MessageBundle) : Visible(messageBundleToSend.conversationId) + data class ConversationVerificationDegraded( + override val conversationId: ConversationId, val messageBundleListToSend: List + ) : Visible(conversationId) sealed class ConversationUnderLegalHold(override val conversationId: ConversationId) : Visible(conversationId) { data class BeforeSending( - val messageBundleToSend: MessageBundle - ) : ConversationUnderLegalHold(messageBundleToSend.conversationId) + override val conversationId: ConversationId, + val messageBundleListToSend: List + ) : ConversationUnderLegalHold(conversationId) - data class AfterSending(val messageId: MessageId, override val conversationId: ConversationId) : - ConversationUnderLegalHold(conversationId) + data class AfterSending( + val messageIdList: List, override val conversationId: ConversationId + ) : ConversationUnderLegalHold(conversationId) } } } 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 fc1304616aa..b4c376a59ca 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 @@ -114,20 +114,35 @@ class SendMessageViewModel @Inject constructor( observeConversationUnderLegalHoldNotified(conversationId).first().let { !it } fun trySendMessage(messageBundle: MessageBundle) { - viewModelScope.launch { - when { - shouldInformAboutDegradedBeforeSendingMessage(messageBundle.conversationId) -> - sureAboutMessagingDialogState = SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded(messageBundle) + trySendMessages(listOf(messageBundle)) + } + + fun trySendMessages(messageBundleList: List) { + val messageBundleMap = messageBundleList.groupBy { it.conversationId } + + messageBundleMap.forEach { (conversationId, bundles) -> + viewModelScope.launch { + when { + shouldInformAboutDegradedBeforeSendingMessage(conversationId) -> + sureAboutMessagingDialogState = + SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded(conversationId, bundles) - shouldInformAboutUnderLegalHoldBeforeSendingMessage(messageBundle.conversationId) -> - sureAboutMessagingDialogState = - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending(messageBundle) + shouldInformAboutUnderLegalHoldBeforeSendingMessage(conversationId) -> + sureAboutMessagingDialogState = + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending(conversationId, messageBundleList) - else -> sendMessage(messageBundle) + else -> sendMessages(messageBundleList) + } } } } + private suspend fun sendMessages(messageBundleList: List) { + messageBundleList.forEach { + sendMessage(it) + } + } + private suspend fun sendMessage(messageBundle: MessageBundle) { when (messageBundle) { is ComposableMessageBundle.EditMessageBundle -> { @@ -265,8 +280,16 @@ class SendMessageViewModel @Inject constructor( private fun CoreFailure.handleLegalHoldFailureAfterSendingMessage(conversationId: ConversationId) = also { if (this is LegalHoldEnabledForConversationFailure) { - sureAboutMessagingDialogState = - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(this.messageId, conversationId) + sureAboutMessagingDialogState = when(val currentState = sureAboutMessagingDialogState) { + // if multiple messages will fail, update messageIdList to retry sending all of them + is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending -> currentState.copy( + messageIdList = currentState.messageIdList.plus(messageId) + ) + SureAboutMessagingDialogState.Hidden, + is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending, + is SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded -> + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(listOf(messageId), conversationId) + } } } @@ -279,6 +302,12 @@ class SendMessageViewModel @Inject constructor( } } + fun retrySendingMessages(messageIdList: List, conversationId: ConversationId) { + messageIdList.forEach { + retrySendingMessage(it, conversationId) + } + } + fun retrySendingMessage(messageId: String, conversationId: ConversationId) { viewModelScope.launch { retryFailedMessage(messageId = messageId, conversationId = conversationId) @@ -295,13 +324,13 @@ class SendMessageViewModel @Inject constructor( it.markAsNotified(it.conversationId) when (it) { is SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded -> - trySendMessage(it.messageBundleToSend) + trySendMessages(it.messageBundleListToSend) is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending -> - trySendMessage(it.messageBundleToSend) + trySendMessages(it.messageBundleListToSend) is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending -> - retrySendingMessage(it.messageId, it.conversationId) + retrySendingMessages(it.messageIdList, it.conversationId) } } } @@ -324,8 +353,4 @@ class SendMessageViewModel @Inject constructor( } sureAboutMessagingDialogState = SureAboutMessagingDialogState.Hidden } - - companion object { - private const val sizeOf1MB = 1024 * 1024 - } } From 64d583fbf256343c79594643f52c772f11a5e2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Thu, 18 Apr 2024 09:43:00 +0200 Subject: [PATCH 02/12] feat: image preview pager --- .../common/imagepreview/AvatarPickerFlow.kt | 6 ++- .../home/conversations/ConversationScreen.kt | 14 +++--- .../media/preview/ImagesPreviewNavArgs.kt | 2 +- .../media/preview/ImagesPreviewScreen.kt | 40 ++++++++++------ .../media/preview/ImagesPreviewState.kt | 3 +- .../media/preview/ImagesPreviewViewModel.kt | 3 +- .../home/messagecomposer/AdditionalOptions.kt | 4 +- .../home/messagecomposer/AttachmentOptions.kt | 48 +++++++++++-------- .../messagecomposer/EnabledMessageComposer.kt | 4 +- .../home/messagecomposer/MessageComposer.kt | 6 +-- .../permission/OpenFileBrowserRequestFlow.kt | 2 +- .../util/permission/OpenGalleryRequestFlow.kt | 15 +++--- .../util/permission/UseStorageRequestFlow.kt | 5 +- 13 files changed, 86 insertions(+), 66 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/imagepreview/AvatarPickerFlow.kt b/app/src/main/kotlin/com/wire/android/ui/common/imagepreview/AvatarPickerFlow.kt index 6cc8d954f23..f80c97b2499 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/imagepreview/AvatarPickerFlow.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/imagepreview/AvatarPickerFlow.kt @@ -19,6 +19,7 @@ package com.wire.android.ui.common.imagepreview import android.net.Uri +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.wire.android.ui.userprofile.avatarpicker.ImageSource @@ -30,7 +31,7 @@ import com.wire.android.util.permission.rememberTakePictureFlow class AvatarPickerFlow( private val takePictureFlow: UseCameraRequestFlow, - private val openGalleryFlow: UseStorageRequestFlow + private val openGalleryFlow: UseStorageRequestFlow ) { fun launch(imageSource: ImageSource) { when (imageSource) { @@ -56,7 +57,8 @@ fun rememberPickPictureState( ) val openGalleryFlow = rememberOpenGalleryFlow( - onGalleryItemPicked = { pickedPictureUri -> onImageSelected(pickedPictureUri) }, + contract = ActivityResultContracts.GetContent(), + onGalleryItemPicked = { pickedPictureUri -> pickedPictureUri?.let { onImageSelected(it) } }, onPermissionDenied = { /* Nothing to do */ }, onPermissionPermanentlyDenied = onPermissionPermanentlyDenied ) 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 985fba4191d..09cae815630 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 @@ -340,13 +340,13 @@ fun ConversationScreen( ) }, onSendMessage = sendMessageViewModel::trySendMessage, - onImagePicked = { + onImagesPicked = { navigator.navigate( NavigationCommand( ImagesPreviewScreenDestination( conversationId = conversationInfoViewModel.conversationInfoViewState.conversationId, conversationName = conversationInfoViewModel.conversationInfoViewState.conversationName.asString(resources), - assetUri = it + assetUriList = ArrayList(it) ) ) ) @@ -639,7 +639,7 @@ private fun ConversationScreen( onOpenProfile: (String) -> Unit, onMessageDetailsClick: (messageId: String, isSelfMessage: Boolean) -> Unit, onSendMessage: (MessageBundle) -> Unit, - onImagePicked: (Uri) -> Unit, + onImagesPicked: (List) -> Unit, onDeleteMessage: (String, Boolean) -> Unit, onAudioClick: (String) -> Unit, onChangeAudioPosition: (String, Int) -> Unit, @@ -756,7 +756,7 @@ private fun ConversationScreen( messageComposerStateHolder = messageComposerStateHolder, messages = conversationMessagesViewState.messages, onSendMessage = onSendMessage, - onImagePicked = onImagePicked, + onImagesPicked = onImagesPicked, onAssetItemClicked = onAssetItemClicked, onAudioItemClicked = onAudioClick, onChangeAudioPosition = onChangeAudioPosition, @@ -804,7 +804,7 @@ private fun ConversationScreenContent( messageComposerStateHolder: MessageComposerStateHolder, messages: Flow>, onSendMessage: (MessageBundle) -> Unit, - onImagePicked: (Uri) -> Unit, + onImagesPicked: (List) -> Unit, onAssetItemClicked: (String) -> Unit, onAudioItemClicked: (String) -> Unit, onChangeAudioPosition: (String, Int) -> Unit, @@ -873,7 +873,7 @@ private fun ConversationScreenContent( tempWritableVideoUri = tempWritableVideoUri, tempWritableImageUri = tempWritableImageUri, onTypingEvent = onTypingEvent, - onImagePicked = onImagePicked + onImagesPicked = onImagesPicked ) } @@ -1152,6 +1152,6 @@ fun PreviewConversationScreen() { messageComposerStateHolder = messageComposerStateHolder, onLinkClick = { _ -> }, onTypingEvent = {}, - onImagePicked = {} + onImagesPicked = {} ) } 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 9bc17cfa753..30bab97843b 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 @@ -23,5 +23,5 @@ import com.wire.kalium.logic.data.id.ConversationId data class ImagesPreviewNavArgs( val conversationId: ConversationId, val conversationName: String, - val assetUri: Uri + val assetUriList: ArrayList ) 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 3088fb2234c..520fd431af1 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 @@ -17,6 +17,7 @@ */ package com.wire.android.ui.home.conversations.media.preview +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -27,6 +28,8 @@ 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.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -91,7 +94,7 @@ fun ImagesPreviewScreen( previewState = imagesPreviewViewModel.viewState, sendState = sendMessageViewModel.viewState, onNavigationPressed = { navigator.navigateBack() }, - onSendMessage = sendMessageViewModel::trySendMessage + onSendMessages = sendMessageViewModel::trySendMessages ) AssetTooLargeDialog( @@ -115,14 +118,16 @@ fun ImagesPreviewScreen( SnackBarMessage(sendMessageViewModel.infoMessage) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun Content( previewState: ImagesPreviewState, sendState: SendMessageState, onNavigationPressed: () -> Unit = {}, - onSendMessage: (MessageBundle) -> Unit + onSendMessages: (List) -> Unit ) { val configuration = LocalConfiguration.current + val pagerState = rememberPagerState(pageCount = { previewState.assetUriList.size }) WireScaffold( topBar = { WireCenterAlignedTopAppBar( @@ -164,11 +169,14 @@ private fun Content( ) }, onClick = { - onSendMessage( - ComposableMessageBundle.AttachmentPickedBundle( - previewState.conversationId, - UriAsset(previewState.assetUri) - ) + onSendMessages( + previewState.assetUriList.map { + ComposableMessageBundle.AttachmentPickedBundle( + previewState.conversationId, + UriAsset(it) + ) + } + ) } ) @@ -184,14 +192,16 @@ private fun Content( .fillMaxHeight() .fillMaxWidth() ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(previewState.assetUri) - .build(), - contentDescription = "preview_asset_image", - contentScale = ContentScale.FillWidth, - modifier = Modifier.width(configuration.screenWidthDp.dp) - ) + HorizontalPager(state = pagerState) { index -> + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(previewState.assetUriList[index]) + .build(), + contentDescription = "preview_asset_image", + contentScale = ContentScale.FillWidth, + modifier = Modifier.width(configuration.screenWidthDp.dp) + ) + } } } } 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 6ca3b29ac6c..526c94dae9d 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 @@ -19,5 +19,6 @@ package com.wire.android.ui.home.conversations.media.preview import android.net.Uri import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.collections.immutable.PersistentList -data class ImagesPreviewState(val conversationId: ConversationId, val conversationName: String, val assetUri: Uri) +data class ImagesPreviewState(val conversationId: ConversationId, val conversationName: String, val assetUriList: PersistentList) 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 ef1f3c8e228..8e9d356e6e5 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 @@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.navArgs +import kotlinx.collections.immutable.toPersistentList import javax.inject.Inject class ImagesPreviewViewModel @Inject constructor( @@ -34,7 +35,7 @@ class ImagesPreviewViewModel @Inject constructor( ImagesPreviewState( conversationId = navArgs.conversationId, conversationName = navArgs.conversationName, - assetUri = navArgs.assetUri + assetUriList = navArgs.assetUriList.toPersistentList() ) ) private set 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 6ebcf5aa40f..42b66c7185d 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 @@ -99,7 +99,7 @@ fun AdditionalOptionSubMenu( onCloseAdditionalAttachment: () -> Unit, onRecordAudioMessageClicked: () -> Unit, additionalOptionsState: AdditionalOptionSubMenuState, - onImagePicked: (Uri) -> Unit, + onImagesPicked: (List) -> Unit, onAttachmentPicked: (UriAsset) -> Unit, onAudioRecorded: (UriAsset) -> Unit, onLocationPicked: (GeoLocatedAddress) -> Unit, @@ -109,7 +109,7 @@ fun AdditionalOptionSubMenu( ) { Box(modifier = modifier) { AttachmentOptionsComponent( - onImagePicked = onImagePicked, + onImagesPicked = onImagesPicked, onAttachmentPicked = onAttachmentPicked, tempWritableImageUri = tempWritableImageUri, tempWritableVideoUri = tempWritableVideoUri, 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 9069d2468f5..451adbd8ff3 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 @@ -19,6 +19,7 @@ package com.wire.android.ui.home.messagecomposer import android.net.Uri +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement @@ -60,7 +61,7 @@ import com.wire.android.util.ui.KeyboardHeight @Composable fun AttachmentOptionsComponent( - onImagePicked: (Uri) -> Unit, + onImagesPicked: (List) -> Unit, onAttachmentPicked: (UriAsset) -> Unit, onRecordAudioMessageClicked: () -> Unit, tempWritableImageUri: Uri?, @@ -73,14 +74,14 @@ fun AttachmentOptionsComponent( val textMeasurer = rememberTextMeasurer() val attachmentOptions = buildAttachmentOptionItems( - isFileSharingEnabled, - tempWritableImageUri, - tempWritableVideoUri, - onImagePicked, - onAttachmentPicked, - onRecordAudioMessageClicked, - onLocationPickerClicked, - onCaptureVideoPermissionPermanentlyDenied + isFileSharingEnabled = isFileSharingEnabled, + tempWritableImageUri = tempWritableImageUri, + tempWritableVideoUri = tempWritableVideoUri, + onImagesPicked = onImagesPicked, + onFilePicked = onAttachmentPicked, + onRecordAudioMessageClicked = onRecordAudioMessageClicked, + onLocationPickerClicked = onLocationPickerClicked, + onPermissionPermanentlyDenied = onCaptureVideoPermissionPermanentlyDenied ) val labelStyle = MaterialTheme.wireTypography.button03 @@ -158,7 +159,7 @@ private fun calculateGridParams( fun FileBrowserFlow( onFilePicked: (Uri) -> Unit, onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit -): UseStorageRequestFlow { +): UseStorageRequestFlow { return rememberOpenFileBrowserFlow( onFileBrowserItemPicked = onFilePicked, onPermissionDenied = { /* Nothing to do */ }, @@ -167,12 +168,17 @@ fun FileBrowserFlow( } @Composable -private fun GalleryFlow( - onFilePicked: (Uri) -> Unit, +private fun MultipleGalleryFlow( + onImagesPicked: (List) -> Unit, onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit -): UseStorageRequestFlow { +): UseStorageRequestFlow> { return rememberOpenGalleryFlow( - onGalleryItemPicked = onFilePicked, + contract = ActivityResultContracts.GetMultipleContents(), + onGalleryItemPicked = { uris -> + if (uris.isNotEmpty()) { + onImagesPicked(uris) + } + }, onPermissionDenied = { /* Nothing to do */ }, onPermissionPermanentlyDenied = onPermissionPermanentlyDenied ) @@ -221,7 +227,7 @@ private fun buildAttachmentOptionItems( isFileSharingEnabled: Boolean, tempWritableImageUri: Uri?, tempWritableVideoUri: Uri?, - onImagePicked: (Uri) -> Unit, + onImagesPicked: (List) -> Unit, onFilePicked: (UriAsset) -> Unit, onRecordAudioMessageClicked: () -> Unit, onLocationPickerClicked: () -> Unit, @@ -231,8 +237,8 @@ private fun buildAttachmentOptionItems( remember { { onFilePicked(UriAsset(it, false)) } }, onPermissionPermanentlyDenied ) - val galleryFlow = GalleryFlow( - remember { { onImagePicked(it) } }, + val galleryFlow = MultipleGalleryFlow( + remember { { onImagesPicked(it) } }, onPermissionPermanentlyDenied ) val cameraFlow = TakePictureFlow( @@ -313,7 +319,7 @@ private data class AttachmentOptionItem( @Composable fun PreviewAttachmentComponents() { AttachmentOptionsComponent( - onImagePicked = {}, + onImagesPicked = {}, onAttachmentPicked = {}, isFileSharingEnabled = true, tempWritableImageUri = null, @@ -334,7 +340,7 @@ fun PreviewAttachmentOptionsComponentSmallScreen() { ) { AttachmentOptionsComponent( onAttachmentPicked = {}, - onImagePicked = {}, + onImagesPicked = {}, isFileSharingEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, @@ -356,7 +362,7 @@ fun PreviewAttachmentOptionsComponentNormalScreen() { ) { AttachmentOptionsComponent( onAttachmentPicked = {}, - onImagePicked = {}, + onImagesPicked = {}, isFileSharingEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, @@ -378,7 +384,7 @@ fun PreviewAttachmentOptionsComponentTabledScreen() { ) { AttachmentOptionsComponent( onAttachmentPicked = {}, - onImagePicked = {}, + onImagesPicked = {}, isFileSharingEnabled = true, tempWritableImageUri = null, tempWritableVideoUri = null, 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 fdaec494e6a..7ba10ba95d8 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 @@ -74,7 +74,7 @@ fun EnabledMessageComposer( onSearchMentionQueryChanged: (String) -> Unit, onTypingEvent: (Conversation.TypingIndicatorMode) -> Unit, onSendButtonClicked: () -> Unit, - onImagePicked: (Uri) -> Unit, + onImagesPicked: (List) -> Unit, onAttachmentPicked: (UriAsset) -> Unit, onAudioRecorded: (UriAsset) -> Unit, onLocationPicked: (GeoLocatedAddress) -> Unit, @@ -285,7 +285,7 @@ fun EnabledMessageComposer( onRecordAudioMessageClicked = ::toAudioRecording, onCloseAdditionalAttachment = ::toInitialAttachmentOptions, onLocationPickerClicked = ::toLocationPicker, - onImagePicked = onImagePicked, + onImagesPicked = onImagesPicked, onAttachmentPicked = onAttachmentPicked, onAudioRecorded = onAudioRecorded, onLocationPicked = onLocationPicked, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt index eeee01e564a..551f150b357 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt @@ -86,7 +86,7 @@ fun MessageComposer( tempWritableVideoUri: Uri?, tempWritableImageUri: Uri?, onTypingEvent: (TypingIndicatorMode) -> Unit, - onImagePicked: (Uri) -> Unit + onImagesPicked: (List) -> Unit ) { with(messageComposerStateHolder) { when (messageComposerViewState.value.interactionAvailability) { @@ -137,7 +137,7 @@ fun MessageComposer( clearMessage() }, onPingOptionClicked = { onSendMessageBundle(Ping(conversationId)) }, - onImagePicked = onImagePicked, + onImagesPicked = onImagesPicked, onAttachmentPicked = { onSendMessageBundle(ComposableMessageBundle.AttachmentPickedBundle(conversationId, it)) }, onAudioRecorded = { onSendMessageBundle(ComposableMessageBundle.AudioMessageBundle(conversationId, it)) }, onLocationPicked = { @@ -282,7 +282,7 @@ private fun BaseComposerPreview( tempWritableVideoUri = null, tempWritableImageUri = null, onTypingEvent = { }, - onImagePicked = {} + onImagesPicked = {} ) } diff --git a/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt b/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt index fc43c0f132b..208ced0a0ba 100644 --- a/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt +++ b/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt @@ -39,7 +39,7 @@ fun rememberOpenFileBrowserFlow( onFileBrowserItemPicked: (Uri) -> Unit, onPermissionDenied: () -> Unit, onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit -): UseStorageRequestFlow { +): UseStorageRequestFlow { val context = LocalContext.current val openFileBrowserLauncher: ManagedActivityResultLauncher = diff --git a/app/src/main/kotlin/com/wire/android/util/permission/OpenGalleryRequestFlow.kt b/app/src/main/kotlin/com/wire/android/util/permission/OpenGalleryRequestFlow.kt index 017d464ab58..5e907c6e4b4 100644 --- a/app/src/main/kotlin/com/wire/android/util/permission/OpenGalleryRequestFlow.kt +++ b/app/src/main/kotlin/com/wire/android/util/permission/OpenGalleryRequestFlow.kt @@ -18,9 +18,9 @@ package com.wire.android.util.permission -import android.net.Uri import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -35,17 +35,18 @@ import com.wire.android.util.extension.getActivity * @param onPermissionDenied action to be executed when the permissions is denied */ @Composable -fun rememberOpenGalleryFlow( - onGalleryItemPicked: (Uri) -> Unit, +fun rememberOpenGalleryFlow( + contract: ActivityResultContract, + onGalleryItemPicked: (T) -> Unit, onPermissionDenied: () -> Unit, onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit, -): UseStorageRequestFlow { +): UseStorageRequestFlow { val context = LocalContext.current - val openGalleryLauncher: ManagedActivityResultLauncher = + val openGalleryLauncher: ManagedActivityResultLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() + contract ) { onChosenPictureUri -> - onChosenPictureUri?.let { onGalleryItemPicked(it) } + onGalleryItemPicked(onChosenPictureUri) } val requestPermissionLauncher: ManagedActivityResultLauncher = diff --git a/app/src/main/kotlin/com/wire/android/util/permission/UseStorageRequestFlow.kt b/app/src/main/kotlin/com/wire/android/util/permission/UseStorageRequestFlow.kt index 7ed6c7d6c2c..689997ea97f 100644 --- a/app/src/main/kotlin/com/wire/android/util/permission/UseStorageRequestFlow.kt +++ b/app/src/main/kotlin/com/wire/android/util/permission/UseStorageRequestFlow.kt @@ -20,16 +20,15 @@ package com.wire.android.util.permission import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.content.Context -import android.net.Uri import android.os.Build import androidx.activity.compose.ManagedActivityResultLauncher import androidx.appcompat.app.AppCompatActivity import com.wire.android.util.extension.checkPermission -class UseStorageRequestFlow( +class UseStorageRequestFlow( private val mimeType: String, private val context: Context, - private val browseStorageActivityLauncher: ManagedActivityResultLauncher, + private val browseStorageActivityLauncher: ManagedActivityResultLauncher, private val accessFilePermissionLauncher: ManagedActivityResultLauncher ) { fun launch() { From a2cf37d087a3a535c6ad32658f84a8e97d727085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 23 Apr 2024 08:56:07 +0200 Subject: [PATCH 03/12] remove duplicated functions --- .../media/preview/ImagesPreviewScreen.kt | 215 ++++++++++++++++-- .../media/preview/ImagesPreviewState.kt | 10 +- .../media/preview/ImagesPreviewViewModel.kt | 41 +++- .../home/conversations/model/MessageTypes.kt | 11 +- .../sendmessage/SendMessageState.kt | 15 +- .../sendmessage/SendMessageViewModel.kt | 73 ++++-- .../ImportMediaAuthenticatedViewModel.kt | 98 +------- .../android/ui/sharing/ImportMediaScreen.kt | 35 ++- .../android/ui/sharing/ImportedMediaAsset.kt | 23 +- .../android/ui/sharing/ImportedMediaTypes.kt | 59 ----- ...ges.kt => SendMessagesSnackbarMessages.kt} | 8 +- .../android/util/ui/WireSessionImageLoader.kt | 3 +- .../main/res/drawable/mock_message_image.png | Bin 120783 -> 0 bytes core/ui-common/build.gradle.kts | 5 + .../wire/android/ui/common/image/WireImage.kt | 75 ++++++ .../common/preview/PreviewMultipleThemes.kt | 49 ++++ .../src/main/res/drawable/mock_image.jpeg | Bin 0 -> 67702 bytes 17 files changed, 471 insertions(+), 249 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt rename app/src/main/kotlin/com/wire/android/ui/sharing/{ImportMediaSnackbarMessages.kt => SendMessagesSnackbarMessages.kt} (66%) delete mode 100644 app/src/main/res/drawable/mock_message_image.png create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/WireImage.kt create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/ui/common/preview/PreviewMultipleThemes.kt create mode 100644 core/ui-common/src/main/res/drawable/mock_image.jpeg 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 520fd431af1..d064642b45a 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 @@ -17,24 +17,38 @@ */ package com.wire.android.ui.home.conversations.media.preview +import android.net.Uri import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size 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 import androidx.compose.runtime.LaunchedEffect 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.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration @@ -43,12 +57,12 @@ 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 coil.compose.AsyncImage -import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph 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.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.button.WirePrimaryButton @@ -57,21 +71,33 @@ 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.image.WireImage 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.model.AssetBundle import com.wire.android.ui.home.conversations.model.UriAsset +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 +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 okio.Path.Companion.toPath @RootNavGraph @Destination( @@ -84,9 +110,24 @@ fun ImagesPreviewScreen( imagesPreviewViewModel: ImagesPreviewViewModel = hiltViewModel(), sendMessageViewModel: SendMessageViewModel = hiltViewModel() ) { - LaunchedEffect(sendMessageViewModel.viewState.messageSent) { - if (sendMessageViewModel.viewState.messageSent) { - navigator.navigateBack() + 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 -> {} } } @@ -94,7 +135,8 @@ fun ImagesPreviewScreen( previewState = imagesPreviewViewModel.viewState, sendState = sendMessageViewModel.viewState, onNavigationPressed = { navigator.navigateBack() }, - onSendMessages = sendMessageViewModel::trySendMessages + onSendMessages = sendMessageViewModel::trySendMessages, + onSelected = imagesPreviewViewModel::onSelected ) AssetTooLargeDialog( @@ -124,10 +166,23 @@ private fun Content( previewState: ImagesPreviewState, sendState: SendMessageState, onNavigationPressed: () -> Unit = {}, - onSendMessages: (List) -> Unit + onSendMessages: (List) -> Unit, + onSelected: (index: Int) -> Unit ) { val configuration = LocalConfiguration.current val pagerState = rememberPagerState(pageCount = { previewState.assetUriList.size }) + LaunchedEffect(key1 = previewState.selectedIndex) { + if (previewState.selectedIndex != pagerState.currentPage) { + pagerState.animateScrollToPage(previewState.selectedIndex) + } + } + + LaunchedEffect(key1 = pagerState.currentPage) { + if (previewState.selectedIndex != pagerState.currentPage) { + onSelected(pagerState.currentPage) + } + } + WireScaffold( topBar = { WireCenterAlignedTopAppBar( @@ -173,7 +228,7 @@ private fun Content( previewState.assetUriList.map { ComposableMessageBundle.AttachmentPickedBundle( previewState.conversationId, - UriAsset(it) + UriAsset(Uri.fromFile(it.assetBundle.dataPath.toFile())) ) } @@ -186,26 +241,119 @@ private fun Content( } ) { padding -> Box( - contentAlignment = Alignment.Center, modifier = Modifier .padding(padding) .fillMaxHeight() .fillMaxWidth() ) { - HorizontalPager(state = pagerState) { index -> - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(previewState.assetUriList[index]) - .build(), - contentDescription = "preview_asset_image", - contentScale = ContentScale.FillWidth, - modifier = Modifier.width(configuration.screenWidthDp.dp) - ) + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .width(configuration.screenWidthDp.dp) + .fillMaxHeight(), + ) { index -> + WireImage( + modifier = Modifier + .width(configuration.screenWidthDp.dp) + .fillMaxHeight(), + model = previewState.assetUriList[index].assetBundle.dataPath.toFile(), + contentDescription = previewState.assetUriList[index].assetBundle.fileName + ) + } + } + + LazyRow( + modifier = Modifier + .padding(bottom = dimensions().spacing8x) + .height(dimensions().spacing72x) + .align(Alignment.BottomCenter), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) + ) { + items( + count = previewState.assetUriList.size, + ) { index -> + Box( + modifier = Modifier + .width(dimensions().spacing72x) + .fillMaxHeight() + ) { + AssetPreview( + modifier = Modifier + .size(dimensions().spacing64x) + .align(Alignment.BottomStart), + asset = previewState.assetUriList[index], + isSelected = previewState.selectedIndex == index, + onClick = { onSelected(index) } + ) + RemoveAssetButton(modifier = Modifier.align(Alignment.TopEnd), onClick = {}) + } + } } } } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AssetPreview( + modifier: Modifier = Modifier, + asset: ImportedMediaAsset, + isSelected: Boolean = false, + onClick: () -> Unit +) { + Box( + modifier + .clip(shape = RoundedCornerShape(dimensions().messageAssetBorderRadius)) + .background( + color = MaterialTheme.wireColorScheme.onPrimary, + shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) + ) + .border( + width = if (isSelected) { + dimensions().spacing2x + } else { + dimensions().spacing1x + }, + color = if (isSelected) { + MaterialTheme.wireColorScheme.primary + } else { + MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline + }, + shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) + ) + .combinedClickable( + onClick = onClick, + onLongClick = {}, + ) + ) { + WireImage( + modifier = Modifier.fillMaxSize(), + model = asset.assetBundle.dataPath.toFile(), + contentScale = ContentScale.Crop, + contentDescription = asset.assetBundle.fileName + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RemoveAssetButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Icon( + modifier = modifier + .combinedClickable(onClick = onClick) + .clip(shape = CircleShape) + .background(color = MaterialTheme.wireColorScheme.inverseSurface) + .padding(dimensions().spacing6x), + painter = painterResource(id = R.drawable.ic_close), contentDescription = "test", + tint = MaterialTheme.wireColorScheme.inverseOnSurface + ) +} + @Composable private fun SnackBarMessage(infoMessages: SharedFlow) { val context = LocalContext.current @@ -219,3 +367,34 @@ private fun SnackBarMessage(infoMessages: SharedFlow) { } } } + + +@PreviewMultipleThemes +@Composable +fun PreviewImagesPreviewScreen() { + WireTheme { + Content( + previewState = ImagesPreviewState( + ConversationId("value", "domain"), + "Conversation", + persistentListOf( + ImportedMediaAsset( + AssetBundle( + "key", + "image/png", + "".toPath(), + 20, + "preview", + assetType = AttachmentType.IMAGE + ), + assetSizeExceeded = null + ) + ), + ), + sendState = SendMessageState(inProgress = false), + 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 526c94dae9d..d534d773363 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 @@ -17,8 +17,14 @@ */ package com.wire.android.ui.home.conversations.media.preview -import android.net.Uri +import com.wire.android.ui.sharing.ImportedMediaAsset import com.wire.kalium.logic.data.id.ConversationId import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf -data class ImagesPreviewState(val conversationId: ConversationId, val conversationName: String, val assetUriList: PersistentList) +data class ImagesPreviewState( + val conversationId: ConversationId, + val conversationName: String, + val assetUriList: PersistentList = persistentListOf(), + val selectedIndex: Int = 0 +) 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 8e9d356e6e5..59e3e70e6e0 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 @@ -17,26 +17,63 @@ */ package com.wire.android.ui.home.conversations.media.preview +import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope import com.wire.android.navigation.SavedStateViewModel +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.navArgs +import com.wire.android.ui.sharing.ImportedMediaAsset +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject +@HiltViewModel class ImagesPreviewViewModel @Inject constructor( override val savedStateHandle: SavedStateHandle, + private val handleUriAsset: HandleUriAssetUseCase, + private val dispatchers: DispatcherProvider ) : SavedStateViewModel(savedStateHandle) { private val navArgs: ImagesPreviewNavArgs = savedStateHandle.navArgs() var viewState by mutableStateOf( ImagesPreviewState( conversationId = navArgs.conversationId, - conversationName = navArgs.conversationName, - assetUriList = navArgs.assetUriList.toPersistentList() + conversationName = navArgs.conversationName ) ) private set + + init { + handleAssets() + } + + fun onSelected(index: Int) { + viewState = viewState.copy(selectedIndex = index) + } + + private fun handleAssets() { + viewModelScope.launch { + val assets = navArgs.assetUriList.map { handleImportedAsset(it) } + viewState = viewState.copy( + assetUriList = assets.filterNotNull().toPersistentList() + ) + } + } + + private suspend fun handleImportedAsset(uri: Uri): ImportedMediaAsset? = withContext(dispatchers.io()) { + when (val result = handleUriAsset.invoke(uri, saveToDeviceIfInvalid = false, audioPath = null)) { + is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) + + HandleUriAssetUseCase.Result.Failure.Unknown -> null + is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) + } + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt index 07bc1be625c..af415610a2a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt @@ -169,11 +169,10 @@ fun MessageButtonsContent( @OptIn(ExperimentalFoundationApi::class) @Composable fun MessageImage( - asset: ImageAsset?, + asset: ImageAsset.Remote?, imgParams: ImageMessageParams, transferStatus: AssetTransferStatus, - onImageClick: Clickable, - shouldFillMaxWidth: Boolean = false, + onImageClick: Clickable ) { Box( Modifier @@ -212,11 +211,7 @@ fun MessageImage( ) } - asset != null -> when (asset) { - is ImageAsset.Local -> ImportedImageMessage(asset, shouldFillMaxWidth) - is ImageAsset.Remote -> DisplayableImageMessage(asset, imgParams.normalizedWidth, imgParams.normalizedHeight) - } - + asset != null -> DisplayableImageMessage(asset, imgParams.normalizedWidth, imgParams.normalizedHeight) // Show error placeholder transferStatus == FAILED_UPLOAD || transferStatus == FAILED_DOWNLOAD -> { ImageMessageFailed( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt index e2a8262caa6..6ba55c765e7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt @@ -17,4 +17,17 @@ */ package com.wire.android.ui.home.conversations.sendmessage -data class SendMessageState(val messageSent: Boolean, val inProgress: Boolean) +import com.wire.kalium.logic.data.id.ConversationId + +data class SendMessageState( + val inProgress: Boolean = false, + val afterMessageSendAction: SendMessageAction = SendMessageAction.None +) + +sealed class SendMessageAction { + data object None : SendMessageAction() + + data object NavigateBack: SendMessageAction() + data class NavigateToConversation(val conversationId: ConversationId) : SendMessageAction() + data object NavigateToHome : SendMessageAction() +} 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 ace00d786e7..8b62c81f467 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 @@ -36,6 +36,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.sharing.SendMessagesSnackbarMessages import com.wire.android.util.ImageUtil import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.getAudioLengthInMs @@ -62,9 +63,11 @@ 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 import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okio.Path @@ -104,12 +107,7 @@ class SendMessageViewModel @Inject constructor( SureAboutMessagingDialogState.Hidden ) - var viewState: SendMessageState by mutableStateOf( - SendMessageState( - messageSent = false, - inProgress = false - ) - ) + var viewState: SendMessageState by mutableStateOf(SendMessageState()) private fun onSnackbarMessage(type: SnackBarMessage) = viewModelScope.launch { _infoMessage.emit(type) @@ -126,28 +124,46 @@ class SendMessageViewModel @Inject constructor( } fun trySendMessages(messageBundleList: List) { - val messageBundleMap = messageBundleList.groupBy { it.conversationId } - - messageBundleMap.forEach { (conversationId, bundles) -> - viewModelScope.launch { - when { - shouldInformAboutDegradedBeforeSendingMessage(conversationId) -> - sureAboutMessagingDialogState = - SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded(conversationId, bundles) - - shouldInformAboutUnderLegalHoldBeforeSendingMessage(conversationId) -> - sureAboutMessagingDialogState = - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending(conversationId, messageBundleList) + if (messageBundleList.size > MAX_LIMIT_MESSAGE_SEND) { + onSnackbarMessage(SendMessagesSnackbarMessages.MaxAmountOfAssetsReached) + } else { + val messageBundleMap = messageBundleList.groupBy { it.conversationId } + messageBundleMap.forEach { (conversationId, bundles) -> + viewModelScope.launch { + when { + shouldInformAboutDegradedBeforeSendingMessage(conversationId) -> + sureAboutMessagingDialogState = + SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded(conversationId, bundles) + + shouldInformAboutUnderLegalHoldBeforeSendingMessage(conversationId) -> + sureAboutMessagingDialogState = + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending( + conversationId, + messageBundleList + ) - else -> sendMessages(messageBundleList) + else -> sendMessages(messageBundleList) + } } } } } private suspend fun sendMessages(messageBundleList: List) { + val jobs: MutableCollection = mutableListOf() messageBundleList.forEach { - sendMessage(it) + val job = viewModelScope.launch { + sendMessage(it) + } + 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) } } @@ -305,11 +321,12 @@ class SendMessageViewModel @Inject constructor( private fun CoreFailure.handleLegalHoldFailureAfterSendingMessage(conversationId: ConversationId) = also { if (this is LegalHoldEnabledForConversationFailure) { - sureAboutMessagingDialogState = when(val currentState = sureAboutMessagingDialogState) { + sureAboutMessagingDialogState = when (val currentState = sureAboutMessagingDialogState) { // if multiple messages will fail, update messageIdList to retry sending all of them is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending -> currentState.copy( messageIdList = currentState.messageIdList.plus(messageId) ) + SureAboutMessagingDialogState.Hidden, is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending, is SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded -> @@ -384,10 +401,20 @@ class SendMessageViewModel @Inject constructor( } private fun beforeSendingMessage() { - viewState = viewState.copy(messageSent = false, inProgress = true) + viewState = viewState.copy(inProgress = true) } private fun Either.handleAfterMessageResult() { - viewState = viewState.copy(messageSent = this.isRight(), inProgress = false) + 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 37e4e48047d..112b2fbeff8 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,7 +36,6 @@ 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.model.AssetBundle 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 @@ -46,18 +45,14 @@ import com.wire.android.ui.home.conversationslist.parseConversationEventType import com.wire.android.ui.home.conversationslist.parsePrivateConversationEventType import com.wire.android.ui.home.conversationslist.showLegalHoldIndicator import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration -import com.wire.android.util.ImageUtil import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.getAudioLengthInMs import com.wire.android.util.parcelableArrayList import com.wire.android.util.ui.WireSessionImageLoader -import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.message.SelfDeletionTimer.Companion.SELF_DELETION_LOG_TAG -import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageResult import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsUseCase import com.wire.kalium.logic.feature.message.SendTextMessageUseCase @@ -70,7 +65,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -79,7 +73,6 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -303,7 +296,7 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( handleImportedAsset(uri)?.let { importedAsset -> if (importedAsset.assetSizeExceeded != null) { onSnackbarMessage( - ImportMediaSnackbarMessages.MaxAssetSizeExceeded(importedAsset.assetSizeExceeded!!) + SendMessagesSnackbarMessages.MaxAssetSizeExceeded(importedAsset.assetSizeExceeded!!) ) } importMediaState = importMediaState.copy(importedAssets = persistentListOf(importedAsset)) @@ -320,67 +313,10 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( importMediaState = importMediaState.copy(importedAssets = importedMediaAssets.toPersistentList()) importedMediaAssets.firstOrNull { it.assetSizeExceeded != null }?.let { - onSnackbarMessage(ImportMediaSnackbarMessages.MaxAssetSizeExceeded(it.assetSizeExceeded!!)) + onSnackbarMessage(SendMessagesSnackbarMessages.MaxAssetSizeExceeded(it.assetSizeExceeded!!)) } } - fun checkRestrictionsAndSendImportedMedia(onSent: (ConversationId) -> Unit) = - viewModelScope.launch(dispatchers.default()) { - val conversation = - importMediaState.selectedConversationItem.firstOrNull() ?: return@launch - val assetsToSend = importMediaState.importedAssets - val textToSend = importMediaState.importedText - - if (assetsToSend.size > MAX_LIMIT_MEDIA_IMPORT) { - onSnackbarMessage(ImportMediaSnackbarMessages.MaxAmountOfAssetsReached) - } else { - val jobs: MutableCollection = mutableListOf() - - textToSend?.let { - sendTextMessage( - conversationId = conversation.conversationId, - text = it - ) - } ?: assetsToSend.forEach { importedAsset -> - val isImage = importedAsset is ImportedMediaAsset.Image - val job = viewModelScope.launch { - sendAssetMessage( - conversationId = conversation.conversationId, - assetDataPath = importedAsset.assetBundle.dataPath, - assetName = importedAsset.assetBundle.fileName, - assetDataSize = importedAsset.assetBundle.dataSize, - assetMimeType = importedAsset.assetBundle.mimeType, - assetWidth = if (isImage) (importedAsset as ImportedMediaAsset.Image).width else 0, - assetHeight = if (isImage) (importedAsset as ImportedMediaAsset.Image).height else 0, - audioLengthInMs = getAudioLengthInMs( - dataPath = importedAsset.assetBundle.dataPath, - mimeType = importedAsset.assetBundle.mimeType, - ) - ).also { - val logConversationId = conversation.conversationId.toLogString() - if (it is ScheduleNewAssetMessageResult.Failure) { - appLogger.e( - "Failed to import asset message to " + - "conversationId=$logConversationId" - ) - } else { - appLogger.d( - "Success importing asset message to " + - "conversationId=$logConversationId" - ) - } - } - } - jobs.add(job) - } - - jobs.joinAll() - withContext(dispatchers.main()) { - onSent(conversation.conversationId) - } - } - } - fun onNewConversationPicked(conversationId: ConversationId) = viewModelScope.launch { importMediaState = importMediaState.copy( selfDeletingTimer = observeSelfDeletionSettingsForConversation( @@ -415,36 +351,10 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( private suspend fun handleImportedAsset(uri: Uri): ImportedMediaAsset? = withContext(dispatchers.io()) { when (val result = handleUriAsset.invoke(uri, saveToDeviceIfInvalid = false, audioPath = null)) { - is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> mapToImportedAsset(result.assetBundle, result.maxLimitInMB) + is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) HandleUriAssetUseCase.Result.Failure.Unknown -> null - is HandleUriAssetUseCase.Result.Success -> mapToImportedAsset(result.assetBundle, null) - } - } - - private fun mapToImportedAsset(assetBundle: AssetBundle, assetSizeExceeded: Int?): ImportedMediaAsset { - return when (assetBundle.assetType) { - AttachmentType.IMAGE -> { - val (imgWidth, imgHeight) = ImageUtil.extractImageWidthAndHeight( - kaliumFileSystem, - assetBundle.dataPath - ) - ImportedMediaAsset.Image( - assetBundle = assetBundle, - width = imgWidth, - height = imgHeight, - assetSizeExceeded = assetSizeExceeded - ) - } - - AttachmentType.GENERIC_FILE, - AttachmentType.AUDIO, - AttachmentType.VIDEO -> { - ImportedMediaAsset.GenericAsset( - assetBundle = assetBundle, - assetSizeExceeded = assetSizeExceeded - ) - } + is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) } } 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 2aabe3bb63a..4421f036e41 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 @@ -54,8 +54,6 @@ import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.SnackBarMessage import com.wire.android.model.UserAvatarData -import com.wire.android.navigation.BackStackMode -import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout @@ -68,10 +66,11 @@ 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.home.FeatureFlagState +import com.wire.android.ui.home.conversations.media.preview.AssetPreview 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.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 @@ -116,19 +115,22 @@ fun ImportMediaScreen( FeatureFlagState.SharingRestrictedState.NONE -> { val importMediaViewModel: ImportMediaAuthenticatedViewModel = hiltViewModel() + val sendMessageViewModel: SendMessageViewModel = hiltViewModel() + ImportMediaRegularContent( importMediaAuthenticatedState = importMediaViewModel.importMediaState, onSearchQueryChanged = importMediaViewModel::onSearchQueryChanged, onConversationClicked = importMediaViewModel::onConversationClicked, checkRestrictionsAndSendImportedMedia = { - importMediaViewModel.checkRestrictionsAndSendImportedMedia { - navigator.navigate( - NavigationCommand( - ConversationScreenDestination(it), - BackStackMode.REMOVE_CURRENT - ) - ) - } + // TODO KBX +// importMediaViewModel.checkRestrictionsAndSendImportedMedia { +// navigator.navigate( +// NavigationCommand( +// ConversationScreenDestination(it), +// BackStackMode.REMOVE_CURRENT +// ) +// ) +// } }, onNewSelfDeletionTimerPicked = importMediaViewModel::onNewSelfDeletionTimerPicked, infoMessage = importMediaViewModel.infoMessage, @@ -381,10 +383,7 @@ private fun ImportMediaContent( } } else if (!isMultipleImport) { Box(modifier = Modifier.padding(horizontal = dimensions().spacing16x)) { - ImportedMediaItemView( - item = importedItemsList.first(), - isMultipleImport = false - ) + AssetPreview(asset = importedItemsList.first(), onClick = {}) } } else { LazyRow( @@ -395,9 +394,9 @@ private fun ImportMediaContent( items( count = importedItemsList.size, ) { index -> - ImportedMediaItemView( - item = importedItemsList[index], - isMultipleImport = true + AssetPreview( + asset = importedItemsList[index], + onClick = {} ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaAsset.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaAsset.kt index 1ea9271e704..97ffb1137f5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaAsset.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaAsset.kt @@ -17,24 +17,9 @@ */ package com.wire.android.ui.sharing -import com.wire.android.model.ImageAsset import com.wire.android.ui.home.conversations.model.AssetBundle -sealed class ImportedMediaAsset( - open val assetBundle: AssetBundle, - open val assetSizeExceeded: Int? -) { - class GenericAsset( - override val assetBundle: AssetBundle, - override val assetSizeExceeded: Int?, - ) : ImportedMediaAsset(assetBundle, assetSizeExceeded) - - class Image( - val width: Int, - val height: Int, - override val assetBundle: AssetBundle, - override val assetSizeExceeded: Int?, - ) : ImportedMediaAsset(assetBundle, assetSizeExceeded) { - val localImageAsset = ImageAsset.Local(assetBundle.dataPath, assetBundle.key) - } -} +data class ImportedMediaAsset( + val assetBundle: AssetBundle, + val assetSizeExceeded: Int? +) diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt deleted file mode 100644 index 424b8b62378..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportedMediaTypes.kt +++ /dev/null @@ -1,59 +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.ui.sharing - -import androidx.compose.runtime.Composable -import com.wire.android.model.Clickable -import com.wire.android.ui.home.conversations.model.MessageGenericAsset -import com.wire.android.ui.home.conversations.model.MessageImage -import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageParams -import com.wire.kalium.logic.data.asset.AssetTransferStatus -import com.wire.kalium.logic.util.fileExtension -import com.wire.kalium.logic.util.splitFileExtension - -@Composable -fun ImportedMediaItemView(item: ImportedMediaAsset, isMultipleImport: Boolean) { - when (item) { - is ImportedMediaAsset.GenericAsset -> ImportedGenericAssetView(item, isMultipleImport) - is ImportedMediaAsset.Image -> ImportedImageView(item, isMultipleImport) - } -} - -@Composable -fun ImportedImageView(item: ImportedMediaAsset.Image, isMultipleImport: Boolean) { - MessageImage( - asset = item.localImageAsset, - imgParams = ImageMessageParams(item.width, item.height), - transferStatus = AssetTransferStatus.NOT_DOWNLOADED, - onImageClick = Clickable(enabled = false), - shouldFillMaxWidth = !isMultipleImport, - ) -} - -@Composable -fun ImportedGenericAssetView(item: ImportedMediaAsset.GenericAsset, isMultipleImport: Boolean) { - MessageGenericAsset( - assetName = item.assetBundle.fileName.splitFileExtension().first, - assetExtension = item.assetBundle.fileName.fileExtension() ?: "", - assetSizeInBytes = item.assetBundle.dataSize, - onAssetClick = Clickable(enabled = false), - assetTransferStatus = AssetTransferStatus.NOT_DOWNLOADED, - shouldFillMaxWidth = !isMultipleImport, - isImportedMediaAsset = true - ) -} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSnackbarMessages.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt similarity index 66% rename from app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSnackbarMessages.kt rename to app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt index d1e5fcc34e0..b88a0c1f952 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSnackbarMessages.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt @@ -21,10 +21,10 @@ import com.wire.android.R import com.wire.android.model.SnackBarMessage import com.wire.android.util.ui.UIText -sealed class ImportMediaSnackbarMessages(override val uiText: UIText) : SnackBarMessage { - object MaxImageSize : ImportMediaSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_image_size_limit)) +sealed class SendMessagesSnackbarMessages(override val uiText: UIText) : SnackBarMessage { // TODO KBX + object MaxImageSize : SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_image_size_limit)) object MaxAmountOfAssetsReached : - ImportMediaSnackbarMessages(UIText.StringResource(R.string.error_limit_number_assets_imported_exceeded)) + SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_limit_number_assets_imported_exceeded)) class MaxAssetSizeExceeded(assetSizeLimit: Int) : - ImportMediaSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_asset_size_limit, assetSizeLimit)) + SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_asset_size_limit, assetSizeLimit)) } diff --git a/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt b/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt index ee32413b1ee..17366c35b4b 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/WireSessionImageLoader.kt @@ -20,6 +20,7 @@ package com.wire.android.util.ui import android.content.Context import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -137,7 +138,7 @@ class WireSessionImageLoader( drawableResultWrapper = DrawableResultWrapper(), ) ) - if (SDK_INT >= 28) { + if (SDK_INT >= VERSION_CODES.P) { add(ImageDecoderDecoder.Factory()) } else { add(GifDecoder.Factory()) diff --git a/app/src/main/res/drawable/mock_message_image.png b/app/src/main/res/drawable/mock_message_image.png deleted file mode 100644 index 9ab60388a78f251d282a62b5edea0d4a25fb374d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120783 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�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 diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 5262f93c7f0..ec13ed135e5 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -28,6 +28,11 @@ dependencies { implementation(libs.accompanist.systemUI) implementation(libs.visibilityModifiers) + // Image loading + implementation(libs.coil.core) + implementation(libs.coil.gif) + implementation(libs.coil.compose) + testImplementation(libs.junit4) androidTestImplementation(libs.androidx.test.extJunit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/WireImage.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/WireImage.kt new file mode 100644 index 00000000000..37c7a9308fc --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/WireImage.kt @@ -0,0 +1,75 @@ +/* + * 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.image + +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import coil.compose.AsyncImage +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import com.wire.android.ui.common.R +import com.wire.android.ui.common.preview.PreviewMultipleThemes +import com.wire.android.ui.theme.WireTheme + +@Composable +fun WireImage( + modifier: Modifier = Modifier, + model: Any?, + contentDescription: String, + contentScale: ContentScale = ContentScale.Fit, + placeholder: Painter? = null, +) { + AsyncImage( + modifier = modifier, + placeholder = if (LocalInspectionMode.current) { + painterResource(R.drawable.mock_image) + } else { + placeholder + }, + model = ImageRequest.Builder(LocalContext.current) + .data(model) + .decoderFactory( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoderDecoder.Factory() + } else { + GifDecoder.Factory() + } + ) + .build(), + contentDescription = contentDescription, + contentScale = contentScale + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewWireImage() { + WireTheme { + WireImage( + model = null, + contentDescription = "preview" + ) + } +} diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/preview/PreviewMultipleThemes.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/preview/PreviewMultipleThemes.kt new file mode 100644 index 00000000000..4c6eae35b4b --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/preview/PreviewMultipleThemes.kt @@ -0,0 +1,49 @@ +/* + * 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.preview + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview +import com.wire.android.ui.theme.WireColorScheme +import com.wire.android.ui.theme.WireTheme + +@Preview( + name = "Dark theme", + showBackground = true, + backgroundColor = 0xFF17181A, + uiMode = UI_MODE_NIGHT_YES +) +@Preview( + name = "Light theme", + showBackground = true, + backgroundColor = 0xFFEDEFF0, + uiMode = UI_MODE_NIGHT_NO +) +/** + * Helper annotation that adds a preview for Light and Dark theme previews, _i.e._ + * with [Preview.uiMode] set to [UI_MODE_NIGHT_NO] and [UI_MODE_NIGHT_YES]. + * It has hardcoded background colors following the [WireColorScheme]. + * + * **Important** + * + * Just like regular [Preview] annotations, it's important that the composable + * preview is actually reactive to the change in theme. So it might be necessary + * to wrap the preview in a [WireTheme] block. + */ +annotation class PreviewMultipleThemes diff --git a/core/ui-common/src/main/res/drawable/mock_image.jpeg b/core/ui-common/src/main/res/drawable/mock_image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..022538228cab95abee4c0a1de96dd1df7a027eca GIT binary patch literal 67702 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 AYXATM literal 0 HcmV?d00001 From 8fc728064a30b2c93bcdcdb6b4cca2c36a00deb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Thu, 25 Apr 2024 10:45:43 +0200 Subject: [PATCH 04/12] chore: merged sending asset functionality --- .../media/preview/ImagesPreviewScreen.kt | 16 ++++++------- .../media/preview/ImagesPreviewState.kt | 2 +- .../media/preview/ImagesPreviewViewModel.kt | 3 +-- .../sendmessage/SendMessageViewModel.kt | 5 ++++ .../home/messagecomposer/MessageComposer.kt | 2 +- .../messagecomposer/model/MessageBundle.kt | 6 +++++ .../ImportMediaAuthenticatedViewModel.kt | 2 +- .../android/ui/sharing/ImportMediaScreen.kt | 24 +++++++++++++++---- .../wire/android/ui/theme/WireDimensions.kt | 2 ++ 9 files changed, 44 insertions(+), 18 deletions(-) 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 d064642b45a..31af5c48f55 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 @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home.conversations.media.preview -import android.net.Uri import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.LocalOverscrollConfiguration @@ -82,7 +81,6 @@ 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.model.AssetBundle -import com.wire.android.ui.home.conversations.model.UriAsset 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 @@ -170,7 +168,7 @@ private fun Content( onSelected: (index: Int) -> Unit ) { val configuration = LocalConfiguration.current - val pagerState = rememberPagerState(pageCount = { previewState.assetUriList.size }) + val pagerState = rememberPagerState(pageCount = { previewState.assetBundleList.size }) LaunchedEffect(key1 = previewState.selectedIndex) { if (previewState.selectedIndex != pagerState.currentPage) { pagerState.animateScrollToPage(previewState.selectedIndex) @@ -225,10 +223,10 @@ private fun Content( }, onClick = { onSendMessages( - previewState.assetUriList.map { + previewState.assetBundleList.map { ComposableMessageBundle.AttachmentPickedBundle( previewState.conversationId, - UriAsset(Uri.fromFile(it.assetBundle.dataPath.toFile())) + it.assetBundle ) } @@ -257,8 +255,8 @@ private fun Content( modifier = Modifier .width(configuration.screenWidthDp.dp) .fillMaxHeight(), - model = previewState.assetUriList[index].assetBundle.dataPath.toFile(), - contentDescription = previewState.assetUriList[index].assetBundle.fileName + model = previewState.assetBundleList[index].assetBundle.dataPath.toFile(), + contentDescription = previewState.assetBundleList[index].assetBundle.fileName ) } } @@ -272,7 +270,7 @@ private fun Content( contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) ) { items( - count = previewState.assetUriList.size, + count = previewState.assetBundleList.size, ) { index -> Box( modifier = Modifier @@ -283,7 +281,7 @@ private fun Content( modifier = Modifier .size(dimensions().spacing64x) .align(Alignment.BottomStart), - asset = previewState.assetUriList[index], + asset = previewState.assetBundleList[index], isSelected = previewState.selectedIndex == index, onClick = { onSelected(index) } ) 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 d534d773363..b6c777cf64c 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 @@ -25,6 +25,6 @@ import kotlinx.collections.immutable.persistentListOf data class ImagesPreviewState( val conversationId: ConversationId, val conversationName: String, - val assetUriList: PersistentList = persistentListOf(), + val assetBundleList: PersistentList = persistentListOf(), val selectedIndex: Int = 0 ) 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 59e3e70e6e0..52a89bb5168 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 @@ -28,7 +28,6 @@ import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.navArgs import com.wire.android.ui.sharing.ImportedMediaAsset import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.kalium.logic.data.asset.KaliumFileSystem import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch @@ -63,7 +62,7 @@ class ImagesPreviewViewModel @Inject constructor( viewModelScope.launch { val assets = navArgs.assetUriList.map { handleImportedAsset(it) } viewState = viewState.copy( - assetUriList = assets.filterNotNull().toPersistentList() + assetBundleList = assets.filterNotNull().toPersistentList() ) } } 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 8b62c81f467..cf9fe79731a 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 @@ -187,6 +187,10 @@ class SendMessageViewModel @Inject constructor( } is ComposableMessageBundle.AttachmentPickedBundle -> { + sendAttachment(messageBundle.assetBundle, messageBundle.conversationId) + } + + is ComposableMessageBundle.UriPickedBundle -> { handleAssetMessageBundle( attachmentUri = messageBundle.attachmentUri, conversationId = messageBundle.conversationId @@ -259,6 +263,7 @@ class SendMessageViewModel @Inject constructor( } is HandleUriAssetUseCase.Result.Success -> { + println("KBX attachment ${result.assetBundle.assetType} ${result.assetBundle.dataPath}") sendAttachment(result.assetBundle, conversationId) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt index 551f150b357..dd83a858d53 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposer.kt @@ -138,7 +138,7 @@ fun MessageComposer( }, onPingOptionClicked = { onSendMessageBundle(Ping(conversationId)) }, onImagesPicked = onImagesPicked, - onAttachmentPicked = { onSendMessageBundle(ComposableMessageBundle.AttachmentPickedBundle(conversationId, it)) }, + onAttachmentPicked = { onSendMessageBundle(ComposableMessageBundle.UriPickedBundle(conversationId, it)) }, onAudioRecorded = { onSendMessageBundle(ComposableMessageBundle.AudioMessageBundle(conversationId, it)) }, onLocationPicked = { onSendMessageBundle( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt index dbaecd99f2c..c9cf99702ab 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.messagecomposer.model import android.location.Location +import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.model.UIMention import com.wire.android.ui.home.conversations.model.UriAsset import com.wire.kalium.logic.data.id.ConversationId @@ -40,6 +41,11 @@ sealed class ComposableMessageBundle(override val conversationId: ConversationId ) : ComposableMessageBundle(conversationId) data class AttachmentPickedBundle( + override val conversationId: ConversationId, + val assetBundle: AssetBundle + ) : ComposableMessageBundle(conversationId) + + data class UriPickedBundle( override val conversationId: ConversationId, val attachmentUri: UriAsset ) : ComposableMessageBundle(conversationId) 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 112b2fbeff8..94b36c45a26 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 @@ -296,7 +296,7 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( handleImportedAsset(uri)?.let { importedAsset -> if (importedAsset.assetSizeExceeded != null) { onSnackbarMessage( - SendMessagesSnackbarMessages.MaxAssetSizeExceeded(importedAsset.assetSizeExceeded!!) + SendMessagesSnackbarMessages.MaxAssetSizeExceeded(importedAsset.assetSizeExceeded) ) } importMediaState = importMediaState.copy(importedAssets = persistentListOf(importedAsset)) 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 4421f036e41..ee196c78149 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 @@ -17,6 +17,7 @@ */ package com.wire.android.ui.sharing +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxSize 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.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider @@ -68,12 +70,15 @@ import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.media.preview.AssetPreview +import com.wire.android.ui.home.conversations.model.UriAsset 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.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.messagecomposer.model.MessageBundle import com.wire.android.ui.home.newconversation.common.SendContentButton import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel import com.wire.android.ui.theme.wireTypography @@ -122,7 +127,13 @@ fun ImportMediaScreen( onSearchQueryChanged = importMediaViewModel::onSearchQueryChanged, onConversationClicked = importMediaViewModel::onConversationClicked, checkRestrictionsAndSendImportedMedia = { - // TODO KBX + sendMessageViewModel.trySendMessages(importMediaViewModel.importMediaState.importedAssets.map { + ComposableMessageBundle.AttachmentPickedBundle( + importMediaViewModel.importMediaState.selectedConversationItem.first().conversationId, + it.assetBundle + ) + }) + // TODO KBX // importMediaViewModel.checkRestrictionsAndSendImportedMedia { // navigator.navigate( // NavigationCommand( @@ -371,7 +382,7 @@ private fun ImportMediaContent( if (state.isImporting) { Box( Modifier - .height(dimensions().spacing100x) + .height(dimensions().spacing120x) .fillMaxWidth() .align(Alignment.CenterHorizontally) ) { @@ -382,12 +393,16 @@ private fun ImportMediaContent( ) } } else if (!isMultipleImport) { - Box(modifier = Modifier.padding(horizontal = dimensions().spacing16x)) { + Box(modifier = Modifier + .padding(horizontal = dimensions().spacing16x) + .height(dimensions().spacing120x)) { AssetPreview(asset = importedItemsList.first(), onClick = {}) } } else { LazyRow( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .height(dimensions().spacing120x), horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) ) { @@ -395,6 +410,7 @@ private fun ImportMediaContent( count = importedItemsList.size, ) { index -> AssetPreview( + modifier = Modifier.width(dimensions().spacing120x), asset = importedItemsList[index], onClick = {} ) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index a4883ec6252..e7a4efe99f8 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -149,6 +149,7 @@ data class WireDimensions( val spacing72x: Dp, val spacing80x: Dp, val spacing100x: Dp, + val spacing120x: Dp, val spacing200x: Dp, // Corners val corner2x: Dp, @@ -300,6 +301,7 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( spacing72x = 72.dp, spacing80x = 80.dp, spacing100x = 100.dp, + spacing120x = 120.dp, spacing200x = 200.dp, corner2x = 2.dp, corner4x = 4.dp, From 2891c0ffffb8817cb3cbcb46c458ba25426ac20f Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 14 May 2024 12:17:28 +0200 Subject: [PATCH 05/12] feat: file preview --- .../home/conversations/AssetTooLargeDialog.kt | 85 +++++--- .../conversations/MessageComposerViewState.kt | 3 +- .../media/preview/AssetFilePreview.kt | 117 +++++++++++ .../media/preview/AssetTilePreview.kt | 187 ++++++++++++++++++ .../media/preview/ImagesPreviewScreen.kt | 178 ++++++++++------- .../media/preview/ImagesPreviewViewModel.kt | 4 + .../messages/item/MessageTypesPreview.kt | 36 +--- .../messages/item/RegularMessageItem.kt | 7 +- .../home/conversations/model/AssetBundle.kt | 21 +- .../home/conversations/model/MessageTypes.kt | 23 --- .../messagetypes/asset/AssetMessageTypes.kt | 52 +++-- .../sendmessage/SendMessageViewModel.kt | 6 +- .../home/messagecomposer/AttachmentOptions.kt | 26 ++- .../android/ui/sharing/ImportMediaScreen.kt | 155 +++++++++++---- .../sharing/SendMessagesSnackbarMessages.kt | 4 +- .../permission/OpenFileBrowserRequestFlow.kt | 15 +- app/src/main/res/drawable/ic_attention.xml | 10 + app/src/main/res/values/strings.xml | 7 + .../sendmessage/SendMessageViewModelTest.kt | 16 +- 19 files changed, 709 insertions(+), 243 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt create mode 100644 app/src/main/res/drawable/ic_attention.xml 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 61dcb9f3a98..cc69292eb7a 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 @@ -29,42 +29,77 @@ import com.wire.kalium.logic.data.asset.AttachmentType @Composable fun AssetTooLargeDialog(dialogState: AssetTooLargeDialogState, hideDialog: () -> Unit) { - if (dialogState is AssetTooLargeDialogState.Visible) { - WireDialog( - title = getTitle(dialogState), - text = getLabel(dialogState), - buttonsHorizontalAlignment = false, - onDismiss = hideDialog, - optionButton1Properties = WireDialogButtonProperties( - text = stringResource(R.string.label_ok), - type = WireDialogButtonType.Primary, - onClick = hideDialog + when (dialogState) { + is AssetTooLargeDialogState.SingleVisible -> { + WireDialog( + title = getTitle(dialogState), + text = getLabel(dialogState), + buttonsHorizontalAlignment = false, + onDismiss = hideDialog, + optionButton1Properties = WireDialogButtonProperties( + text = stringResource(R.string.label_ok), + type = WireDialogButtonType.Primary, + onClick = hideDialog + ) ) - ) + } + + is AssetTooLargeDialogState.MultipleVisible -> { + WireDialog( + title = getTitle(dialogState), + text = getLabel(dialogState), + buttonsHorizontalAlignment = false, + onDismiss = hideDialog, + optionButton1Properties = WireDialogButtonProperties( + text = stringResource(R.string.label_ok), + type = WireDialogButtonType.Primary, + onClick = hideDialog + ) + ) + } + + AssetTooLargeDialogState.Hidden -> { + + } } } @Composable -private fun getTitle(dialogState: AssetTooLargeDialogState.Visible) = 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) +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) + } } @Composable -private fun getLabel(dialogState: 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 +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) { + 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 + } } + @Preview @Composable fun PreviewAssetTooLargeDialog() { - AssetTooLargeDialog(AssetTooLargeDialogState.Visible(AttachmentType.VIDEO, 100, true)) {} + AssetTooLargeDialog(AssetTooLargeDialogState.SingleVisible(AttachmentType.VIDEO, 100, true)) {} +} + +@Preview +@Composable +fun PreviewMultipleAssetTooLargeDialog() { + AssetTooLargeDialog(AssetTooLargeDialogState.MultipleVisible(20)) {} } 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 b1c2c4be36d..4e043e5c387 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,7 +36,8 @@ data class MessageComposerViewState( sealed class AssetTooLargeDialogState { data object Hidden : AssetTooLargeDialogState() - data class Visible(val assetType: AttachmentType, val maxLimitInMB: Int, val savedToDevice: Boolean) : AssetTooLargeDialogState() + data class SingleVisible(val assetType: AttachmentType, val maxLimitInMB: Int, val savedToDevice: Boolean) : AssetTooLargeDialogState() + data class MultipleVisible(val maxLimitInMB: Int) : AssetTooLargeDialogState() } sealed class VisitLinkDialogState { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt new file mode 100644 index 00000000000..1ca0682028d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt @@ -0,0 +1,117 @@ +/* + * 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.preview + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.wire.android.R +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.PreviewMultipleThemes +import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireTypography +import java.util.Locale + +@Composable +fun AssetFilePreview( + modifier: Modifier = Modifier, + assetName: String, + sizeInBytes: Long, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .fillMaxSize() + .padding(horizontal = dimensions().spacing32x) + ) { + Box(contentAlignment = Alignment.BottomCenter) { + Icon( + modifier = Modifier.size(dimensions().spacing80x), + painter = painterResource(id = R.drawable.ic_file), + contentDescription = assetName, + tint = MaterialTheme.wireColorScheme.secondaryText + ) + Text( + modifier = Modifier.padding(bottom = dimensions().spacing8x), + text = assetName.split(".").last().uppercase(Locale.getDefault()), + style = MaterialTheme.wireTypography.title01.copy( + fontWeight = FontWeight.W900, + color = MaterialTheme.colorScheme.onPrimary + ) + ) + } + VerticalSpace.x16() + Text( + assetName, + style = MaterialTheme.wireTypography.title02.copy(color = MaterialTheme.colorScheme.onBackground), + textAlign = TextAlign.Center, + maxLines = 2, overflow = TextOverflow.Ellipsis + ) + VerticalSpace.x8() + Text(sizeInBytes.toFileSize(), style = MaterialTheme.wireTypography.body01.copy(MaterialTheme.wireColorScheme.secondaryText)) + } +} + +private fun Long.toFileSize(): String { + val kilobyte = 1024.0 + val megabyte = kilobyte * 1024 + val gigabyte = megabyte * 1024 + + return when { + this < kilobyte -> "$this B" + this < megabyte -> String.format("%.2f KB", this / kilobyte) + this < gigabyte -> String.format("%.2f MB", this / megabyte) + else -> String.format("%.2f GB", this / gigabyte) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewWireImage() { + Box( + modifier = Modifier + .width(400.dp) + .height(800.dp) + ) { + WireTheme { + AssetFilePreview( + assetName = "very long file naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaame.png", + sizeInBytes = 1500 + ) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt new file mode 100644 index 00000000000..13df42363cb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt @@ -0,0 +1,187 @@ +/* + * 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.preview + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import com.wire.android.R +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.image.WireImage +import com.wire.android.ui.common.spacers.HorizontalSpace +import com.wire.android.ui.home.conversations.model.AssetBundle +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.data.asset.AttachmentType +import okio.Path.Companion.toPath +import java.util.Locale + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AssetTilePreview( + modifier: Modifier = Modifier, + assetBundle: AssetBundle, + isSelected: Boolean = false, + showOnlyExtension: Boolean, + onClick: () -> Unit +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(shape = RoundedCornerShape(dimensions().messageAssetBorderRadius)) + .background( + color = MaterialTheme.wireColorScheme.onPrimary, + shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) + ) + .border( + width = if (isSelected) { + dimensions().spacing2x + } else { + dimensions().spacing1x + }, + color = if (isSelected) { + MaterialTheme.wireColorScheme.primary + } else { + MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline + }, + shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) + ) + .combinedClickable(onClick = onClick) + ) { + when (assetBundle.assetType) { + AttachmentType.IMAGE -> WireImage( + modifier = Modifier.fillMaxSize(), + model = assetBundle.dataPath.toFile(), + contentScale = ContentScale.Crop, + contentDescription = assetBundle.fileName + ) + + AttachmentType.GENERIC_FILE, + AttachmentType.AUDIO, + AttachmentType.VIDEO -> if (showOnlyExtension) { + AssetExtensionPreviewTile(assetBundle.assetName) + } else { + AssetFilePreviewTile(assetBundle, Modifier.fillMaxSize()) + } + } + } +} + +@Composable +fun AssetExtensionPreviewTile(assetName: String) { + Text( + text = assetName.split(".").last().uppercase(Locale.getDefault()), + style = MaterialTheme.wireTypography.title01.copy( + fontWeight = FontWeight.W900, + color = MaterialTheme.wireColorScheme.secondaryText + ) + ) +} + +@Composable +fun AssetFilePreviewTile(assetBundle: AssetBundle, modifier: Modifier = Modifier) { + Column(modifier = modifier.padding(dimensions().spacing8x)) { + Text( + modifier = Modifier.weight(1F), + text = assetBundle.assetName, + style = MaterialTheme.wireTypography.body02, + maxLines = 4, + overflow = TextOverflow.Ellipsis + ) + Row { + Image( + modifier = Modifier, + painter = painterResource(R.drawable.ic_file), + contentDescription = stringResource(R.string.content_description_image_message), + colorFilter = ColorFilter.tint(MaterialTheme.wireColorScheme.badge) + ) + HorizontalSpace.x4() + Text( + text = assetBundle.extensionWithSize, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.wireColorScheme.secondaryText, + fontSize = 12.sp, + style = MaterialTheme.wireTypography.subline01 + ) + } + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewAssetTileItemPreview() { + WireTheme { + AssetFilePreviewTile( + AssetBundle( + "key", + "file/pdf", + dataPath = "some-data-path".toPath(), + 20_000, + "long naaaaaaaaaaaaaaaaaaaaaaaaaaaaame document.pdf", + AttachmentType.GENERIC_FILE + ), + modifier = Modifier + .height(dimensions().spacing120x) + .width(dimensions().spacing120x) + ) + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewAssetTileFullWidthPreview() { + WireTheme { + AssetFilePreviewTile( + AssetBundle( + "key", + "file/pdf", + dataPath = "some-data-path".toPath(), + 20_000, + "long naaaaaaaaaaaaaaaaaaaaaaaaaaaaame document.pdf", + AttachmentType.GENERIC_FILE + ), + modifier = Modifier.height(dimensions().spacing120x) + ) + } +} 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 31af5c48f55..05545183980 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.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -29,7 +28,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -45,11 +43,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -95,6 +93,7 @@ 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 @RootNavGraph @@ -134,7 +133,8 @@ fun ImagesPreviewScreen( sendState = sendMessageViewModel.viewState, onNavigationPressed = { navigator.navigateBack() }, onSendMessages = sendMessageViewModel::trySendMessages, - onSelected = imagesPreviewViewModel::onSelected + onSelected = imagesPreviewViewModel::onSelected, + onRemoveAsset = imagesPreviewViewModel::onRemove ) AssetTooLargeDialog( @@ -165,19 +165,23 @@ private fun Content( sendState: SendMessageState, onNavigationPressed: () -> Unit = {}, onSendMessages: (List) -> Unit, - onSelected: (index: Int) -> Unit + onSelected: (index: Int) -> Unit, + onRemoveAsset: (index: Int) -> Unit ) { val configuration = LocalConfiguration.current val pagerState = rememberPagerState(pageCount = { previewState.assetBundleList.size }) + val scope = rememberCoroutineScope() LaunchedEffect(key1 = previewState.selectedIndex) { - if (previewState.selectedIndex != pagerState.currentPage) { - pagerState.animateScrollToPage(previewState.selectedIndex) + if (previewState.selectedIndex != pagerState.settledPage) { + scope.launch { + pagerState.animateScrollToPage(previewState.selectedIndex) + } } } - LaunchedEffect(key1 = pagerState.currentPage) { - if (previewState.selectedIndex != pagerState.currentPage) { - onSelected(pagerState.currentPage) + LaunchedEffect(key1 = pagerState.settledPage) { + if (previewState.selectedIndex != pagerState.settledPage) { + onSelected(pagerState.settledPage) } } @@ -251,22 +255,33 @@ private fun Content( .width(configuration.screenWidthDp.dp) .fillMaxHeight(), ) { index -> - WireImage( - modifier = Modifier - .width(configuration.screenWidthDp.dp) - .fillMaxHeight(), - model = previewState.assetBundleList[index].assetBundle.dataPath.toFile(), - contentDescription = previewState.assetBundleList[index].assetBundle.fileName - ) + val assetBundle = previewState.assetBundleList[index].assetBundle + + when (assetBundle.assetType) { + AttachmentType.IMAGE -> WireImage( + modifier = Modifier + .width(configuration.screenWidthDp.dp) + .fillMaxHeight(), + model = previewState.assetBundleList[index].assetBundle.dataPath.toFile(), + contentDescription = previewState.assetBundleList[index].assetBundle.fileName + ) + + AttachmentType.GENERIC_FILE, + AttachmentType.AUDIO, + AttachmentType.VIDEO -> AssetFilePreview( + assetName = assetBundle.fileName, + sizeInBytes = assetBundle.dataSize + ) + } } } LazyRow( modifier = Modifier .padding(bottom = dimensions().spacing8x) - .height(dimensions().spacing72x) + .height(dimensions().spacing80x) .align(Alignment.BottomCenter), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing4x), contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) ) { items( @@ -274,18 +289,27 @@ private fun Content( ) { index -> Box( modifier = Modifier - .width(dimensions().spacing72x) + .width(dimensions().spacing80x) .fillMaxHeight() ) { - AssetPreview( + AssetTilePreview( modifier = Modifier .size(dimensions().spacing64x) - .align(Alignment.BottomStart), - asset = previewState.assetBundleList[index], + .align(Alignment.Center), + assetBundle = previewState.assetBundleList[index].assetBundle, isSelected = previewState.selectedIndex == index, + showOnlyExtension = true, onClick = { onSelected(index) } ) - RemoveAssetButton(modifier = Modifier.align(Alignment.TopEnd), onClick = {}) + + if (previewState.assetBundleList.size > 1) { + RemoveAssetButton(modifier = Modifier.align(Alignment.TopEnd), onClick = { + onRemoveAsset(index) + }) + } + if (previewState.assetBundleList[index].assetSizeExceeded != null) { + ErrorIcon(modifier = Modifier.align(Alignment.Center)) + } } } } @@ -293,48 +317,6 @@ private fun Content( } } -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun AssetPreview( - modifier: Modifier = Modifier, - asset: ImportedMediaAsset, - isSelected: Boolean = false, - onClick: () -> Unit -) { - Box( - modifier - .clip(shape = RoundedCornerShape(dimensions().messageAssetBorderRadius)) - .background( - color = MaterialTheme.wireColorScheme.onPrimary, - shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) - ) - .border( - width = if (isSelected) { - dimensions().spacing2x - } else { - dimensions().spacing1x - }, - color = if (isSelected) { - MaterialTheme.wireColorScheme.primary - } else { - MaterialTheme.wireColorScheme.secondaryButtonDisabledOutline - }, - shape = RoundedCornerShape(dimensions().messageAssetBorderRadius) - ) - .combinedClickable( - onClick = onClick, - onLongClick = {}, - ) - ) { - WireImage( - modifier = Modifier.fillMaxSize(), - model = asset.assetBundle.dataPath.toFile(), - contentScale = ContentScale.Crop, - contentDescription = asset.assetBundle.fileName - ) - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun RemoveAssetButton( @@ -343,15 +325,32 @@ fun RemoveAssetButton( ) { 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 = "test", + 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 @@ -374,25 +373,60 @@ fun PreviewImagesPreviewScreen() { Content( previewState = ImagesPreviewState( ConversationId("value", "domain"), - "Conversation", - persistentListOf( + selectedIndex = 0, + conversationName = "Conversation", + assetBundleList = persistentListOf( ImportedMediaAsset( AssetBundle( "key", "image/png", "".toPath(), 20, - "preview", + "preview.png", assetType = AttachmentType.IMAGE ), assetSizeExceeded = null + ), + ImportedMediaAsset( + AssetBundle( + "key1", + "video/mp4", + "".toPath(), + 20, + "preview.mp4", + assetType = AttachmentType.VIDEO + ), + assetSizeExceeded = null + ), + ImportedMediaAsset( + AssetBundle( + "key2", + "audio/mp3", + "".toPath(), + 20, + "preview.mp3", + assetType = AttachmentType.AUDIO + ), + assetSizeExceeded = 20 + ), + ImportedMediaAsset( + AssetBundle( + "key3", + "document/pdf", + "".toPath(), + 20, + "preview.pdf", + assetType = AttachmentType.GENERIC_FILE + ), + assetSizeExceeded = null ) ), ), - sendState = SendMessageState(inProgress = false), + sendState = SendMessageState(inProgress = true), onNavigationPressed = {}, onSendMessages = {}, - onSelected = {} + onSelected = {}, + onRemoveAsset = {} ) } } 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 52a89bb5168..337101f6f6f 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 @@ -58,6 +58,10 @@ class ImagesPreviewViewModel @Inject constructor( viewState = viewState.copy(selectedIndex = index) } + fun onRemove(index: Int) { + viewState = viewState.copy(assetBundleList = viewState.assetBundleList.removeAt(index)) + } + private fun handleAssets() { viewModelScope.launch { val assets = navArgs.assetUriList.map { handleImportedAsset(it) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageTypesPreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageTypesPreview.kt index 646a14e002f..d9921f68990 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageTypesPreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageTypesPreview.kt @@ -38,10 +38,10 @@ import com.wire.android.ui.home.conversations.mock.mockedImageUIMessage import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.MessageBody import com.wire.android.ui.home.conversations.model.MessageFlowStatus -import com.wire.android.ui.home.conversations.model.MessageGenericAsset import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage +import com.wire.android.ui.home.conversations.model.messagetypes.asset.MessageAsset import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText @@ -266,30 +266,12 @@ fun PreviewAssetMessageWithReactions() { @Composable fun PreviewImportedMediaAssetMessageContent() { WireTheme { - MessageGenericAsset( + MessageAsset( assetName = "Some test cool long but very cool long but very asjkl cool long but very long message", assetExtension = "rar.tgz", assetSizeInBytes = 99201224L, onAssetClick = Clickable(enabled = false), - assetTransferStatus = AssetTransferStatus.NOT_DOWNLOADED, - shouldFillMaxWidth = false, - isImportedMediaAsset = true - ) - } -} - -@PreviewMultipleThemes -@Composable -fun PreviewWideImportedAssetMessageContent() { - WireTheme { - MessageGenericAsset( - assetName = "Some test cool long but very cool long but very asjkl cool long but very long message", - assetExtension = "rar.tgz", - assetSizeInBytes = 99201224L, - onAssetClick = Clickable(enabled = false), - assetTransferStatus = AssetTransferStatus.NOT_DOWNLOADED, - shouldFillMaxWidth = true, - isImportedMediaAsset = true + assetTransferStatus = AssetTransferStatus.NOT_DOWNLOADED ) } } @@ -298,14 +280,12 @@ fun PreviewWideImportedAssetMessageContent() { @Composable fun PreviewLoadingAssetMessage() { WireTheme { - MessageGenericAsset( + MessageAsset( assetName = "Some test cool long but very cool long but very asjkl cool long but very long message", assetExtension = "rar.tgz", assetSizeInBytes = 99201224L, onAssetClick = Clickable(enabled = false), - assetTransferStatus = AssetTransferStatus.DOWNLOAD_IN_PROGRESS, - shouldFillMaxWidth = true, - isImportedMediaAsset = false + assetTransferStatus = AssetTransferStatus.DOWNLOAD_IN_PROGRESS ) } } @@ -314,14 +294,12 @@ fun PreviewLoadingAssetMessage() { @Composable fun PreviewFailedDownloadAssetMessage() { WireTheme { - MessageGenericAsset( + MessageAsset( assetName = "Some test cool long but very cool long but very asjkl cool long but very long message", assetExtension = "rar.tgz", assetSizeInBytes = 99201224L, onAssetClick = Clickable(enabled = false), - assetTransferStatus = AssetTransferStatus.FAILED_DOWNLOAD, - shouldFillMaxWidth = true, - isImportedMediaAsset = false + assetTransferStatus = AssetTransferStatus.FAILED_DOWNLOAD ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index 5758e4601b0..fd02e3cb93c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -79,7 +79,6 @@ import com.wire.android.ui.home.conversations.model.DeliveryStatusContent import com.wire.android.ui.home.conversations.model.MessageBody import com.wire.android.ui.home.conversations.model.MessageFlowStatus import com.wire.android.ui.home.conversations.model.MessageFooter -import com.wire.android.ui.home.conversations.model.MessageGenericAsset import com.wire.android.ui.home.conversations.model.MessageHeader import com.wire.android.ui.home.conversations.model.MessageImage import com.wire.android.ui.home.conversations.model.MessageSource @@ -88,6 +87,7 @@ 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.wire.android.ui.home.conversations.model.UIQuotedMessage +import com.wire.android.ui.home.conversations.model.messagetypes.asset.MessageAsset import com.wire.android.ui.home.conversations.model.messagetypes.asset.RestrictedAssetMessage import com.wire.android.ui.home.conversations.model.messagetypes.asset.RestrictedGenericFileMessage import com.wire.android.ui.home.conversations.model.messagetypes.audio.AudioMessage @@ -312,7 +312,8 @@ private fun SwipableToReplyBox( enableDismissFromEndToStart = false, backgroundContent = { Row( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .drawBehind { // TODO(RTL): Might need adjusting once RTL is supported (also lacking in SwipeToDismissBox) drawRect( @@ -645,7 +646,7 @@ private fun MessageContent( is UIMessageContent.AssetMessage -> { Column { - MessageGenericAsset( + MessageAsset( assetName = messageContent.assetName, assetExtension = messageContent.assetExtension, assetSizeInBytes = messageContent.assetSizeInBytes, 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 ab9e502496b..8f667381946 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 @@ -19,8 +19,10 @@ 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 okio.Path +import kotlin.math.roundToInt /** * Represents a set of metadata information of an asset message @@ -32,7 +34,24 @@ data class AssetBundle( val dataSize: Long, val fileName: String, val assetType: AttachmentType -) +) { + + @Stable + val extensionWithSize: String + get() { + val assetExtension = fileName.split(".").last() + val oneKB = 1024L + val oneMB = oneKB * oneKB + return when { + dataSize < oneKB -> "${assetExtension.uppercase()} ($dataSize B)" + dataSize in oneKB..oneMB -> "${assetExtension.uppercase()} (${dataSize / oneKB} KB)" + else -> "${assetExtension.uppercase()} (${((dataSize / oneMB) * 100.0).roundToInt() / 100.0} MB)" // 2 decimals round off + } + } + + val assetName: String + get() = fileName.split(".").first() +} /** * @param uri Uri of the asset diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt index af415610a2a..c57400b929a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt @@ -46,13 +46,11 @@ import com.wire.android.ui.common.clickable import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.CompositeMessageViewModel import com.wire.android.ui.home.conversations.CompositeMessageViewModelImpl -import com.wire.android.ui.home.conversations.model.messagetypes.asset.MessageAsset import com.wire.android.ui.home.conversations.model.messagetypes.image.AsyncImageMessage import com.wire.android.ui.home.conversations.model.messagetypes.image.DisplayableImageMessage import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageFailed import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageInProgress import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageParams -import com.wire.android.ui.home.conversations.model.messagetypes.image.ImportedImageMessage import com.wire.android.ui.markdown.DisplayMention import com.wire.android.ui.markdown.MarkdownConstants.MENTION_MARK import com.wire.android.ui.markdown.MarkdownDocument @@ -286,27 +284,6 @@ fun MediaAssetImage( } } -@Composable -internal fun MessageGenericAsset( - assetName: String, - assetExtension: String, - assetSizeInBytes: Long, - onAssetClick: Clickable, - assetTransferStatus: AssetTransferStatus, - shouldFillMaxWidth: Boolean = true, - isImportedMediaAsset: Boolean = false -) { - MessageAsset( - assetName, - assetExtension, - assetSizeInBytes, - onAssetClick, - assetTransferStatus, - shouldFillMaxWidth, - isImportedMediaAsset - ) -} - /** * Maps all mentions to DisplayMention in order to find them easier after converting * to markdown document as positions changes due to markdown characters. diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt index 058634c7d7f..967d6be8807 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt @@ -69,9 +69,7 @@ internal fun MessageAsset( assetExtension: String, assetSizeInBytes: Long, onAssetClick: Clickable, - assetTransferStatus: AssetTransferStatus, - shouldFillMaxWidth: Boolean, - isImportedMediaAsset: Boolean + assetTransferStatus: AssetTransferStatus ) { val assetDescription = provideAssetDescription(assetExtension, assetSizeInBytes) Box( @@ -91,25 +89,21 @@ internal fun MessageAsset( if (assetTransferStatus == AssetTransferStatus.UPLOAD_IN_PROGRESS) { UploadInProgressAssetMessage() } else { - val assetModifier = if (shouldFillMaxWidth) Modifier + val assetModifier = Modifier .align(Alignment.Center) .fillMaxWidth() - else Modifier - .size(dimensions().importedMediaAssetSize) - .align(Alignment.Center) - .fillMaxSize() Column(modifier = assetModifier.padding(dimensions().spacing8x)) { Text( text = assetName, style = MaterialTheme.wireTypography.body02, fontSize = 15.sp, - maxLines = if (shouldFillMaxWidth) 2 else 4, + maxLines = 2, overflow = TextOverflow.Ellipsis ) val descriptionModifier = Modifier .padding(top = dimensions().spacing8x) .fillMaxWidth() - ConstraintLayout(modifier = (if (!shouldFillMaxWidth) descriptionModifier.fillMaxHeight() else descriptionModifier)) { + ConstraintLayout(modifier = descriptionModifier) { val (icon, description, downloadStatus) = createRefs() Image( modifier = Modifier @@ -137,26 +131,24 @@ internal fun MessageAsset( fontSize = 12.sp, style = MaterialTheme.wireTypography.subline01 ) - if (!isImportedMediaAsset) { - Row( - modifier = Modifier - .wrapContentWidth() - .constrainAs(downloadStatus) { - top.linkTo(parent.top) - end.linkTo(parent.end) - bottom.linkTo(parent.bottom) - } - ) { - Text( - modifier = Modifier.padding(end = dimensions().spacing4x), - text = getDownloadStatusText(assetTransferStatus), - color = MaterialTheme.wireColorScheme.run { - if (assetTransferStatus.isFailed()) error else secondaryText - }, - style = MaterialTheme.wireTypography.subline01 - ) - DownloadStatusIcon(assetTransferStatus) - } + Row( + modifier = Modifier + .wrapContentWidth() + .constrainAs(downloadStatus) { + top.linkTo(parent.top) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + } + ) { + Text( + modifier = Modifier.padding(end = dimensions().spacing4x), + text = getDownloadStatusText(assetTransferStatus), + color = MaterialTheme.wireColorScheme.run { + if (assetTransferStatus.isFailed()) error else secondaryText + }, + style = MaterialTheme.wireTypography.subline01 + ) + DownloadStatusIcon(assetTransferStatus) } } } 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 cf9fe79731a..9671082ef03 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 @@ -125,7 +125,7 @@ class SendMessageViewModel @Inject constructor( fun trySendMessages(messageBundleList: List) { if (messageBundleList.size > MAX_LIMIT_MESSAGE_SEND) { - onSnackbarMessage(SendMessagesSnackbarMessages.MaxAmountOfAssetsReached) + onSnackbarMessage(SendMessagesSnackbarMessages.MaxAmountOfAssetsReached(MAX_LIMIT_MESSAGE_SEND)) } else { val messageBundleMap = messageBundleList.groupBy { it.conversationId } messageBundleMap.forEach { (conversationId, bundles) -> @@ -151,6 +151,7 @@ class SendMessageViewModel @Inject constructor( private suspend fun sendMessages(messageBundleList: List) { val jobs: MutableCollection = mutableListOf() + beforeSendingMessage() messageBundleList.forEach { val job = viewModelScope.launch { sendMessage(it) @@ -251,7 +252,7 @@ class SendMessageViewModel @Inject constructor( audioPath = audioPath )) { is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> { - assetTooLargeDialogState = AssetTooLargeDialogState.Visible( + assetTooLargeDialogState = AssetTooLargeDialogState.SingleVisible( assetType = result.assetBundle.assetType, maxLimitInMB = result.maxLimitInMB, savedToDevice = attachmentUri.saveToDeviceIfInvalid @@ -263,7 +264,6 @@ class SendMessageViewModel @Inject constructor( } is HandleUriAssetUseCase.Result.Success -> { - println("KBX attachment ${result.assetBundle.assetType} ${result.assetBundle.dataPath}") sendAttachment(result.assetBundle, conversationId) } } 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 451adbd8ff3..fb4aa4e7b35 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 @@ -161,7 +161,27 @@ fun FileBrowserFlow( onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit ): UseStorageRequestFlow { return rememberOpenFileBrowserFlow( - onFileBrowserItemPicked = onFilePicked, + contract = ActivityResultContracts.GetContent(), + onFileBrowserItemPicked = { uri -> + uri?.let(onFilePicked) + }, + onPermissionDenied = { /* Nothing to do */ }, + onPermissionPermanentlyDenied = onPermissionPermanentlyDenied + ) +} + +@Composable +fun MultipleFileBrowserFlow( + onFilesPicked: (List) -> Unit, + onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit +): UseStorageRequestFlow> { + return rememberOpenFileBrowserFlow( + contract = ActivityResultContracts.GetMultipleContents(), + onFileBrowserItemPicked = { uris -> + if (uris.isNotEmpty()) { + onFilesPicked(uris) + } + }, onPermissionDenied = { /* Nothing to do */ }, onPermissionPermanentlyDenied = onPermissionPermanentlyDenied ) @@ -233,8 +253,8 @@ private fun buildAttachmentOptionItems( onLocationPickerClicked: () -> Unit, onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit ): List { - val fileFlow = FileBrowserFlow( - remember { { onFilePicked(UriAsset(it, false)) } }, + val fileFlow = MultipleFileBrowserFlow( + remember { { onImagesPicked(it) } }, onPermissionPermanentlyDenied ) val galleryFlow = MultipleGalleryFlow( 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 ee196c78149..da0e7fd0f53 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 @@ -17,15 +17,14 @@ */ package com.wire.android.ui.sharing -import android.net.Uri import androidx.activity.compose.BackHandler -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -56,6 +55,8 @@ import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.SnackBarMessage import com.wire.android.model.UserAvatarData +import com.wire.android.navigation.BackStackMode +import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.common.UserProfileAvatar import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout @@ -68,30 +69,36 @@ 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.media.preview.AssetPreview -import com.wire.android.ui.home.conversations.model.UriAsset +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.messagecomposer.model.MessageBundle import com.wire.android.ui.home.newconversation.common.SendContentButton import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel +import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper import com.wire.android.util.extension.getActivity import com.wire.android.util.ui.LinkText import com.wire.android.util.ui.LinkTextData +import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.util.isPositiveNotNull import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import okio.Path.Companion.toPath @RootNavGraph @Destination @@ -122,6 +129,28 @@ fun ImportMediaScreen( 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 -> { + } + } + } + ImportMediaRegularContent( importMediaAuthenticatedState = importMediaViewModel.importMediaState, onSearchQueryChanged = importMediaViewModel::onSearchQueryChanged, @@ -133,15 +162,6 @@ fun ImportMediaScreen( it.assetBundle ) }) - // TODO KBX -// importMediaViewModel.checkRestrictionsAndSendImportedMedia { -// navigator.navigate( -// NavigationCommand( -// ConversationScreenDestination(it), -// BackStackMode.REMOVE_CURRENT -// ) -// ) -// } }, onNewSelfDeletionTimerPicked = importMediaViewModel::onNewSelfDeletionTimerPicked, infoMessage = importMediaViewModel.infoMessage, @@ -360,7 +380,6 @@ private fun ImportMediaBottomBar( } @Composable -@OptIn(ExperimentalFoundationApi::class) private fun ImportMediaContent( state: ImportMediaAuthenticatedState, internalPadding: PaddingValues, @@ -393,10 +412,16 @@ private fun ImportMediaContent( ) } } else if (!isMultipleImport) { - Box(modifier = Modifier - .padding(horizontal = dimensions().spacing16x) - .height(dimensions().spacing120x)) { - AssetPreview(asset = importedItemsList.first(), onClick = {}) + Box( + modifier = Modifier + .padding(horizontal = dimensions().spacing16x) + .height(dimensions().spacing120x) + ) { + AssetTilePreview( + modifier = Modifier.fillMaxHeight(), + assetBundle = importedItemsList.first().assetBundle, + showOnlyExtension = false, + onClick = {}) } } else { LazyRow( @@ -409,9 +434,12 @@ private fun ImportMediaContent( items( count = importedItemsList.size, ) { index -> - AssetPreview( - modifier = Modifier.width(dimensions().spacing120x), - asset = importedItemsList[index], + AssetTilePreview( + modifier = Modifier + .width(dimensions().spacing120x) + .fillMaxHeight(), + assetBundle = importedItemsList[index].assetBundle, + showOnlyExtension = false, onClick = {} ) } @@ -475,33 +503,88 @@ private fun SnackBarMessage( @Preview(showBackground = true) @Composable fun PreviewImportMediaScreenLoggedOut() { - ImportMediaLoggedOutContent(FeatureFlagState.SharingRestrictedState.NO_USER) {} + WireTheme { + ImportMediaLoggedOutContent(FeatureFlagState.SharingRestrictedState.NO_USER) {} + } } @Preview(showBackground = true) @Composable fun PreviewImportMediaScreenRestricted() { - ImportMediaRestrictedContent( - FeatureFlagState.SharingRestrictedState.RESTRICTED_IN_TEAM, - ImportMediaAuthenticatedState() - ) {} + WireTheme { + ImportMediaRestrictedContent( + FeatureFlagState.SharingRestrictedState.RESTRICTED_IN_TEAM, + ImportMediaAuthenticatedState() + ) {} + } } @Preview(showBackground = true) @Composable fun PreviewImportMediaScreenRegular() { - ImportMediaRegularContent( - ImportMediaAuthenticatedState(), - {}, - {}, - {}, - {}, - MutableSharedFlow() - ) {} + WireTheme { + ImportMediaRegularContent( + ImportMediaAuthenticatedState( + importedAssets = persistentListOf( + ImportedMediaAsset( + AssetBundle( + "key", + "image/png", + "".toPath(), + 20, + "preview.png", + assetType = AttachmentType.IMAGE + ), + assetSizeExceeded = null + ), + ImportedMediaAsset( + AssetBundle( + "key1", + "video/mp4", + "".toPath(), + 20, + "preview.mp4", + assetType = AttachmentType.VIDEO + ), + assetSizeExceeded = null + ), + ImportedMediaAsset( + AssetBundle( + "key2", + "audio/mp3", + "".toPath(), + 24000000, + "preview.mp3", + assetType = AttachmentType.AUDIO + ), + assetSizeExceeded = 20 + ), + ImportedMediaAsset( + AssetBundle( + "key3", + "document/pdf", + "".toPath(), + 20, + "preview.pdf", + assetType = AttachmentType.GENERIC_FILE + ), + assetSizeExceeded = null + ) + ), + ), + {}, + {}, + {}, + {}, + MutableSharedFlow() + ) {} + } } @Preview(showBackground = true) @Composable fun PreviewImportMediaBottomBar() { - ImportMediaBottomBar(ImportMediaAuthenticatedState(), rememberImportMediaScreenState()) {} + WireTheme { + ImportMediaBottomBar(ImportMediaAuthenticatedState(), rememberImportMediaScreenState()) {} + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt index b88a0c1f952..b4e6913b440 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt @@ -21,9 +21,9 @@ import com.wire.android.R import com.wire.android.model.SnackBarMessage import com.wire.android.util.ui.UIText -sealed class SendMessagesSnackbarMessages(override val uiText: UIText) : SnackBarMessage { // TODO KBX +sealed class SendMessagesSnackbarMessages(override val uiText: UIText) : SnackBarMessage { object MaxImageSize : SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_image_size_limit)) - object MaxAmountOfAssetsReached : + class MaxAmountOfAssetsReached(maxAmount: Int) : // TODO add max amount to string resource SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_limit_number_assets_imported_exceeded)) class MaxAssetSizeExceeded(assetSizeLimit: Int) : SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_asset_size_limit, assetSizeLimit)) diff --git a/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt b/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt index 208ced0a0ba..4af716f4c08 100644 --- a/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt +++ b/app/src/main/kotlin/com/wire/android/util/permission/OpenFileBrowserRequestFlow.kt @@ -18,9 +18,9 @@ package com.wire.android.util.permission -import android.net.Uri import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -35,18 +35,19 @@ import com.wire.android.util.extension.getActivity * @param onPermissionDenied action to be executed when the permissions is denied */ @Composable -fun rememberOpenFileBrowserFlow( - onFileBrowserItemPicked: (Uri) -> Unit, +fun rememberOpenFileBrowserFlow( + contract: ActivityResultContract, + onFileBrowserItemPicked: (T) -> Unit, onPermissionDenied: () -> Unit, onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit -): UseStorageRequestFlow { +): UseStorageRequestFlow { val context = LocalContext.current - val openFileBrowserLauncher: ManagedActivityResultLauncher = + val openFileBrowserLauncher: ManagedActivityResultLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() + contract ) { onChosenFileUri -> - onChosenFileUri?.let { onFileBrowserItemPicked(it) } + onFileBrowserItemPicked(onChosenFileUri) } val requestPermissionLauncher: ManagedActivityResultLauncher = diff --git a/app/src/main/res/drawable/ic_attention.xml b/app/src/main/res/drawable/ic_attention.xml new file mode 100644 index 00000000000..7fb97ada215 --- /dev/null +++ b/app/src/main/res/drawable/ic_attention.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac1b79197a6..39d6eed8371 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1216,9 +1216,11 @@ Video could not be sent Image could not be sent File could not be sent + Assets could not be sent 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 The file was saved to your device. Self-deleting message • %1$s @@ -1421,4 +1423,9 @@ Allow Wire to access your device location to send your location. Please wait... Location could not be shared + + + Remove asset + Asset attention + 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 1496dbd1de3..9916f12decf 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 @@ -169,7 +169,7 @@ class SendMessageViewModelTest { .withSuccessfulSendAttachmentMessage() .withHandleUriAsset(HandleUriAssetUseCase.Result.Failure.AssetTooLarge(mockedAttachment, 25)) .arrange() - val mockedMessageBundle = ComposableMessageBundle.AttachmentPickedBundle( + val mockedMessageBundle = ComposableMessageBundle.UriPickedBundle( conversationId = conversationId, attachmentUri = UriAsset("mocked_image.jpeg".toUri(), false) ) @@ -190,7 +190,7 @@ class SendMessageViewModelTest { any() ) } - assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.Visible) + assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.SingleVisible) } @Test @@ -211,7 +211,7 @@ class SendMessageViewModelTest { .withSuccessfulSendAttachmentMessage() .withHandleUriAsset(HandleUriAssetUseCase.Result.Failure.AssetTooLarge(mockedAttachment, limit)) .arrange() - val mockedMessageBundle = ComposableMessageBundle.AttachmentPickedBundle( + val mockedMessageBundle = ComposableMessageBundle.UriPickedBundle( conversationId = conversationId, attachmentUri = UriAsset("mocked_image.jpeg".toUri(), false) ) @@ -232,7 +232,7 @@ class SendMessageViewModelTest { any() ) } - assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.Visible) + assert(viewModel.assetTooLargeDialogState is AssetTooLargeDialogState.SingleVisible) } @Test @@ -244,7 +244,7 @@ class SendMessageViewModelTest { .withSuccessfulSendAttachmentMessage() .withHandleUriAsset(HandleUriAssetUseCase.Result.Failure.Unknown) .arrange() - val mockedMessageBundle = ComposableMessageBundle.AttachmentPickedBundle( + val mockedMessageBundle = ComposableMessageBundle.UriPickedBundle( conversationId = conversationId, attachmentUri = UriAsset("mocked_image.jpeg".toUri(), false) ) @@ -419,7 +419,7 @@ class SendMessageViewModelTest { ) } assertEquals( - SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded(messageBundle), + SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded(conversationId, listOf(messageBundle)), viewModel.sureAboutMessagingDialogState ) } @@ -438,7 +438,7 @@ class SendMessageViewModelTest { // then coVerify(exactly = 0) { arrangement.sendTextMessage.invoke(any(), any(), any(), any()) } assertEquals( - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending(messageBundle), + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending(conversationId, listOf(messageBundle)), viewModel.sureAboutMessagingDialogState ) } @@ -497,7 +497,7 @@ class SendMessageViewModelTest { // then coVerify(exactly = 0) { arrangement.retryFailedMessageUseCase.invoke(eq(messageId), any()) } assertEquals( - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(messageId, conversationId), + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(listOf(messageId), conversationId), viewModel.sureAboutMessagingDialogState ) } From f0bad352e1e730140f62a675f7bb318018a6de2a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 14 May 2024 12:48:13 +0200 Subject: [PATCH 06/12] detekt fix --- .../conversations/model/messagetypes/asset/AssetMessageTypes.kt | 2 -- .../ui/home/conversations/sendmessage/SendMessageState.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt index 967d6be8807..214c81cbaca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt @@ -27,8 +27,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt index 6ba55c765e7..a649eebab95 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt @@ -26,7 +26,6 @@ data class SendMessageState( sealed class SendMessageAction { data object None : SendMessageAction() - data object NavigateBack: SendMessageAction() data class NavigateToConversation(val conversationId: ConversationId) : SendMessageAction() data object NavigateToHome : SendMessageAction() From 61111bc874d6e9f2dc5eef4637e6c35e5e308e76 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 15 May 2024 09:12:04 +0200 Subject: [PATCH 07/12] detekt fix --- .../android/ui/home/conversations/AssetTooLargeDialog.kt | 5 +---- .../ui/home/conversations/MessageComposerViewState.kt | 6 ++++-- .../ui/home/conversations/media/preview/AssetFilePreview.kt | 6 ++++-- .../home/conversations/media/preview/ImagesPreviewScreen.kt | 1 - .../ui/home/conversations/sendmessage/SendMessageState.kt | 2 +- .../home/conversations/sendmessage/SendMessageViewModel.kt | 5 +++-- .../android/ui/sharing/ImportMediaAuthenticatedViewModel.kt | 6 ------ .../kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt | 3 ++- .../wire/android/ui/sharing/SendMessagesSnackbarMessages.kt | 1 - .../conversations/sendmessage/SendMessageViewModelTest.kt | 2 +- 10 files changed, 16 insertions(+), 21 deletions(-) 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 cc69292eb7a..6c91faf6178 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 @@ -58,9 +58,7 @@ fun AssetTooLargeDialog(dialogState: AssetTooLargeDialogState, hideDialog: () -> ) } - AssetTooLargeDialogState.Hidden -> { - - } + AssetTooLargeDialogState.Hidden -> {} } } @@ -91,7 +89,6 @@ private fun getLabel(dialogState: AssetTooLargeDialogState) = when (dialogState) } } - @Preview @Composable fun PreviewAssetTooLargeDialog() { 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 4e043e5c387..2f7b04605e2 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 @@ -54,7 +54,8 @@ sealed class SureAboutMessagingDialogState { data object Hidden : SureAboutMessagingDialogState() sealed class Visible(open val conversationId: ConversationId) : SureAboutMessagingDialogState() { data class ConversationVerificationDegraded( - override val conversationId: ConversationId, val messageBundleListToSend: List + override val conversationId: ConversationId, + val messageBundleListToSend: List ) : Visible(conversationId) sealed class ConversationUnderLegalHold(override val conversationId: ConversationId) : Visible(conversationId) { @@ -64,7 +65,8 @@ sealed class SureAboutMessagingDialogState { ) : ConversationUnderLegalHold(conversationId) data class AfterSending( - val messageIdList: List, override val conversationId: ConversationId + override val conversationId: ConversationId, + val messageIdList: List ) : ConversationUnderLegalHold(conversationId) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt index 1ca0682028d..4b7e3119cfe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt @@ -49,7 +49,7 @@ import java.util.Locale fun AssetFilePreview( modifier: Modifier = Modifier, assetName: String, - sizeInBytes: Long, + sizeInBytes: Long ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -79,13 +79,15 @@ fun AssetFilePreview( assetName, style = MaterialTheme.wireTypography.title02.copy(color = MaterialTheme.colorScheme.onBackground), textAlign = TextAlign.Center, - maxLines = 2, overflow = TextOverflow.Ellipsis + maxLines = 2, + overflow = TextOverflow.Ellipsis ) VerticalSpace.x8() Text(sizeInBytes.toFileSize(), style = MaterialTheme.wireTypography.body01.copy(MaterialTheme.wireColorScheme.secondaryText)) } } +@Suppress("MagicNumber") private fun Long.toFileSize(): String { val kilobyte = 1024.0 val megabyte = kilobyte * 1024 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 05545183980..f2b4da7e660 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 @@ -365,7 +365,6 @@ private fun SnackBarMessage(infoMessages: SharedFlow) { } } - @PreviewMultipleThemes @Composable fun PreviewImagesPreviewScreen() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt index a649eebab95..6d13cd703a4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageState.kt @@ -26,7 +26,7 @@ data class SendMessageState( sealed class SendMessageAction { data object None : SendMessageAction() - data object NavigateBack: SendMessageAction() + data object NavigateBack : SendMessageAction() data class NavigateToConversation(val conversationId: ConversationId) : SendMessageAction() data object NavigateToHome : SendMessageAction() } 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 9671082ef03..cde9d7e8c50 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 @@ -335,7 +335,7 @@ class SendMessageViewModel @Inject constructor( SureAboutMessagingDialogState.Hidden, is SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.BeforeSending, is SureAboutMessagingDialogState.Visible.ConversationVerificationDegraded -> - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(listOf(messageId), conversationId) + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(conversationId, listOf(messageId)) } } } @@ -415,7 +415,8 @@ class SendMessageViewModel @Inject constructor( SendMessageAction.None // TODO KBX pass action } else { SendMessageAction.None - }, inProgress = false + }, + inProgress = false ) } 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 94b36c45a26..51dfec7955f 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 @@ -48,14 +48,11 @@ import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.parcelableArrayList import com.wire.android.util.ui.WireSessionImageLoader -import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.message.SelfDeletionTimer.Companion.SELF_DELETION_LOG_TAG -import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsUseCase -import com.wire.kalium.logic.feature.message.SendTextMessageUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase @@ -84,9 +81,6 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( private val getSelf: GetSelfUserUseCase, private val userTypeMapper: UserTypeMapper, private val observeConversationListDetails: ObserveConversationListDetailsUseCase, - private val sendAssetMessage: ScheduleNewAssetMessageUseCase, - private val sendTextMessage: SendTextMessageUseCase, - private val kaliumFileSystem: KaliumFileSystem, private val handleUriAsset: HandleUriAssetUseCase, private val persistNewSelfDeletionTimerUseCase: PersistNewSelfDeletionTimerUseCase, private val observeSelfDeletionSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, 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 da0e7fd0f53..cee097f6cd9 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 @@ -421,7 +421,8 @@ private fun ImportMediaContent( modifier = Modifier.fillMaxHeight(), assetBundle = importedItemsList.first().assetBundle, showOnlyExtension = false, - onClick = {}) + onClick = {} + ) } } else { LazyRow( diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt index b4e6913b440..8b5d7d4dc22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt @@ -22,7 +22,6 @@ import com.wire.android.model.SnackBarMessage import com.wire.android.util.ui.UIText sealed class SendMessagesSnackbarMessages(override val uiText: UIText) : SnackBarMessage { - object MaxImageSize : SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_image_size_limit)) class MaxAmountOfAssetsReached(maxAmount: Int) : // TODO add max amount to string resource SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_limit_number_assets_imported_exceeded)) class MaxAssetSizeExceeded(assetSizeLimit: Int) : 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 9916f12decf..0826316a526 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 @@ -497,7 +497,7 @@ class SendMessageViewModelTest { // then coVerify(exactly = 0) { arrangement.retryFailedMessageUseCase.invoke(eq(messageId), any()) } assertEquals( - SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(listOf(messageId), conversationId), + SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold.AfterSending(conversationId, listOf(messageId)), viewModel.sureAboutMessagingDialogState ) } From f7f0b82d0ca2a1023cebf717affdabe59e9cd9f9 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 15 May 2024 09:22:18 +0200 Subject: [PATCH 08/12] detekt fix --- .../ui/home/conversations/sendmessage/SendMessageViewModel.kt | 2 +- .../com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 cde9d7e8c50..41223c0df6a 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 @@ -125,7 +125,7 @@ class SendMessageViewModel @Inject constructor( fun trySendMessages(messageBundleList: List) { if (messageBundleList.size > MAX_LIMIT_MESSAGE_SEND) { - onSnackbarMessage(SendMessagesSnackbarMessages.MaxAmountOfAssetsReached(MAX_LIMIT_MESSAGE_SEND)) + onSnackbarMessage(SendMessagesSnackbarMessages.MaxAmountOfAssetsReached) } else { val messageBundleMap = messageBundleList.groupBy { it.conversationId } messageBundleMap.forEach { (conversationId, bundles) -> diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt index 8b5d7d4dc22..108956bbc19 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/SendMessagesSnackbarMessages.kt @@ -22,7 +22,7 @@ import com.wire.android.model.SnackBarMessage import com.wire.android.util.ui.UIText sealed class SendMessagesSnackbarMessages(override val uiText: UIText) : SnackBarMessage { - class MaxAmountOfAssetsReached(maxAmount: Int) : // TODO add max amount to string resource + data object MaxAmountOfAssetsReached : // TODO add max amount to string resource SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_limit_number_assets_imported_exceeded)) class MaxAssetSizeExceeded(assetSizeLimit: Int) : SendMessagesSnackbarMessages(UIText.StringResource(R.string.error_conversation_max_asset_size_limit, assetSizeLimit)) From 555d0513bcb4f4c3ae12e1d0a3c6c75b48f68f6f Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 16 May 2024 13:32:14 +0200 Subject: [PATCH 09/12] review fixes 1 --- .../home/conversations/AssetTooLargeDialog.kt | 15 +-------- .../media/preview/AssetFilePreview.kt | 30 +++++------------ .../media/preview/AssetTilePreview.kt | 7 ++-- .../messagetypes/asset/AssetMessageTypes.kt | 10 ++---- .../com/wire/android/util/DeviceUtil.kt | 33 ++++--------------- .../wire/android/ui/theme/WireTypography.kt | 3 +- .../android/ui/theme/WireTypographyBase.kt | 3 ++ 7 files changed, 25 insertions(+), 76 deletions(-) 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 6c91faf6178..416326aab21 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,20 +30,7 @@ import com.wire.kalium.logic.data.asset.AttachmentType @Composable fun AssetTooLargeDialog(dialogState: AssetTooLargeDialogState, hideDialog: () -> Unit) { when (dialogState) { - is AssetTooLargeDialogState.SingleVisible -> { - WireDialog( - title = getTitle(dialogState), - text = getLabel(dialogState), - buttonsHorizontalAlignment = false, - onDismiss = hideDialog, - optionButton1Properties = WireDialogButtonProperties( - text = stringResource(R.string.label_ok), - type = WireDialogButtonType.Primary, - onClick = hideDialog - ) - ) - } - + is AssetTooLargeDialogState.SingleVisible, is AssetTooLargeDialogState.MultipleVisible -> { WireDialog( title = getTitle(dialogState), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt index 4b7e3119cfe..0a8cdba4127 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -43,6 +42,7 @@ import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.DeviceUtil import java.util.Locale @Composable @@ -68,36 +68,24 @@ fun AssetFilePreview( Text( modifier = Modifier.padding(bottom = dimensions().spacing8x), text = assetName.split(".").last().uppercase(Locale.getDefault()), - style = MaterialTheme.wireTypography.title01.copy( - fontWeight = FontWeight.W900, - color = MaterialTheme.colorScheme.onPrimary - ) + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.wireTypography.title05 ) } VerticalSpace.x16() Text( assetName, - style = MaterialTheme.wireTypography.title02.copy(color = MaterialTheme.colorScheme.onBackground), + style = MaterialTheme.wireTypography.title02, + color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, maxLines = 2, overflow = TextOverflow.Ellipsis ) VerticalSpace.x8() - Text(sizeInBytes.toFileSize(), style = MaterialTheme.wireTypography.body01.copy(MaterialTheme.wireColorScheme.secondaryText)) - } -} - -@Suppress("MagicNumber") -private fun Long.toFileSize(): String { - val kilobyte = 1024.0 - val megabyte = kilobyte * 1024 - val gigabyte = megabyte * 1024 - - return when { - this < kilobyte -> "$this B" - this < megabyte -> String.format("%.2f KB", this / kilobyte) - this < gigabyte -> String.format("%.2f MB", this / megabyte) - else -> String.format("%.2f GB", this / gigabyte) + Text( + DeviceUtil.formatSize(sizeInBytes), + style = MaterialTheme.wireTypography.body01.copy(MaterialTheme.wireColorScheme.secondaryText) + ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt index 13df42363cb..3cfb98fe9e3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt @@ -111,10 +111,8 @@ fun AssetTilePreview( fun AssetExtensionPreviewTile(assetName: String) { Text( text = assetName.split(".").last().uppercase(Locale.getDefault()), - style = MaterialTheme.wireTypography.title01.copy( - fontWeight = FontWeight.W900, - color = MaterialTheme.wireColorScheme.secondaryText - ) + style = MaterialTheme.wireTypography.title05, + color = MaterialTheme.wireColorScheme.secondaryText ) } @@ -141,7 +139,6 @@ fun AssetFilePreviewTile(assetBundle: AssetBundle, modifier: Modifier = Modifier maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.wireColorScheme.secondaryText, - fontSize = 12.sp, style = MaterialTheme.wireTypography.subline01 ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt index 214c81cbaca..c38fd904f57 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/asset/AssetMessageTypes.kt @@ -56,10 +56,10 @@ import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.DeviceUtil import com.wire.kalium.logic.data.asset.AssetTransferStatus import com.wire.kalium.logic.data.asset.isFailed import com.wire.kalium.logic.data.asset.isInProgress -import kotlin.math.roundToInt @Composable internal fun MessageAsset( @@ -317,11 +317,5 @@ private fun isNotClickable(assetTransferStatus: AssetTransferStatus) = @Suppress("MagicNumber") @Stable private fun provideAssetDescription(assetExtension: String, assetSizeInBytes: Long): String { - val oneKB = 1024L - val oneMB = oneKB * oneKB - return when { - assetSizeInBytes < oneKB -> "${assetExtension.uppercase()} ($assetSizeInBytes B)" - assetSizeInBytes in oneKB..oneMB -> "${assetExtension.uppercase()} (${assetSizeInBytes / oneKB} KB)" - else -> "${assetExtension.uppercase()} (${((assetSizeInBytes / oneMB) * 100.0).roundToInt() / 100.0} MB)" // 2 decimals round off - } + return "${assetExtension.uppercase()} (${DeviceUtil.formatSize(assetSizeInBytes)})" } 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 74ad6cbbffb..c1ad2791e24 100644 --- a/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt @@ -24,7 +24,6 @@ object DeviceUtil { private const val BYTES_IN_KILOBYTE = 1024 private const val BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024 private const val BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024 - private const val DIGITS_GROUP_SIZE = 3 // Number of digits between commas in formatted size. fun getAvailableInternalMemorySize(): String = try { val path = Environment.getDataDirectory() @@ -46,32 +45,12 @@ object DeviceUtil { "" } - private fun formatSize(sizeInBytes: Long): String { - var size = sizeInBytes - var suffix: String? = null - when { - size >= BYTES_IN_GIGABYTE -> { - suffix = "GB" - size /= BYTES_IN_GIGABYTE - } - - size >= BYTES_IN_MEGABYTE -> { - suffix = "MB" - size /= BYTES_IN_MEGABYTE - } - - size >= BYTES_IN_KILOBYTE -> { - suffix = "KB" - size /= BYTES_IN_KILOBYTE - } - } - val resultBuffer = StringBuilder(size.toString()) - var commaOffset = resultBuffer.length - DIGITS_GROUP_SIZE - while (commaOffset > 0) { - resultBuffer.insert(commaOffset, ',') - commaOffset -= DIGITS_GROUP_SIZE + 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) } - suffix?.let { resultBuffer.append(it) } - return resultBuffer.toString() } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt index 884a3dec3a9..afd9cf04eb5 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt @@ -25,7 +25,7 @@ import io.github.esentsov.PackagePrivate @Immutable data class WireTypography( - val title01: TextStyle, val title02: TextStyle, val title03: TextStyle, val title04: TextStyle, + val title01: TextStyle, val title02: TextStyle, val title03: TextStyle, val title04: TextStyle, val title05: TextStyle, val body01: TextStyle, val body02: TextStyle, val body03: TextStyle, val body04: TextStyle, val body05: TextStyle, val button01: TextStyle, val button02: TextStyle, val button03: TextStyle, val button04: TextStyle, val button05: TextStyle, val label01: TextStyle, val label02: TextStyle, val label03: TextStyle, val label04: TextStyle, val label05: TextStyle, @@ -45,6 +45,7 @@ private val DefaultWireTypography = WireTypography( title02 = WireTypographyBase.Title02, title03 = WireTypographyBase.Title03, title04 = WireTypographyBase.Title04, + title05 = WireTypographyBase.Title05, body01 = WireTypographyBase.Body01, body02 = WireTypographyBase.Body02, body03 = WireTypographyBase.Body03, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypographyBase.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypographyBase.kt index bc9d9d44739..0a12f33559e 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypographyBase.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypographyBase.kt @@ -49,6 +49,9 @@ object WireTypographyBase { val Title04 = Title01.copy( fontWeight = FontWeight.W400, ) + val Title05 = Title01.copy( + fontWeight = FontWeight.W900 + ) val Body01 = TextStyle( fontWeight = FontWeight.W400, fontSize = 15.sp, From b147cb2b2ee9405a0d46f8a5cef599784fd112ce Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 16 May 2024 14:15:37 +0200 Subject: [PATCH 10/12] detekt fix --- .../ui/home/conversations/media/preview/AssetTilePreview.kt | 2 -- .../src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt index 3cfb98fe9e3..62a1bcfe035 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetTilePreview.kt @@ -40,9 +40,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.sp import com.wire.android.R import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.image.WireImage diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt index afd9cf04eb5..e4a496eee3d 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireTypography.kt @@ -16,6 +16,8 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ +@file:Suppress("ParameterListWrapping") + package com.wire.android.ui.theme import androidx.compose.material3.Typography From f6c35c0717dd7316020a833161a88db3a89ed272 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 16 May 2024 14:39:05 +0200 Subject: [PATCH 11/12] review fixes --- .../ui/home/conversations/media/preview/AssetFilePreview.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt index 0a8cdba4127..98170ce5697 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/AssetFilePreview.kt @@ -84,7 +84,8 @@ fun AssetFilePreview( VerticalSpace.x8() Text( DeviceUtil.formatSize(sizeInBytes), - style = MaterialTheme.wireTypography.body01.copy(MaterialTheme.wireColorScheme.secondaryText) + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.secondaryText ) } } From 501ac265557ffbe0f3ba08c852fbd5344333e001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Mon, 27 May 2024 12:00:45 +0200 Subject: [PATCH 12/12] feat: image preview improvements [WPB-8801] (#3019) --- .../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 | 106 ++++++----- .../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, 446 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..a8445d77da1 --- /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..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 @@ -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,14 @@ 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 +263,19 @@ 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..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 @@ -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,41 @@ 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 +598,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 @@ + + + + +