diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt index a8445d77da1..9a18623f5b1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt @@ -41,7 +41,7 @@ class CheckAssetRestrictionsViewModel @Inject constructor() : ViewModel() { assetType = it.assetBundle.assetType, maxLimitInMB = it.assetSizeExceeded!!, savedToDevice = false, - multipleAssets = true + multipleAssets = importedMediaList.size > 1, ) } ?: onSuccess(importedMediaList.map { it.assetBundle }) } 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 8c9a072d670..b088d945cad 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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -230,48 +231,63 @@ private fun Content( ) } - LazyRow( + if (previewState.assetBundleList.size > 1) { + ThumbnailsRow( + previewState = previewState, + onSelected = onSelected, + onRemoveAsset = onRemoveAsset + ) + } + } + } +} + +@Composable +private fun BoxScope.ThumbnailsRow( + previewState: ImagesPreviewState, + onSelected: (index: Int) -> Unit, + onRemoveAsset: (index: Int) -> Unit +) { + LazyRow( + modifier = Modifier + .padding(bottom = dimensions().spacing8x) + .height(dimensions().spacing80x) + .align(Alignment.BottomCenter), + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing4x), + contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) + ) { + items( + count = previewState.assetBundleList.size, + ) { index -> + Box( modifier = Modifier - .padding(bottom = dimensions().spacing8x) - .height(dimensions().spacing80x) - .align(Alignment.BottomCenter), - horizontalArrangement = Arrangement.spacedBy(dimensions().spacing4x), - contentPadding = PaddingValues(start = dimensions().spacing16x, end = dimensions().spacing16x) + .width(dimensions().spacing80x) + .fillMaxHeight() ) { - items( - count = previewState.assetBundleList.size, - ) { index -> - Box( - modifier = Modifier - .width(dimensions().spacing80x) - .fillMaxHeight() - ) { - AssetTilePreview( - modifier = Modifier - .size(dimensions().spacing64x) - .align(Alignment.Center), - assetBundle = previewState.assetBundleList[index].assetBundle, - isSelected = previewState.selectedIndex == index, - showOnlyExtension = true, - onClick = { onSelected(index) } - ) + AssetTilePreview( + modifier = Modifier + .size(dimensions().spacing64x) + .align(Alignment.Center), + assetBundle = previewState.assetBundleList[index].assetBundle, + isSelected = previewState.selectedIndex == index, + showOnlyExtension = true, + onClick = { onSelected(index) } + ) - if (previewState.assetBundleList.size > 1) { - RemoveIcon( - modifier = Modifier.align(Alignment.TopEnd), - onClick = { - onRemoveAsset(index) - }, - contentDescription = stringResource(id = R.string.remove_asset_description) - ) - } - if (previewState.assetBundleList[index].assetSizeExceeded != null) { - ErrorIcon( - stringResource(id = R.string.asset_attention_description), - modifier = Modifier.align(Alignment.Center) - ) - } - } + if (previewState.assetBundleList.size > 1) { + RemoveIcon( + modifier = Modifier.align(Alignment.TopEnd), + onClick = { + onRemoveAsset(index) + }, + contentDescription = stringResource(id = R.string.remove_asset_description) + ) + } + if (previewState.assetBundleList[index].assetSizeExceeded != null) { + ErrorIcon( + stringResource(id = R.string.asset_attention_description), + modifier = Modifier.align(Alignment.Center) + ) } } } @@ -280,7 +296,7 @@ private fun Content( @PreviewMultipleThemes @Composable -fun PreviewImagesPreviewScreen() { +fun PreviewImagesPreviewScreenMultipleAssets() { WireTheme { Content( previewState = ImagesPreviewState( @@ -341,3 +357,34 @@ fun PreviewImagesPreviewScreen() { ) } } + +@PreviewMultipleThemes +@Composable +fun PreviewImagesPreviewScreenSingleAsset() { + WireTheme { + Content( + previewState = ImagesPreviewState( + ConversationId("value", "domain"), + selectedIndex = 0, + conversationName = "Conversation", + assetBundleList = persistentListOf( + ImportedMediaAsset( + AssetBundle( + "key", + "image/png", + "".toPath(), + 20, + "preview.png", + assetType = AttachmentType.IMAGE + ), + assetSizeExceeded = null + ) + ), + ), + onNavigationPressed = {}, + onSendMessages = {}, + onSelected = {}, + onRemoveAsset = {} + ) + } +} 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 4c0e225e224..c6a93151461 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 @@ -21,7 +21,12 @@ import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.content.MediaType +import androidx.compose.foundation.content.consume +import androidx.compose.foundation.content.contentReceiver +import androidx.compose.foundation.content.hasMediaType import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -54,6 +59,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -69,11 +75,12 @@ import com.wire.android.ui.home.conversations.model.UriAsset import com.wire.android.ui.home.messagecomposer.state.AdditionalOptionSubMenuState import com.wire.android.ui.home.messagecomposer.state.InputType import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder +import com.wire.android.util.isImage import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.SelfDeletionTimer import kotlin.math.roundToInt -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) @Suppress("ComplexMethod") @Composable fun EnabledMessageComposer( @@ -95,6 +102,7 @@ fun EnabledMessageComposer( tempWritableImageUri: Uri?, modifier: Modifier = Modifier, ) { + val context = LocalContext.current val density = LocalDensity.current val navBarHeight = BottomNavigationBarHeight() val isImeVisible = WindowInsets.isImeVisible @@ -234,7 +242,27 @@ fun EnabledMessageComposer( AdditionalOptionSubMenuState.RecordAudio -> {} } - }, + } + .contentReceiver( + receiveContentListener = { transferableContent -> + if (transferableContent.hasMediaType(MediaType.Image)) { + val imageUriList = mutableListOf() + transferableContent + .consume { item -> + // Only use URIs with images + (item.uri != null && item.uri.isImage(context)) + .also { hasImageUri -> + if (hasImageUri) imageUriList.add(item.uri) + } + } + .also { + onImagesPicked(imageUriList) + } + } else { + transferableContent + } + } + ), ) val mentionSearchResult = messageComposerViewState.value.mentionSearchResult diff --git a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt index 9ca2ede4651..932b37b0298 100644 --- a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt @@ -201,29 +201,33 @@ suspend fun Uri.resampleImageAndCopyToTempPath( dispatcher: DispatcherProvider = DefaultDispatcherProvider() ): Long { return withContext(dispatcher.io()) { - var size: Long val originalImage = toByteArray(context, dispatcher) if (originalImage.isEmpty()) return@withContext 0L // if the image is empty, resampling it would cause an exception - ImageUtil.resample(originalImage, sizeClass).let { processedImage -> - val file = tempCachePath.toFile() - try { - size = processedImage.size.toLong() - file.setWritable(true) - file.outputStream().use { it.write(processedImage) } - } catch (e: FileNotFoundException) { - appLogger.e("[ResampleImage] Cannot find file ${file.path}", e) - throw e - } catch (e: IOException) { - appLogger.e("[ResampleImage] I/O error while writing the image", e) - throw e - } + val mimeType = this@resampleImageAndCopyToTempPath.getMimeType(context) + if (mimeType == "image/gif") { + // GIFs are not resampled, it takes long and usually GIFs are small enough to be shared as is. + // If the GIF is too large, the user will be informed about that, just like for all other files. + originalImage.writeToFile(tempCachePath.toFile()) + } else { + ImageUtil.resample(originalImage, sizeClass).writeToFile(tempCachePath.toFile()) } - - size } } +private fun ByteArray.writeToFile(file: File): Long = + try { + file.setWritable(true) + file.outputStream().use { it.write(this) } + this.size.toLong() + } catch (e: FileNotFoundException) { + appLogger.e("[ResampleImage] Cannot find file ${file.path}", e) + throw e + } catch (e: IOException) { + appLogger.e("[ResampleImage] I/O error while writing the image", e) + throw e + } + fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) else -> uri.path?.let(::File)?.name diff --git a/app/src/main/kotlin/com/wire/android/util/ImageUtil.kt b/app/src/main/kotlin/com/wire/android/util/ImageUtil.kt index beda1c52bad..b875aa6d64a 100644 --- a/app/src/main/kotlin/com/wire/android/util/ImageUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/ImageUtil.kt @@ -145,6 +145,11 @@ fun Uri.toBitmap(context: Context): Bitmap? { } } +/** + * Checks whether it is the URI of the image + */ +fun Uri.isImage(context: Context): Boolean = isImageFile(this.getMimeType(context)) + /** * Rotates the image to its [ExifInterface.ORIENTATION_NORMAL] in case it's rotated with a different orientation than * landscape or portrait See more about exif interface at: