diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 43cf1196d6b..407f05d92be 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -3881,11 +3881,10 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamRippleConfig public final class io/getstream/chat/android/compose/ui/theme/StreamShapes { public static final field $stable I public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamShapes$Companion; - public fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V + public fun (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)V public final fun component1 ()Landroidx/compose/ui/graphics/Shape; public final fun component10 ()Landroidx/compose/ui/graphics/Shape; public final fun component11 ()Landroidx/compose/ui/graphics/Shape; - public final fun component12 ()Landroidx/compose/ui/graphics/Shape; public final fun component2 ()Landroidx/compose/ui/graphics/Shape; public final fun component3 ()Landroidx/compose/ui/graphics/Shape; public final fun component4 ()Landroidx/compose/ui/graphics/Shape; @@ -3894,11 +3893,10 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamShapes { public final fun component7 ()Landroidx/compose/ui/graphics/Shape; public final fun component8 ()Landroidx/compose/ui/graphics/Shape; public final fun component9 ()Landroidx/compose/ui/graphics/Shape; - public final fun copy (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)Lio/getstream/chat/android/compose/ui/theme/StreamShapes; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamShapes; + public final fun copy (Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;)Lio/getstream/chat/android/compose/ui/theme/StreamShapes; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/ui/graphics/Shape;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamShapes; public fun equals (Ljava/lang/Object;)Z public final fun getAttachment ()Landroidx/compose/ui/graphics/Shape; - public final fun getAttachmentSiteLabel ()Landroidx/compose/ui/graphics/Shape; public final fun getAvatar ()Landroidx/compose/ui/graphics/Shape; public final fun getBottomSheet ()Landroidx/compose/ui/graphics/Shape; public final fun getHeader ()Landroidx/compose/ui/graphics/Shape; diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt index a57aed10e22..8e077d9f017 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt @@ -59,7 +59,7 @@ public object StreamAttachmentFactories { * The default max length of the link attachments description. We limit this, because for some links the description * can be too long. */ - private const val DEFAULT_LINK_DESCRIPTION_MAX_LINES = 5 + private const val DEFAULT_LINK_DESCRIPTION_MAX_LINES = 2 /** * Default attachment factories we provide, which can transform image, file and link attachments. diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt index b85a18dbc06..8a35c229ba5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt @@ -56,6 +56,7 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.theme.StreamDimens import io.getstream.chat.android.compose.ui.util.AsyncImagePreviewHandler import io.getstream.chat.android.compose.ui.util.StreamAsyncImage +import io.getstream.chat.android.compose.ui.util.applyIf import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.Message @@ -215,7 +216,7 @@ public fun GiphyAttachmentContent( Box( modifier = modifier .size(giphyDimensions) - .clip(ChatTheme.shapes.attachment) + .applyIf(message.text.isNotEmpty()) { clip(ChatTheme.shapes.attachment) } .combinedClickable( indication = null, interactionSource = remember { MutableInteractionSource() }, @@ -332,6 +333,7 @@ internal fun GiphyAttachmentContent() { GiphyAttachmentContent( attachmentState = AttachmentState( message = Message( + text = "Hello", attachments = listOf( Attachment( titleLink = "https://giphy.com/gifs/funny-cat-3oEjI6SIIHBdRxXI40", diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.kt index 25076ce26b6..01e35adb3f9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.kt @@ -25,35 +25,31 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.net.toUri import coil3.ColorImage import coil3.annotation.ExperimentalCoilApi @@ -63,6 +59,8 @@ import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState import io.getstream.chat.android.compose.ui.components.ShimmerProgressIndicator import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.MessageStyling +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.ui.util.AsyncImagePreviewHandler import io.getstream.chat.android.compose.ui.util.StreamAsyncImage import io.getstream.chat.android.models.Attachment @@ -132,20 +130,17 @@ public fun LinkAttachmentContent( val context = LocalContext.current val attachment = message.attachments.firstOrNull { it.hasLink() && !it.isGiphy() } + val textColor = MessageStyling.textColor(outgoing = isMine) checkNotNull(attachment) { "Missing link attachment." } val previewUrl = attachment.titleLink ?: attachment.ogUrl - val urlWithScheme = previewUrl?.addSchemeToUrlIfNeeded() - checkNotNull(previewUrl) { "Missing preview URL." } - val errorMessage = stringResource(R.string.stream_compose_message_list_error_cannot_open_link, previewUrl) - Column( modifier = modifier .clip(ChatTheme.shapes.attachment) @@ -154,140 +149,116 @@ public fun LinkAttachmentContent( indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { - try { - if (urlWithScheme != null) { - onItemClick( - LinkAttachmentClickData( - context = context, - url = urlWithScheme, - attachment = attachment, - message = message, - ), - ) - } else { - Toast - .makeText(context, errorMessage, Toast.LENGTH_LONG) - .show() - } - } catch (e: ActivityNotFoundException) { - e.printStackTrace() - Toast - .makeText(context, errorMessage, Toast.LENGTH_LONG) - .show() - } + onItemClick( + LinkAttachmentClickData( + context = context, + url = previewUrl.addSchemeToUrlIfNeeded(), + attachment = attachment, + message = message, + ), + ) }, onLongClick = { onLongItemClick(message) }, ), ) { - val imagePreviewUrl = attachment.imagePreviewUrl - if (imagePreviewUrl != null) { - LinkAttachmentImagePreview(attachment, isMine) + attachment.imagePreviewUrl?.let { + LinkAttachmentImagePreview(it) } - val title = attachment.title - if (title != null) { - LinkAttachmentTitle(title) + Spacer(Modifier.height(12.dp)) + + attachment.title?.let { + LinkAttachmentTitle(it, textColor) } - val description = attachment.text - if (description != null) { - LinkAttachmentDescription(description, linkDescriptionMaxLines) + attachment.text?.let { + LinkAttachmentDescription(it, linkDescriptionMaxLines, textColor) } + + AttachmentLink(previewUrl, textColor) } } @Composable -private fun LinkAttachmentImagePreview(attachment: Attachment, isMine: Boolean) { - val data = attachment.imagePreviewUrl - var maxWidth by remember { mutableStateOf(0.dp) } - - Box( - modifier = Modifier.onSizeChanged { size -> maxWidth = size.width.dp }, - ) { - val contentScale = ContentScale.FillWidth - StreamAsyncImage( - modifier = Modifier - .heightIn(max = 250.dp) - .clip(ChatTheme.shapes.attachment) - .testTag("Stream_LinkAttachmentPreview"), - data = data, - contentScale = contentScale, - ) { state -> - val painter = state.painter - - if (painter == null) { - ShimmerProgressIndicator( - modifier = Modifier.fillMaxSize(), - ) - } else { - val intrinsicSize = painter.intrinsicSize - val aspectRatio = intrinsicSize.width / intrinsicSize.height - - Image( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(aspectRatio), - painter = painter, - contentDescription = null, - contentScale = contentScale, - ) - } - } - - val authorName = attachment.authorName - - if (authorName != null) { - Text( - text = authorName, - color = ChatTheme.colors.primaryAccent, - maxLines = 1, - style = ChatTheme.typography.bodyBold, - fontSize = 16.sp, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .widthIn(max = maxWidth / 2) - .background( - color = getLinkBackgroundColor(isMine), - shape = ChatTheme.shapes.attachmentSiteLabel, - ) - .padding(vertical = 6.dp, horizontal = 12.dp) - .align(Alignment.BottomStart), +private fun LinkAttachmentImagePreview(imageUrl: String) { + val contentScale = ContentScale.FillWidth + StreamAsyncImage( + modifier = Modifier + .heightIn(max = 144.dp) + .testTag("Stream_LinkAttachmentPreview"), + data = imageUrl, + contentScale = contentScale, + ) { state -> + when (val painter = state.painter) { + null -> ShimmerProgressIndicator(Modifier.fillMaxSize()) + else -> Image( + modifier = Modifier.fillMaxSize(), + painter = painter, + contentDescription = null, + contentScale = contentScale, ) } } } @Composable -private fun LinkAttachmentTitle(text: String) { +private fun LinkAttachmentTitle(text: String, color: Color) { Text( modifier = Modifier - .padding(start = 8.dp, end = 8.dp) + .padding(horizontal = StreamTokens.spacingSm) .testTag("Stream_LinkAttachmentTitle"), text = text, - style = ChatTheme.typography.bodyBold, - color = ChatTheme.colors.textHighEmphasis, + style = ChatTheme.typography.captionEmphasis, + color = color, ) } @Composable -private fun LinkAttachmentDescription(description: String, linkDescriptionMaxLines: Int) { +private fun LinkAttachmentDescription(description: String, maxLines: Int, color: Color) { Text( modifier = Modifier .padding( - start = 8.dp, - end = 8.dp, - bottom = 4.dp, - top = 2.dp, + start = StreamTokens.spacingSm, + end = StreamTokens.spacingSm, + top = StreamTokens.spacing2xs, ) .testTag("Stream_LinkAttachmentDescription"), text = description, - style = ChatTheme.typography.footnote, - color = ChatTheme.colors.textHighEmphasis, - maxLines = linkDescriptionMaxLines, + style = ChatTheme.typography.metadataDefault, + color = color, + maxLines = maxLines, overflow = TextOverflow.Ellipsis, ) } +@Composable +private fun AttachmentLink(link: String, color: Color) { + Row( + modifier = Modifier.padding( + start = StreamTokens.spacingSm, + end = StreamTokens.spacingSm, + top = StreamTokens.spacing2xs, + bottom = StreamTokens.spacingSm, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.stream_compose_ic_link), + contentDescription = null, + modifier = Modifier.size(StreamTokens.size12), + tint = color, + ) + Text( + text = link, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = StreamTokens.spacing2xs), + style = ChatTheme.typography.metadataDefault, + color = color, + ) + } +} + @Composable private fun getLinkBackgroundColor(isMine: Boolean): Color { return if (isMine) { @@ -304,12 +275,15 @@ private fun getLinkBackgroundColor(isMine: Boolean): Color { * @param url The url of the link attachment being clicked. */ internal fun onLinkAttachmentContentClick(context: Context, url: String) { - context.startActivity( - Intent( - Intent.ACTION_VIEW, - url.toUri(), - ), - ) + try { + context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) + } catch (_: ActivityNotFoundException) { + val errorMessage = + context.getString(R.string.stream_compose_message_list_error_cannot_open_link, url) + Toast + .makeText(context, errorMessage, Toast.LENGTH_LONG) + .show() + } } @Preview(showBackground = true) @@ -338,7 +312,7 @@ internal fun LinkAttachmentContent() { state = AttachmentState( message = Message(attachments = listOf(attachment)), ), - linkDescriptionMaxLines = 5, + linkDescriptionMaxLines = 2, ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 376a524236d..3b37310b690 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -32,10 +32,12 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -49,6 +51,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -76,10 +79,13 @@ import io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPrev import io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewContract.Input import io.getstream.chat.android.compose.ui.components.ShimmerProgressIndicator import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.compose.ui.util.AsyncImagePreviewHandler import io.getstream.chat.android.compose.ui.util.LocalStreamImageLoader import io.getstream.chat.android.compose.ui.util.StreamAsyncImage +import io.getstream.chat.android.compose.ui.util.applyIf import io.getstream.chat.android.compose.ui.util.extensions.internal.imagePreviewData +import io.getstream.chat.android.compose.ui.util.ifNotNull import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.Message @@ -201,7 +207,6 @@ public fun MediaAttachmentContent( }, ) { val (message, _, onLongItemClick, onMediaGalleryPreviewResult) = state - val gridSpacing = ChatTheme.dimens.attachmentsContentMediaGridSpacing // Prepare the image loader for the media gallery val imageLoader = LocalStreamImageLoader.current @@ -212,40 +217,30 @@ public fun MediaAttachmentContent( val attachments = message.attachments.filter { !it.hasLink() && (it.isImage() || it.isVideo()) } - val attachmentCount = attachments.size - val description = if (attachmentCount > 1) { - stringResource(R.string.stream_ui_message_list_semantics_message_attachments, attachmentCount) + if (attachments.size == 1) { + SingleMediaAttachment( + attachment = attachments.first(), + message = message, + modifier = modifier, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + skipEnrichUrl = skipEnrichUrl, + onContentItemClick = onItemClick, + overlayContent = itemOverlayContent, + ) } else { - null - } - - Row( - modifier - .semantics { - if (description != null) { - contentDescription = description - } - } - .clip(ChatTheme.shapes.attachment), - horizontalArrangement = Arrangement.spacedBy(gridSpacing), - ) { - if (attachmentCount == 1) { - val attachment = attachments.first() - - SingleMediaAttachment( - attachment = attachment, - message = message, - onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, - onLongItemClick = onLongItemClick, - skipEnrichUrl = skipEnrichUrl, - onContentItemClick = onItemClick, - overlayContent = itemOverlayContent, - ) - } else { - MultipleMediaAttachments( + val gridSpacing = StreamTokens.spacing2xs + val description = + stringResource(R.string.stream_ui_message_list_semantics_message_attachments, attachments.size) + Row( + modifier = modifier + .semantics { this.contentDescription = description } + .padding(StreamTokens.spacingXs), + horizontalArrangement = Arrangement.spacedBy(gridSpacing), + ) { + MultipleMediaAttachmentsColumns( attachments = attachments, - attachmentCount = attachmentCount, gridSpacing = gridSpacing, maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, message = message, @@ -278,6 +273,7 @@ public fun MediaAttachmentContent( internal fun SingleMediaAttachment( attachment: Attachment, message: Message, + modifier: Modifier, skipEnrichUrl: Boolean, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onLongItemClick: (Message) -> Unit, @@ -299,9 +295,11 @@ internal fun SingleMediaAttachment( } } + val shouldBeFullSize = message.text.isEmpty() && message.replyTo == null MediaAttachmentContentItem( attachment = attachment, - modifier = Modifier + modifier = modifier + .applyIf(!shouldBeFullSize) { padding(StreamTokens.spacingXs) } .heightIn( max = if (isVideo) { ChatTheme.dimens.attachmentsContentVideoMaxHeight @@ -317,6 +315,7 @@ internal fun SingleMediaAttachment( }, ) .aspectRatio(ratio ?: EqualDimensionsRatio), + shape = if (shouldBeFullSize) null else ChatTheme.shapes.attachment, message = message, attachmentPosition = 0, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, @@ -331,7 +330,6 @@ internal fun SingleMediaAttachment( * Displays previews of multiple image and video attachment laid out in a grid. * * @param attachments The list of attachments that are to be previewed. - * @param attachmentCount The number of attachments that are to be previewed. * @param gridSpacing Determines the spacing strategy between items. * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed * in a group when previewing Media attachments in the message list. @@ -347,9 +345,8 @@ internal fun SingleMediaAttachment( */ @Suppress("LongParameterList", "LongMethod") @Composable -internal fun RowScope.MultipleMediaAttachments( +internal fun RowScope.MultipleMediaAttachmentsColumns( attachments: List, - attachmentCount: Int, gridSpacing: Dp, maximumNumberOfPreviewedItems: Int = 4, message: Message, @@ -359,69 +356,65 @@ internal fun RowScope.MultipleMediaAttachments( onContentItemClick: (MediaAttachmentClickData) -> Unit, itemOverlayContent: @Composable (attachmentType: String?) -> Unit, ) { + val totalPreviewCount = attachments.size.coerceAtMost(maximumNumberOfPreviewedItems) + val col1Count = totalPreviewCount / 2 + val col2Count = totalPreviewCount - col1Count + + val columnSizingModifier = Modifier + .weight(1f, fill = false) + .size( + width = ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2, + height = ChatTheme.dimens.attachmentsContentGroupPreviewHeight, + ) + Column( - modifier = Modifier - .weight(1f, fill = false) - .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) - .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight) - .testTag("Stream_MultipleMediaAttachmentsColumn"), + modifier = columnSizingModifier.testTag("Stream_MultipleMediaAttachmentsColumn"), verticalArrangement = Arrangement.spacedBy(gridSpacing), ) { - for (attachmentIndex in 0 until maximumNumberOfPreviewedItems step 2) { - if (attachmentIndex < attachmentCount) { - MediaAttachmentContentItem( - attachment = attachments[attachmentIndex], - modifier = Modifier.weight(1f), - message = message, - skipEnrichUrl = skipEnrichUrl, - attachmentPosition = attachmentIndex, - onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, - onLongItemClick = onLongItemClick, - onItemClick = onContentItemClick, - overlayContent = itemOverlayContent, - ) - } + for (positionInColumn in 0 until col1Count) { + val shape = attachmentShape( + positionInColumn = positionInColumn, + isFirstColumn = true, + itemsInColumn = col1Count, + ) + MediaAttachmentContentItem( + attachment = attachments[positionInColumn], + modifier = Modifier.weight(1f), + shape = shape, + message = message, + skipEnrichUrl = skipEnrichUrl, + attachmentPosition = positionInColumn, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + onItemClick = onContentItemClick, + overlayContent = itemOverlayContent, + ) } } Column( - modifier = Modifier - .weight(1f, fill = false) - .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) - .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight), + modifier = columnSizingModifier, verticalArrangement = Arrangement.spacedBy(gridSpacing), ) { - for (attachmentIndex in 1 until maximumNumberOfPreviewedItems step 2) { - if (attachmentIndex < attachmentCount) { - val attachment = attachments[attachmentIndex] - val isUploading = attachment.uploadState is Attachment.UploadState.InProgress - val lastItemInColumnIndex = (maximumNumberOfPreviewedItems - 1) - (maximumNumberOfPreviewedItems % 2) - - if (attachmentIndex == lastItemInColumnIndex && attachmentCount > maximumNumberOfPreviewedItems) { - Box(modifier = Modifier.weight(1f)) { - MediaAttachmentContentItem( - attachment = attachment, - message = message, - skipEnrichUrl = skipEnrichUrl, - attachmentPosition = attachmentIndex, - onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, - onLongItemClick = onLongItemClick, - onItemClick = onContentItemClick, - overlayContent = itemOverlayContent, - ) + for (positionInColumn in 0 until col2Count) { + val attachmentIndex = col1Count + positionInColumn + val attachment = attachments[attachmentIndex] + val isUploading = attachment.uploadState is Attachment.UploadState.InProgress + + val isLastVisibleSlot = attachmentIndex == maximumNumberOfPreviewedItems - 1 + val hasHiddenItems = attachments.size > maximumNumberOfPreviewedItems + + val shape = attachmentShape( + positionInColumn = positionInColumn, + isFirstColumn = false, + itemsInColumn = col2Count, + ) - if (!isUploading) { - MediaAttachmentShowMoreOverlay( - mediaCount = attachmentCount, - maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } else { + if (isLastVisibleSlot && hasHiddenItems) { + Box(modifier = Modifier.weight(1f)) { MediaAttachmentContentItem( attachment = attachment, - modifier = Modifier.weight(1f), + shape = shape, message = message, skipEnrichUrl = skipEnrichUrl, attachmentPosition = attachmentIndex, @@ -430,12 +423,53 @@ internal fun RowScope.MultipleMediaAttachments( onItemClick = onContentItemClick, overlayContent = itemOverlayContent, ) + + if (!isUploading) { + MediaAttachmentShowMoreOverlay( + mediaCount = attachments.size, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, + shape = shape, + modifier = Modifier.align(Alignment.Center), + ) + } } + } else { + MediaAttachmentContentItem( + attachment = attachment, + modifier = Modifier.weight(1f), + shape = shape, + message = message, + skipEnrichUrl = skipEnrichUrl, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + onItemClick = onContentItemClick, + overlayContent = itemOverlayContent, + ) } } } } +private fun attachmentShape( + positionInColumn: Int, + isFirstColumn: Boolean, + itemsInColumn: Int, +): RoundedCornerShape { + val outer = StreamTokens.radiusLg + val inner = StreamTokens.radiusMd + + val isFirstInColumn = positionInColumn == 0 + val isLastInColumn = positionInColumn == itemsInColumn - 1 + + return RoundedCornerShape( + topStart = if (isFirstColumn && isFirstInColumn) outer else inner, + topEnd = if (!isFirstColumn && isFirstInColumn) outer else inner, + bottomEnd = if (!isFirstColumn && isLastInColumn) outer else inner, + bottomStart = if (isFirstColumn && isLastInColumn) outer else inner, + ) +} + /** * Displays previews of image and video attachments. * @@ -446,6 +480,7 @@ internal fun RowScope.MultipleMediaAttachments( * @param attachment The attachment that is previewed. * @param skipEnrichUrl Used by the media gallery. If set to true will skip enriching URLs when you update the message * by deleting an attachment contained within it. Set to false by default. + * @param shape Optional shape of the item. * @param onMediaGalleryPreviewResult The result of the activity used for propagating * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when the item is long clicked. @@ -462,6 +497,7 @@ internal fun MediaAttachmentContentItem( attachmentPosition: Int, attachment: Attachment, skipEnrichUrl: Boolean, + shape: Shape?, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, @@ -508,7 +544,7 @@ internal fun MediaAttachmentContentItem( contentDescription = description } } - .background(Color.Black) + .ifNotNull(shape, Modifier::clip) .fillMaxWidth() .combinedClickable( interactionSource = remember { MutableInteractionSource() }, @@ -544,7 +580,7 @@ internal fun MediaAttachmentContentItem( StreamAsyncImage( imageRequest = imageRequest, - modifier = modifier + modifier = Modifier .fillMaxSize() .background(backgroundColor), contentScale = ContentScale.Crop, @@ -638,6 +674,7 @@ internal fun PlayButton( internal fun MediaAttachmentShowMoreOverlay( mediaCount: Int, maximumNumberOfPreviewedItems: Int, + shape: Shape, modifier: Modifier = Modifier, ) { val remainingMediaCount = mediaCount - maximumNumberOfPreviewedItems @@ -645,7 +682,7 @@ internal fun MediaAttachmentShowMoreOverlay( Box( modifier = Modifier .fillMaxSize() - .background(color = ChatTheme.colors.showMoreOverlay), + .background(color = ChatTheme.colors.showMoreOverlay, shape = shape), ) { Text( modifier = modifier @@ -747,6 +784,7 @@ internal fun SingleMediaAttachmentContent() { MediaAttachmentContent( attachmentState = AttachmentState( message = Message( + text = "Hello", attachments = listOf( Attachment( type = AttachmentType.IMAGE, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt index 198dd20871d..d3124782503 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt @@ -19,11 +19,11 @@ package io.getstream.chat.android.compose.ui.attachments.content import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.utils.attachment.isGiphy import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.theme.StreamTokens import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.common.utils.extensions.hasLink @@ -68,14 +68,16 @@ public fun MessageAttachmentsContent( ) if (attachmentFactory != null) { - attachmentFactory.content( + attachmentFactory.content(Modifier, attachmentState) + } else if (linkFactory != null) { + linkFactory.content( Modifier.padding( - 2.dp.takeIf { message.text.isEmpty() } ?: 8.dp, + top = StreamTokens.spacingXs, + start = StreamTokens.spacingXs, + end = StreamTokens.spacingXs, ), attachmentState, ) - } else if (linkFactory != null) { - linkFactory.content(Modifier.padding(8.dp), attachmentState) } } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageBubble.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageBubble.kt index 14b78bead6f..2df19653f35 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageBubble.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageBubble.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isErrorOrFailed import io.getstream.chat.android.client.utils.message.isGiphyEphemeral @@ -48,7 +47,7 @@ public fun MessageBubble( color: Color, shape: Shape, modifier: Modifier = Modifier, - border: BorderStroke? = BorderStroke(1.dp, ChatTheme.colors.borders), + border: BorderStroke? = null, contentPadding: PaddingValues = PaddingValues(), content: @Composable () -> Unit, ) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index 588039798dc..8d74fc72670 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -19,7 +19,6 @@ package io.getstream.chat.android.compose.ui.messages.list import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -645,7 +644,7 @@ public fun RegularMessageContent( message = message, shape = messageBubbleShape, color = messageBubbleColor, - border = BorderStroke(1.dp, ChatTheme.colors.borders), + border = null, content = content, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt index b634f1117e0..973f4e09f2f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt @@ -210,7 +210,7 @@ public data class StreamDimens( attachmentsContentMediaGridSpacing = 2.dp, attachmentsContentVideoWidth = 250.dp, attachmentsContentGroupPreviewWidth = 250.dp, - attachmentsContentGroupPreviewHeight = 250.dp, + attachmentsContentGroupPreviewHeight = 196.dp, pollOptionInputHeight = 56.dp, messageComposerShadowElevation = 24.dp, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamShapes.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamShapes.kt index 2a431dadcfc..a472ce7bd58 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamShapes.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamShapes.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.unit.dp * @param imageThumbnail The shape of image thumbnails, shown in selected attachments and image file attachments. * @param bottomSheet The shape of components used as bottom sheets. * @param suggestionList The shape of suggestion list popup. - * @param attachmentSiteLabel The shape of the label showing website name over link attachments. * @param header The shape of the headers, such as the ones appearing on the Channel or Message screens. * @param quotedAttachment The shape of quoted attachments. */ @@ -48,7 +47,6 @@ public data class StreamShapes( public val imageThumbnail: Shape, public val bottomSheet: Shape, public val suggestionList: Shape, - public val attachmentSiteLabel: Shape, public val header: Shape, public val quotedAttachment: Shape, public val pollOptionInput: Shape, @@ -64,11 +62,10 @@ public data class StreamShapes( myMessageBubble = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 20.dp), otherMessageBubble = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomEnd = 20.dp), inputField = RoundedCornerShape(24.dp), - attachment = RoundedCornerShape(12.dp), + attachment = RoundedCornerShape(StreamTokens.radiusLg), imageThumbnail = RoundedCornerShape(8.dp), bottomSheet = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), suggestionList = RoundedCornerShape(16.dp), - attachmentSiteLabel = RoundedCornerShape(topEnd = 14.dp), header = RectangleShape, quotedAttachment = RoundedCornerShape(4.dp), pollOptionInput = RoundedCornerShape(16.dp), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt index 80be2d2ce5c..a5e2744425a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt @@ -36,6 +36,8 @@ internal object StreamTokens { val radius3xl = CornerSize(24.dp) val radius4xl = CornerSize(32.dp) + val size12 = 12.dp + val spacing3xs = 2.dp val spacing2xs = 4.dp val spacingXs = 8.dp diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_link_attachment_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_link_attachment_content.png index 8e6f1a5c03b..eaa48678ec1 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_link_attachment_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_link_attachment_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_multiple_media_attachment_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_multiple_media_attachment_content.png index 7aa8487da33..af6b46634f4 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_multiple_media_attachment_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_multiple_media_attachment_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_single_media_attachment_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_single_media_attachment_content.png index 6a24eb99a98..c04222760da 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_single_media_attachment_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.content_AttachmentsContentTest_single_media_attachment_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png index a7cf93b3cb3..5f9d594a443 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.list_RegularMessageContentTest_error_own_regular_content.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png index dea780b3b83..f97115ee8d5 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png index 98c03191b59..36351cf35aa 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageListTest_loaded_messages_in_dark_mode.png differ