From 0a50063638a6c7d7b1914419ffefd79f6a37637f Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 10 Jun 2025 08:59:46 +1000 Subject: [PATCH 01/23] Early setup for composable messages --- .../java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index 1af7cfb22c..bfb1ea9b4f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -39,5 +39,8 @@ data class Dimensions( val shapeSmall: Dp = 12.dp, val shapeMedium: Dp = 16.dp, + val messageCornerRadius: Dp = 19.dp, + val messageVerticalPadding: Dp = 10.dp, + val maxContentWidth: Dp = 410.dp, ) From ed6a32d207ba5de575cbf97706625444a20ee533 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 10 Jun 2025 09:00:08 +1000 Subject: [PATCH 02/23] Composable message base setup and todos --- .../v3/compose/MessageComposables.kt | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt new file mode 100644 index 0000000000..0e829e82ba --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -0,0 +1,143 @@ +package org.thoughtcrime.securesms.conversation.v3.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors + +//todo CONVOv3 basic bubble +//todo CONVOv3 basic message structure (bubble+avatar+name+status) +//todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) +//todo CONVOv3 text formatting in bubble including mentions and links +//todo CONVOv3 images +//todo CONVOv3 audio +//todo CONVOv3 links handling +//todo CONVOv3 typing indicator +//todo CONVOv3 long press views (overlay+message+recent reactions+menu) +//todo CONVOv3 reactions +//todo CONVOv3 quotes +//todo CONVOv3 control messages +//todo CONVOv3 time/date "separator" +//todo CONVOv3 bottom search +//todo CONVOv3 text input +//todo CONVOv3 voice recording +//todo CONVOv3 collapsible + menu for attachments +//todo CONVOv3 jump down to last message button +//todo CONVOv3 community invites +//todo CONVOv3 document view +//todo CONVOv3 attachment controls +//todo CONVOv3 deleted messages +//todo CONVOv3 swipe to reply +//todo CONVOv3 inputbar quote/reply +//todo CONVOv3 proper accessibility on overall message control + +/** + * Basic message building block: Bubble + */ +@Composable +fun MessageBubble( + color: Color, + modifier: Modifier = Modifier, + content: @Composable () -> Unit = {} +) { + Box( + modifier = modifier.background( + color = color, + shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius) + ) + .padding( + horizontal = LocalDimensions.current.smallSpacing, + vertical = LocalDimensions.current.messageVerticalPadding + ) + ) { + content() + } +} + +/** + * A message: Bubble with content, avatar, status + */ +@Composable +fun Message( + messageType: MessageType, + modifier: Modifier = Modifier +) { + //todo CONVOv3 handle start/end positioning based on outgoing/incoming + //todo CONVOv3 handle max width + //todo CONVOv3 update composable in Landing + Column( + modifier = modifier + ) { + MessageBubble( + color = if(messageType.outgoing) LocalColors.current.primary + else LocalColors.current.backgroundBubbleReceived + ) { + // Apply content based on message type + when(messageType) { + // Text messages + is MessageType.Text -> MessageText( + data = messageType + ) + } + } + } +} + +@Composable +fun MessageText( + data: MessageType.Text, + modifier: Modifier = Modifier +){ + //todo CONVOv3 handle look into textBubbleSent and textBubbleReceived and make sure they're needed vs reusing other existing values + Text( + modifier = modifier, + text = data.text, + style = LocalType.current.large, + color = if(data.outgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived, + ) +} + +sealed class MessageType(){ + abstract val outgoing: Boolean + data class Text( + override val outgoing: Boolean, + val text: AnnotatedString + ): MessageType() +} + +@Preview +@Composable +fun MessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.padding(LocalDimensions.current.spacing) + .width(600.dp) + ) { + Message(messageType = MessageType.Text(outgoing = true, AnnotatedString("Hi there"))) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(messageType = MessageType.Text(outgoing = false, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good"))) + } + } +} \ No newline at end of file From 9df1f96f04367459780ce7a4d80f57a26f3fdb8f Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 10 Jun 2025 09:09:44 +1000 Subject: [PATCH 03/23] Fixed colors --- .../conversation/v3/compose/MessageComposables.kt | 5 ++--- .../thoughtcrime/securesms/ui/theme/ThemeColors.kt | 14 +++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 0e829e82ba..165c2c0eb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -106,7 +106,6 @@ fun MessageText( data: MessageType.Text, modifier: Modifier = Modifier ){ - //todo CONVOv3 handle look into textBubbleSent and textBubbleReceived and make sure they're needed vs reusing other existing values Text( modifier = modifier, text = data.text, @@ -130,8 +129,8 @@ fun MessagePreview( ) { PreviewTheme(colors) { Column( - modifier = Modifier.padding(LocalDimensions.current.spacing) - .width(600.dp) + modifier = Modifier.width(600.dp).padding(LocalDimensions.current.spacing) + ) { Message(messageType = MessageType.Text(outgoing = true, AnnotatedString("Hi there"))) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColors.kt index 593905db92..e2a99d0ba5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColors.kt @@ -110,9 +110,9 @@ data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors override val text = classicDark6 override val textSecondary = classicDark5 override val borders = classicDark3 - override val textBubbleSent = Color.Black + override val textBubbleSent = classicDark0 override val backgroundBubbleReceived = classicDark2 - override val textBubbleReceived = Color.White + override val textBubbleReceived = classicDark6 override val qrCodeContent = background override val qrCodeBackground = text override val primaryButtonFillText = Color.Black @@ -131,9 +131,9 @@ data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColor override val text = classicLight0 override val textSecondary = classicLight1 override val borders = classicLight3 - override val textBubbleSent = text + override val textBubbleSent = classicLight0 override val backgroundBubbleReceived = classicLight4 - override val textBubbleReceived = classicLight4 + override val textBubbleReceived = classicLight0 override val qrCodeContent = text override val qrCodeBackground = backgroundSecondary override val primaryButtonFillText = Color.Black @@ -152,9 +152,9 @@ data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors { override val text = oceanDark7 override val textSecondary = oceanDark5 override val borders = oceanDark4 - override val textBubbleSent = Color.Black + override val textBubbleSent = oceanDark0 override val backgroundBubbleReceived = oceanDark4 - override val textBubbleReceived = oceanDark4 + override val textBubbleReceived = oceanDark7 override val qrCodeContent = background override val qrCodeBackground = text override val primaryButtonFillText = Color.Black @@ -173,7 +173,7 @@ data class OceanLight(override val primary: Color = primaryBlue) : ThemeColors { override val text = oceanLight1 override val textSecondary = oceanLight2 override val borders = oceanLight3 - override val textBubbleSent = text + override val textBubbleSent = oceanLight1 override val backgroundBubbleReceived = oceanLight4 override val textBubbleReceived = oceanLight1 override val qrCodeContent = text From 5123eb98217d95b3273404974dcf146d673a96e7 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 11 Jun 2025 09:11:57 +1000 Subject: [PATCH 04/23] More work on message composables --- .../v3/compose/MessageComposables.kt | 151 +++++++++++++++--- .../securesms/ui/theme/Dimensions.kt | 1 + 2 files changed, 127 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 165c2c0eb4..696d0b5bee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -1,30 +1,44 @@ package org.thoughtcrime.securesms.conversation.v3.compose +import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +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.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.max +import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.ui.theme.bold +import org.thoughtcrime.securesms.ui.theme.primaryBlue +import org.thoughtcrime.securesms.util.AvatarUIData +import org.thoughtcrime.securesms.util.AvatarUIElement -//todo CONVOv3 basic bubble -//todo CONVOv3 basic message structure (bubble+avatar+name+status) +//todo CONVOv3 status +//todo CONVOv3 status animated icon for disappearing messages //todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) //todo CONVOv3 text formatting in bubble including mentions and links //todo CONVOv3 images @@ -59,10 +73,11 @@ fun MessageBubble( content: @Composable () -> Unit = {} ) { Box( - modifier = modifier.background( - color = color, - shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius) - ) + modifier = modifier + .background( + color = color, + shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius) + ) .padding( horizontal = LocalDimensions.current.smallSpacing, vertical = LocalDimensions.current.messageVerticalPadding @@ -73,34 +88,90 @@ fun MessageBubble( } /** - * A message: Bubble with content, avatar, status + * A message content: Bubble with content, avatar, status */ @Composable -fun Message( - messageType: MessageType, +fun MessageContent( + data: MessageViewData, modifier: Modifier = Modifier ) { - //todo CONVOv3 handle start/end positioning based on outgoing/incoming - //todo CONVOv3 handle max width //todo CONVOv3 update composable in Landing + Column( - modifier = modifier + modifier = modifier, ) { - MessageBubble( - color = if(messageType.outgoing) LocalColors.current.primary - else LocalColors.current.backgroundBubbleReceived - ) { - // Apply content based on message type - when(messageType) { - // Text messages - is MessageType.Text -> MessageText( - data = messageType + Row { + if (data.avatar != null) { + Avatar( + modifier = Modifier.align(Alignment.Bottom), + size = LocalDimensions.current.iconMediumAvatar, + data = data.avatar ) + + Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing)) + } + + Column { + if (data.name != null) { + Text( + modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing), + text = data.name, + style = LocalType.current.base.bold(), + color = LocalColors.current.text + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) + } + + MessageBubble( + color = if (data.type.outgoing) LocalColors.current.primary + else LocalColors.current.backgroundBubbleReceived + ) { + // Apply content based on message type + when (data.type) { + // Text messages + is MessageType.Text -> MessageText( + data = data.type + ) + } + } } } + + // status + if (data.status != null) { + + } } } +/** + * The overall Message composable + */ +@Composable +fun Message( + data: MessageViewData, + modifier: Modifier = Modifier +) { + BoxWithConstraints( + modifier = modifier.fillMaxWidth() + ) { + val maxMessageWidth = max( + LocalDimensions.current.minMessageWidth, + maxWidth * 0.8f // 80% of available width + ) + + MessageContent( + modifier = Modifier + .align(if (data.type.outgoing) Alignment.CenterEnd else Alignment.CenterStart) + .widthIn(max = maxMessageWidth) + .wrapContentWidth(), + data = data + ) + } +} + + @Composable fun MessageText( data: MessageType.Text, @@ -114,6 +185,23 @@ fun MessageText( ) } +data class MessageViewData( + val avatar: AvatarUIData? = null, + val name: String? = null, + val status: MessageViewStatus? = null, + val type: MessageType +) + +data class MessageViewStatus( + val name: String, + val icon: MessageViewStatusIcon +) + +sealed interface MessageViewStatusIcon{ + data class DrawableIcon(@DrawableRes val icon: Int): MessageViewStatusIcon + data object DisappearingMessageIcon: MessageViewStatusIcon +} + sealed class MessageType(){ abstract val outgoing: Boolean data class Text( @@ -122,6 +210,7 @@ sealed class MessageType(){ ): MessageType() } +@PreviewScreenSizes @Preview @Composable fun MessagePreview( @@ -129,14 +218,26 @@ fun MessagePreview( ) { PreviewTheme(colors) { Column( - modifier = Modifier.width(600.dp).padding(LocalDimensions.current.spacing) + modifier = Modifier.fillMaxSize().padding(LocalDimensions.current.spacing) ) { - Message(messageType = MessageType.Text(outgoing = true, AnnotatedString("Hi there"))) + Message(data = MessageViewData( + type = MessageType.Text(outgoing = true, AnnotatedString("Hi there")) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + name = "Toto", + avatar = AvatarUIData(listOf(AvatarUIElement(name = "TO", color = primaryBlue))), + type = MessageType.Text(outgoing = false, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?")) + )) Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - Message(messageType = MessageType.Text(outgoing = false, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good"))) + Message(data = MessageViewData( + type = MessageType.Text(outgoing = true, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?")) + )) } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index bfb1ea9b4f..c6804a2eea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -20,6 +20,7 @@ data class Dimensions( val minItemButtonHeight: Dp = 50.dp, val minLargeItemButtonHeight: Dp = 60.dp, val minButtonWidth: Dp = 160.dp, + val minMessageWidth: Dp = 200.dp, val indicatorHeight: Dp = 4.dp, From b8292c2e404f3d82a28af1f751fdedc206225d00 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Sun, 22 Jun 2025 09:25:13 +1000 Subject: [PATCH 05/23] DocumentView --- .../v3/compose/MessageComposables.kt | 206 ++++++++++++++++-- .../thoughtcrime/securesms/ui/theme/Colors.kt | 1 + .../securesms/ui/theme/Dimensions.kt | 1 + .../main/res/layout/view_visible_message.xml | 1 + 4 files changed, 196 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 696d0b5bee..cc2d90a44b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -1,29 +1,44 @@ package org.thoughtcrime.securesms.conversation.v3.compose import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +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.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor 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.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.theme.LocalColors @@ -36,8 +51,10 @@ import org.thoughtcrime.securesms.ui.theme.bold import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUIElement +import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator +import org.thoughtcrime.securesms.ui.theme.blackAlpha06 -//todo CONVOv3 status //todo CONVOv3 status animated icon for disappearing messages //todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) //todo CONVOv3 text formatting in bubble including mentions and links @@ -56,12 +73,12 @@ import org.thoughtcrime.securesms.util.AvatarUIElement //todo CONVOv3 collapsible + menu for attachments //todo CONVOv3 jump down to last message button //todo CONVOv3 community invites -//todo CONVOv3 document view //todo CONVOv3 attachment controls //todo CONVOv3 deleted messages //todo CONVOv3 swipe to reply //todo CONVOv3 inputbar quote/reply //todo CONVOv3 proper accessibility on overall message control +//todo CONVOv3 new "read more" expandable feature /** * Basic message building block: Bubble @@ -78,10 +95,7 @@ fun MessageBubble( color = color, shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius) ) - .padding( - horizontal = LocalDimensions.current.smallSpacing, - vertical = LocalDimensions.current.messageVerticalPadding - ) + .clip(shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius)) ) { content() } @@ -124,13 +138,19 @@ fun MessageContent( } MessageBubble( - color = if (data.type.outgoing) LocalColors.current.primary + color = if (data.type.outgoing) LocalColors.current.accent else LocalColors.current.backgroundBubbleReceived ) { // Apply content based on message type when (data.type) { // Text messages is MessageType.Text -> MessageText( + modifier = Modifier.padding(defaultMessageBubblePadding()), + data = data.type + ) + + // Document messages + is MessageType.Document -> DocumentMessage( data = data.type ) } @@ -140,7 +160,12 @@ fun MessageContent( // status if (data.status != null) { - + Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + MessageStatus( + modifier = Modifier.align(Alignment.End) + .padding(horizontal = 2.dp), + data = data.status + ) } } } @@ -171,6 +196,38 @@ fun Message( } } +@Composable +fun MessageStatus( + data: MessageViewStatus, + modifier: Modifier = Modifier +){ + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = data.name, + style = LocalType.current.small, + color = LocalColors.current.text + ) + + when(data.icon){ + is MessageViewStatusIcon.DrawableIcon -> { + Image( + painter = painterResource(id = data.icon.icon), + colorFilter = ColorFilter.tint(LocalColors.current.text), + contentDescription = null, + modifier = Modifier.size(LocalDimensions.current.iconStatus) + ) + } + is MessageViewStatusIcon.DisappearingMessageIcon -> { + //todo Convov3 disappearing message icon + } + } + } +} + @Composable fun MessageText( @@ -181,15 +238,80 @@ fun MessageText( modifier = modifier, text = data.text, style = LocalType.current.large, - color = if(data.outgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived, + color = getTextColor(data.outgoing), ) } +@Composable +fun DocumentMessage( + data: MessageType.Document, + modifier: Modifier = Modifier +){ + Row( + modifier = modifier.height(IntrinsicSize.Min), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing) + ) { + // icon box + Box( + modifier = Modifier.fillMaxHeight() + .background(blackAlpha06) + .padding(horizontal = LocalDimensions.current.xsSpacing), + contentAlignment = Alignment.Center + ) { + if(data.loading){ + SmallCircularProgressIndicator(color = getTextColor(data.outgoing)) + } else { + Image( + painter = painterResource(id = R.drawable.ic_file), + contentDescription = null, + colorFilter = ColorFilter.tint(getTextColor(data.outgoing)), + modifier = Modifier.align(Alignment.Center).size(LocalDimensions.current.iconMedium) + ) + } + } + + val padding = defaultMessageBubblePadding() + Column( + modifier = Modifier.padding( + top = padding.calculateTopPadding(), + bottom = padding.calculateBottomPadding(), + end = padding.calculateEndPadding(LocalLayoutDirection.current) + ) + ) { + Text( + text = data.name, + style = LocalType.current.large, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = getTextColor(data.outgoing) + ) + + Text( + text = data.size, + style = LocalType.current.small, + color = getTextColor(data.outgoing) + ) + } + } +} + +@Composable +private fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent +else LocalColors.current.textBubbleReceived + +@Composable +private fun defaultMessageBubblePadding() = PaddingValues( + horizontal = LocalDimensions.current.smallSpacing, + vertical = LocalDimensions.current.messageVerticalPadding +) + data class MessageViewData( + val type: MessageType, val avatar: AvatarUIData? = null, val name: String? = null, val status: MessageViewStatus? = null, - val type: MessageType + val quote: MessageViewData? = null, ) data class MessageViewStatus( @@ -208,9 +330,16 @@ sealed class MessageType(){ override val outgoing: Boolean, val text: AnnotatedString ): MessageType() + + data class Document( + override val outgoing: Boolean, + val name: String, + val size: String, + val loading: Boolean + ): MessageType() } -@PreviewScreenSizes +/*@PreviewScreenSizes*/ @Preview @Composable fun MessagePreview( @@ -236,8 +365,59 @@ fun MessagePreview( Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) Message(data = MessageViewData( - type = MessageType.Text(outgoing = true, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?")) + type = MessageType.Text(outgoing = true, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?")), + status = MessageViewStatus( + name = "Sent", + icon = MessageViewStatusIcon.DrawableIcon(icon = R.drawable.ic_circle_check) + ) + )) + } + } +} + +@Preview +@Composable +fun DocumentMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + type = MessageType.Document( + outgoing = true, + name = "Document", + size = "5.4MB", + loading = false + )) + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + name = "Toto", + avatar = AvatarUIData(listOf(AvatarUIElement(name = "TO", color = primaryBlue))), + type = MessageType.Document( + outgoing = false, + name = "Document with a really long name that should ellepsize once it reaches the max width", + size = "5.4MB", + loading = false + ) )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + type = MessageType.Document( + outgoing = true, + name = "Another Document", + size = "7.8MB", + loading = true + )) + ) } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt index d9694eb0a6..e364726874 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt @@ -52,3 +52,4 @@ val disabledDark = Color(0xFFA1A2A1) val disabledLight = Color(0xFF6D6D6D) val blackAlpha40 = Color.Black.copy(alpha = 0.4f) +val blackAlpha06 = Color.Black.copy(alpha = 0.06f) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index c6804a2eea..7a07dfc70e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -27,6 +27,7 @@ data class Dimensions( val borderStroke: Dp = 1.dp, val iconXXSmall: Dp = 9.dp, + val iconStatus: Dp = 12.dp, val iconXSmall: Dp = 14.dp, val iconSmall: Dp = 20.dp, val iconMedium: Dp = 24.dp, diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml index 49214ff41a..acf41eeb77 100644 --- a/app/src/main/res/layout/view_visible_message.xml +++ b/app/src/main/res/layout/view_visible_message.xml @@ -137,6 +137,7 @@ android:layout_width="@dimen/message_spacing" android:layout_height="@dimen/message_spacing" android:layout_gravity="center" + tools:tint="?android:textColorPrimary" android:src="@drawable/ic_circle_check" /> Date: Sun, 22 Jun 2025 10:58:52 +1000 Subject: [PATCH 06/23] Quotes --- .../v3/compose/MessageComposables.kt | 341 +++++++++++++++--- .../securesms/ui/theme/Dimensions.kt | 3 +- .../thoughtcrime/securesms/ui/theme/Themes.kt | 4 +- 3 files changed, 299 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index cc2d90a44b..906487f566 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -20,10 +20,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -37,7 +35,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import org.thoughtcrime.securesms.ui.components.Avatar @@ -126,10 +123,10 @@ fun MessageContent( } Column { - if (data.name != null) { + if (data.displayName) { Text( modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing), - text = data.name, + text = data.author, style = LocalType.current.base.bold(), color = LocalColors.current.text ) @@ -141,18 +138,38 @@ fun MessageContent( color = if (data.type.outgoing) LocalColors.current.accent else LocalColors.current.backgroundBubbleReceived ) { - // Apply content based on message type - when (data.type) { - // Text messages - is MessageType.Text -> MessageText( - modifier = Modifier.padding(defaultMessageBubblePadding()), - data = data.type - ) - - // Document messages - is MessageType.Document -> DocumentMessage( - data = data.type - ) + Column { + // Display quote if there is one + if(data.quote != null){ + MessageQuote( + outgoing = data.type.outgoing, + quote = data.quote + ) + } + + // Apply content based on message type + when (data.type) { + // Text messages + is MessageType.Text -> MessageText( + modifier = Modifier.padding(defaultMessageBubblePadding()), + data = data.type + ) + + // Document messages + is MessageType.Document -> DocumentMessage( + data = data.type + ) + + // Audio messages + is MessageType.Audio -> { + //todo CONVOv3 audio message + } + + // Media messages + is MessageType.Media -> { + //todo CONVOv3 media message + } + } } } } @@ -228,6 +245,72 @@ fun MessageStatus( } } +@Composable +fun MessageQuote( + outgoing: Boolean, + quote: MessageQuote, + modifier: Modifier = Modifier +){ + Row( + modifier = modifier.height(IntrinsicSize.Min) + .padding(horizontal = LocalDimensions.current.xsSpacing) + .padding(top = LocalDimensions.current.xsSpacing), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing) + ) { + // icon + when(quote.icon){ + is MessageQuoteIcon.Bar -> { + Box( + modifier = Modifier.fillMaxHeight() + .background(color = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.accent) + .width(4.dp), + ) + } + + is MessageQuoteIcon.Icon -> { + Box( + modifier = Modifier.fillMaxHeight() + .background( + color = blackAlpha06, + shape = RoundedCornerShape(LocalDimensions.current.shapeXXS) + ) + .size(40.dp), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = quote.icon.icon), + contentDescription = null, + colorFilter = ColorFilter.tint(getTextColor(outgoing)), + modifier = Modifier.align(Alignment.Center).size(LocalDimensions.current.iconMedium) + ) + } + } + + is MessageQuoteIcon.Image -> { + //todo CONVOv3 quote image + } + } + + Column{ + Text( + text = quote.title, + style = LocalType.current.base.bold(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = getTextColor(outgoing) + ) + + Text( + text = quote.subtitle, + style = LocalType.current.base, + color = getTextColor(outgoing) + ) + } + } +} + + @Composable fun MessageText( @@ -308,12 +391,25 @@ private fun defaultMessageBubblePadding() = PaddingValues( data class MessageViewData( val type: MessageType, + val author: String, + val displayName: Boolean = false, val avatar: AvatarUIData? = null, - val name: String? = null, val status: MessageViewStatus? = null, - val quote: MessageViewData? = null, + val quote: MessageQuote? = null, +) + +data class MessageQuote( + val title: String, + val subtitle: String, + val icon: MessageQuoteIcon ) +sealed class MessageQuoteIcon(){ + data object Bar: MessageQuoteIcon() + data class Icon(@DrawableRes val icon: Int): MessageQuoteIcon() + data class Image(val uri: String): MessageQuoteIcon() +} + data class MessageViewStatus( val name: String, val icon: MessageViewStatusIcon @@ -335,10 +431,47 @@ sealed class MessageType(){ override val outgoing: Boolean, val name: String, val size: String, + val uri: String, + val loading: Boolean + ): MessageType() + + data class Audio( + override val outgoing: Boolean, + val name: String, + val time: String, + val uri: String, + val progress: Float, + val audioState: MessageAudioState + ): MessageType() + + data class Media( + override val outgoing: Boolean, + val items: List, val loading: Boolean ): MessageType() } +sealed class MessageMediaItem(){ + abstract val uri: String + abstract val filename: String + + data class Image( + override val uri: String, + override val filename: String + ): MessageMediaItem() + + data class Video( + override val uri: String, + override val filename: String + ): MessageMediaItem() +} + +sealed class MessageAudioState(){ + data object Loading: MessageAudioState() + data object Playing: MessageAudioState() + data object Paused: MessageAudioState() +} + /*@PreviewScreenSizes*/ @Preview @Composable @@ -351,25 +484,29 @@ fun MessagePreview( ) { Message(data = MessageViewData( - type = MessageType.Text(outgoing = true, AnnotatedString("Hi there")) + author = "Toto", + type = PreviewMessageData.text() )) Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) Message(data = MessageViewData( - name = "Toto", - avatar = AvatarUIData(listOf(AvatarUIElement(name = "TO", color = primaryBlue))), - type = MessageType.Text(outgoing = false, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?")) + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.text( + outgoing = false, + text = "Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?" + ) )) Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) Message(data = MessageViewData( - type = MessageType.Text(outgoing = true, AnnotatedString("Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?")), - status = MessageViewStatus( - name = "Sent", - icon = MessageViewStatusIcon.DrawableIcon(icon = R.drawable.ic_circle_check) - ) + author = "Toto", + type = PreviewMessageData.text( + text = "Hello, this is a message with multiple lines To test out styling and making sure it looks good but also continues for even longer as we are testing various screen width and I need to see how far it will go before reaching the max available width so there is a lot to say but also none of this needs to mean anything and yet here we are, are you still reading this by the way?" + ), + status = PreviewMessageData.sentStatus )) } } @@ -386,34 +523,26 @@ fun DocumentMessagePreview( ) { Message(data = MessageViewData( - type = MessageType.Document( - outgoing = true, - name = "Document", - size = "5.4MB", - loading = false - )) - ) + author = "Toto", + type = PreviewMessageData.document() + )) Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) Message(data = MessageViewData( - name = "Toto", - avatar = AvatarUIData(listOf(AvatarUIElement(name = "TO", color = primaryBlue))), - type = MessageType.Document( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.document( outgoing = false, - name = "Document with a really long name that should ellepsize once it reaches the max width", - size = "5.4MB", - loading = false + name = "Document with a really long name that should ellepsize once it reaches the max width" ) )) Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) Message(data = MessageViewData( - type = MessageType.Document( - outgoing = true, - name = "Another Document", - size = "7.8MB", + author = "Toto", + type = PreviewMessageData.document( loading = true )) ) @@ -421,3 +550,125 @@ fun DocumentMessagePreview( } } +@Preview +@Composable +fun QuoteMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(outgoing = false, text="Quoting text"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(text="Quoting text"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.text(outgoing = false, text="Quoting a document"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Icon(R.drawable.ic_file)) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Text(outgoing = true, AnnotatedString("Quoting audio")), + quote = PreviewMessageData.quote( + title = "You", + subtitle = "Audio message", + icon = MessageQuoteIcon.Icon(R.drawable.ic_mic) + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Text(outgoing = true, AnnotatedString("Quoting an image")), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Image("")) + )) + } + } +} + +private object PreviewMessageData { + + // Common data + val sampleAvatar = AvatarUIData(listOf(AvatarUIElement(name = "TO", color = primaryBlue))) + val sentStatus = MessageViewStatus( + name = "Sent", + icon = MessageViewStatusIcon.DrawableIcon(icon = R.drawable.ic_circle_check) + ) + + fun text( + text: String = "Hi there", + outgoing: Boolean = true + ) = MessageType.Text(outgoing = outgoing, AnnotatedString(text)) + + fun document( + name: String = "Document", + size: String = "5.4MB", + outgoing: Boolean = true, + loading: Boolean = false + ) = MessageType.Document( + outgoing = outgoing, + name = name, + size = size, + loading = loading, + uri = "" + ) + + fun audio( + name: String = "Audio", + time: String = "1:23", + outgoing: Boolean = true, + progress: Float = 0.5f, + state: MessageAudioState = MessageAudioState.Playing + ) = MessageType.Audio( + outgoing = outgoing, + name = name, + time = time, + uri = "", + progress = progress, + audioState = state + ) + + fun image() = MessageMediaItem.Image( + "", + "" + ) + + fun video() = MessageMediaItem.Video( + "", + "" + ) + + fun quote( + title: String = "Toto", + subtitle: String = "This is a quote", + icon: MessageQuoteIcon = MessageQuoteIcon.Bar + ) = MessageQuote( + title = title, + subtitle = subtitle + , + icon = icon + ) +} + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index 7a07dfc70e..0bf43e707b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -37,7 +37,8 @@ data class Dimensions( val iconXLarge: Dp = 60.dp, val iconXXLarge: Dp = 80.dp, - val shapeExtraSmall: Dp = 8.dp, + val shapeXXS: Dp = 4.dp, + val shapeXS: Dp = 8.dp, val shapeSmall: Dp = 12.dp, val shapeMedium: Dp = 16.dp, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 6c1395d1be..93e7ebd5a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -13,8 +13,6 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import org.session.libsession.utilities.AppTextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.ApplicationContext @@ -74,7 +72,7 @@ val buttonShape = pillShape @Composable fun sessionShapes() = Shapes( - extraSmall = RoundedCornerShape(LocalDimensions.current.shapeExtraSmall), + extraSmall = RoundedCornerShape(LocalDimensions.current.shapeXS), small = RoundedCornerShape(LocalDimensions.current.shapeSmall), medium = RoundedCornerShape(LocalDimensions.current.shapeMedium) From 40ec5e6e8b383a1cde28003e12ec646c7db9f9d9 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Sun, 22 Jun 2025 11:19:58 +1000 Subject: [PATCH 07/23] quote images --- .../v3/compose/MessageComposables.kt | 38 +++++++++++++++++-- .../securesms/ui/theme/Dimensions.kt | 2 + 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 906487f566..c37678654a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -1,8 +1,12 @@ +@file:OptIn(ExperimentalGlideComposeApi::class) + package org.thoughtcrime.securesms.conversation.v3.compose +import android.net.Uri import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -11,6 +15,7 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -29,14 +34,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -49,8 +58,10 @@ import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUIElement import network.loki.messenger.R +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.theme.blackAlpha06 +import androidx.core.net.toUri //todo CONVOv3 status animated icon for disappearing messages //todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) @@ -275,7 +286,7 @@ fun MessageQuote( color = blackAlpha06, shape = RoundedCornerShape(LocalDimensions.current.shapeXXS) ) - .size(40.dp), + .size(LocalDimensions.current.quoteIconSize), contentAlignment = Alignment.Center ) { Image( @@ -288,7 +299,15 @@ fun MessageQuote( } is MessageQuoteIcon.Image -> { - //todo CONVOv3 quote image + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier.background( + color = blackAlpha06, + shape = RoundedCornerShape(LocalDimensions.current.shapeXXS) + ).size(LocalDimensions.current.quoteIconSize), + model = DecryptableStreamUriLoader.DecryptableUri(quote.icon.uri), + contentDescription = quote.icon.filename + ) } } @@ -407,7 +426,10 @@ data class MessageQuote( sealed class MessageQuoteIcon(){ data object Bar: MessageQuoteIcon() data class Icon(@DrawableRes val icon: Int): MessageQuoteIcon() - data class Image(val uri: String): MessageQuoteIcon() + data class Image( + val uri: Uri, + val filename: String + ): MessageQuoteIcon() } data class MessageViewStatus( @@ -600,7 +622,7 @@ fun QuoteMessagePreview( Message(data = MessageViewData( author = "Toto", type = MessageType.Text(outgoing = true, AnnotatedString("Quoting an image")), - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Image("")) + quote = PreviewMessageData.quote(icon = PreviewMessageData.quoteImage()) )) } } @@ -668,6 +690,14 @@ private object PreviewMessageData { , icon = icon ) + + fun quoteImage( + uri: Uri = "".toUri(), + filename: String = "" + ) = MessageQuoteIcon.Image( + uri = uri, + filename = filename + ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index 0bf43e707b..5f1337bf1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -46,4 +46,6 @@ data class Dimensions( val messageVerticalPadding: Dp = 10.dp, val maxContentWidth: Dp = 410.dp, + + val quoteIconSize: Dp = 40.dp ) From 36b6831a52995adde747088de083dad440849214 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Sun, 22 Jun 2025 11:20:18 +1000 Subject: [PATCH 08/23] clean up --- .../conversation/v3/compose/MessageComposables.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index c37678654a..d370d87119 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -6,7 +6,6 @@ import android.net.Uri import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -37,31 +35,30 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max +import androidx.core.net.toUri import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage +import network.loki.messenger.R +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.ui.components.Avatar +import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.ui.theme.blackAlpha06 import org.thoughtcrime.securesms.ui.theme.bold import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUIElement -import network.loki.messenger.R -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader -import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator -import org.thoughtcrime.securesms.ui.theme.blackAlpha06 -import androidx.core.net.toUri //todo CONVOv3 status animated icon for disappearing messages //todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) @@ -72,7 +69,6 @@ import androidx.core.net.toUri //todo CONVOv3 typing indicator //todo CONVOv3 long press views (overlay+message+recent reactions+menu) //todo CONVOv3 reactions -//todo CONVOv3 quotes //todo CONVOv3 control messages //todo CONVOv3 time/date "separator" //todo CONVOv3 bottom search From b7c64f349f6048f2d580db6c54d9fc5ec654749f Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 30 Jun 2025 10:28:05 +1000 Subject: [PATCH 09/23] Link data and UI in mesasges --- .../v3/compose/MessageComposables.kt | 130 +++++++++++++++++- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index d370d87119..caf1d33a7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -36,12 +37,14 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.core.net.toUri +import com.bumptech.glide.integration.compose.CrossFade import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import network.loki.messenger.R @@ -65,7 +68,6 @@ import org.thoughtcrime.securesms.util.AvatarUIElement //todo CONVOv3 text formatting in bubble including mentions and links //todo CONVOv3 images //todo CONVOv3 audio -//todo CONVOv3 links handling //todo CONVOv3 typing indicator //todo CONVOv3 long press views (overlay+message+recent reactions+menu) //todo CONVOv3 reactions @@ -106,7 +108,7 @@ fun MessageBubble( } /** - * A message content: Bubble with content, avatar, status + * All the content of a message: Bubble with its internal content, avatar, status */ @Composable fun MessageContent( @@ -154,6 +156,15 @@ fun MessageContent( ) } + // display link data if any + if(data.link != null){ + MessageLink( + modifier = Modifier.padding(top = if(data.quote != null) LocalDimensions.current.xxsSpacing else 0.dp), + data = data.link, + outgoing = data.type.outgoing + ) + } + // Apply content based on message type when (data.type) { // Text messages @@ -196,6 +207,7 @@ fun MessageContent( /** * The overall Message composable + * This controls the width and position of the message as a whole */ @Composable fun Message( @@ -325,8 +337,6 @@ fun MessageQuote( } } - - @Composable fun MessageText( data: MessageType.Text, @@ -394,6 +404,53 @@ fun DocumentMessage( } } +@Composable +fun MessageLink( + data: MessageLinkData, + outgoing: Boolean, + modifier: Modifier = Modifier +){ + Row( + modifier = modifier.fillMaxWidth().background( + color = blackAlpha06 + ), + ) { + Box( + modifier = Modifier.size(100.dp) + .background(color = blackAlpha06) + ){ + if(data.imageUri == null){ + Image( + painter = painterResource(id = R.drawable.ic_link), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalColors.current.text), + modifier = Modifier.align(Alignment.Center) + ) + } else { + GlideImage( + model = data.imageUri, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = null, + transition = CrossFade, + ) + } + } + + Text( + modifier = Modifier.weight(1f) + .align(Alignment.CenterVertically) + .padding(horizontal = LocalDimensions.current.xsSpacing), + text = data.title, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + style = LocalType.current.base.bold(), + color = getTextColor(outgoing) + ) + } +} + @Composable private fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived @@ -411,6 +468,7 @@ data class MessageViewData( val avatar: AvatarUIData? = null, val status: MessageViewStatus? = null, val quote: MessageQuote? = null, + val link: MessageLinkData? = null ) data class MessageQuote( @@ -433,6 +491,12 @@ data class MessageViewStatus( val icon: MessageViewStatusIcon ) +data class MessageLinkData( + val url: String, + val title: String, + val imageUri: String? = null +) + sealed interface MessageViewStatusIcon{ data class DrawableIcon(@DrawableRes val icon: Int): MessageViewStatusIcon data object DisappearingMessageIcon: MessageViewStatusIcon @@ -624,6 +688,64 @@ fun QuoteMessagePreview( } } +@Preview +@Composable +fun LinkMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(outgoing = false, text="Quoting text"), + link = MessageLinkData( + url = "https://getsession.org/", + title = "Welcome to Session", + imageUri = null + ) + )) + + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(text="Quoting text"), + link = MessageLinkData( + url = "https://picsum.photos/id/0/367/267", + title = "Welcome to Session with a very long name", + imageUri = "https://picsum.photos/id/1/200/300" + ) + )) + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(outgoing = false, text="Quoting text"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + link = MessageLinkData( + url = "https://getsession.org/", + title = "Welcome to Session", + imageUri = null + ) + )) + + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(text="Quoting text"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + link = MessageLinkData( + url = "https://picsum.photos/id/0/367/267", + title = "Welcome to Session with a very long name", + imageUri = "https://picsum.photos/id/1/200/300" + ) + )) + } + } +} + private object PreviewMessageData { // Common data From 1ebd7fbc0d2a35641fc8a64ca0cce7fcf9aa474e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 3 Jul 2025 10:58:27 +1000 Subject: [PATCH 10/23] Using boxwithconstraint scope --- .../securesms/conversation/v3/compose/MessageComposables.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index caf1d33a7b..7cbcc5a3fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -219,7 +219,7 @@ fun Message( ) { val maxMessageWidth = max( LocalDimensions.current.minMessageWidth, - maxWidth * 0.8f // 80% of available width + this.maxWidth * 0.8f // 80% of available width ) MessageContent( From ae7f9d64d7a49a38e291adf91ea5fa9128bac855 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 3 Jul 2025 15:19:40 +1000 Subject: [PATCH 11/23] Audio UI and using our new composable in the Landing page --- .../v3/compose/MessageComposables.kt | 132 +++++++++++++++-- .../securesms/onboarding/landing/Landing.kt | 135 +++++++----------- .../thoughtcrime/securesms/ui/theme/Colors.kt | 1 + 3 files changed, 175 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 7cbcc5a3fb..b431585afd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -23,9 +23,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth 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 @@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.blackAlpha06 +import org.thoughtcrime.securesms.ui.theme.blackAlpha12 import org.thoughtcrime.securesms.ui.theme.bold import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.util.AvatarUIData @@ -67,7 +68,6 @@ import org.thoughtcrime.securesms.util.AvatarUIElement //todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) //todo CONVOv3 text formatting in bubble including mentions and links //todo CONVOv3 images -//todo CONVOv3 audio //todo CONVOv3 typing indicator //todo CONVOv3 long press views (overlay+message+recent reactions+menu) //todo CONVOv3 reactions @@ -115,8 +115,6 @@ fun MessageContent( data: MessageViewData, modifier: Modifier = Modifier ) { - //todo CONVOv3 update composable in Landing - Column( modifier = modifier, ) { @@ -179,9 +177,9 @@ fun MessageContent( ) // Audio messages - is MessageType.Audio -> { - //todo CONVOv3 audio message - } + is MessageType.Audio -> AudioMessage( + data = data.type + ) // Media messages is MessageType.Media -> { @@ -451,6 +449,74 @@ fun MessageLink( } } +@Composable +fun AudioMessage( + data: MessageType.Audio, + modifier: Modifier = Modifier +){ + Box( + modifier = modifier.width(160.dp) + .height(IntrinsicSize.Min), + ) { + // progress background + Box( + modifier = Modifier.fillMaxHeight() + .wrapContentWidth() + .fillMaxWidth(data.progress) + .background(blackAlpha12) + .align(Alignment.CenterStart) + ) + + // content + Row( + modifier = Modifier.padding(defaultMessageBubblePadding()), + verticalAlignment = Alignment.CenterVertically, + ) { + if(data.audioState == MessageAudioState.Loading){ + SmallCircularProgressIndicator(color = LocalColors.current.background) + } else { + Image( + painter = painterResource( + id = if (data.audioState == MessageAudioState.Paused) + R.drawable.exo_icon_play else R.drawable.exo_icon_pause + ), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalColors.current.text), + modifier = Modifier.size(LocalDimensions.current.iconMedium) + .background( + color = LocalColors.current.background, + shape = MaterialTheme.shapes.medium + ) + .padding(2.dp) + ) + } + + Box( + modifier = Modifier.weight(1f) + .height(1.dp) + .background(LocalColors.current.background) + + ) + + Text( + modifier = Modifier.background( + color = LocalColors.current.background, + shape = MaterialTheme.shapes.medium + ) + .padding( + horizontal = LocalDimensions.current.xxsSpacing, + vertical = LocalDimensions.current.xxxsSpacing + ), + text = data.time, + style = LocalType.current.base, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = LocalColors.current.text + ) + } + } +} + @Composable private fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived @@ -590,6 +656,17 @@ fun MessagePreview( ), status = PreviewMessageData.sentStatus )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.text( + outgoing = false, + text = "Hello" + ) + )) } } } @@ -746,6 +823,45 @@ fun LinkMessagePreview( } } +@Preview +@Composable +fun AudioMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.audio() + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.audio( + outgoing = false, + state = MessageAudioState.Loading, + name = "Audio with a really long name that should ellepsize once it reaches the max width" + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.audio( + state = MessageAudioState.Paused + )) + ) + } + } +} + private object PreviewMessageData { // Common data @@ -777,7 +893,7 @@ private object PreviewMessageData { name: String = "Audio", time: String = "1:23", outgoing: Boolean = true, - progress: Float = 0.5f, + progress: Float = 0.3f, state: MessageAudioState = MessageAudioState.Playing ) = MessageType.Audio( outgoing = outgoing, diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt index d620e7eebd..0af37a0a21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt @@ -29,7 +29,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -39,6 +41,9 @@ import kotlinx.coroutines.delay import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY +import org.thoughtcrime.securesms.conversation.v3.compose.Message +import org.thoughtcrime.securesms.conversation.v3.compose.MessageType +import org.thoughtcrime.securesms.conversation.v3.compose.MessageViewData import org.thoughtcrime.securesms.ui.AlertDialog import org.thoughtcrime.securesms.ui.DialogButtonData import org.thoughtcrime.securesms.ui.GetString @@ -73,6 +78,40 @@ internal fun LandingScreen( ) { var count by remember { mutableStateOf(0) } val listState = rememberLazyListState() + val context = LocalContext.current + + val messages = remember { + listOf( + MessageViewData( + type = MessageType.Text(text = AnnotatedString( + Phrase.from(context.getString(R.string.onboardingBubbleWelcomeToSession)) + .put(APP_NAME_KEY, context.getString(R.string.app_name)) + .put(EMOJI_KEY, "\uD83D\uDC4B") // this hardcoded emoji might be moved to NonTranslatableConstants eventually + .format().toString() + ), outgoing = false), + author = "Test" + ), + MessageViewData( + type = MessageType.Text(text = AnnotatedString( + Phrase.from(context.getString(R.string.onboardingBubbleSessionIsEngineered)) + .put(APP_NAME_KEY, context.getString(R.string.app_name)) + .format().toString()), outgoing = true), + author = "Test" + ), + MessageViewData( + type = MessageType.Text(text = AnnotatedString(context.getString(R.string.onboardingBubbleNoPhoneNumber)), outgoing = false), + author = "Test" + ), + MessageViewData( + type = MessageType.Text(text = AnnotatedString( + Phrase.from(context.getString(R.string.onboardingBubbleCreatingAnAccountIsEasy)) + .put(EMOJI_KEY, "\uD83D\uDC47") // this hardcoded emoji might be moved to NonTranslatableConstants eventually + .format().toString() + ), outgoing = true), + author = "Test" + ), + ) + } var isUrlDialogVisible by remember { mutableStateOf(false) } @@ -97,7 +136,7 @@ internal fun LandingScreen( LaunchedEffect(Unit) { delay(500.milliseconds) - while(count < MESSAGES.size) { + while(count < messages.size) { count += 1 listState.animateScrollToItem(0.coerceAtLeast((count - 1))) delay(1500L) @@ -116,7 +155,7 @@ internal fun LandingScreen( style = LocalType.current.h4, textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + Spacer(modifier = Modifier.weight(1f)) LazyColumn( state = listState, @@ -127,35 +166,12 @@ internal fun LandingScreen( verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { items( - MESSAGES.take(count), - key = { it.stringId } + messages.take(count), + key = { it.hashCode() } ) { item -> - // Perform string substitution only in the bubbles that require it - val bubbleTxt = when (item.stringId) { - R.string.onboardingBubbleWelcomeToSession -> { - Phrase.from(stringResource(item.stringId)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .put(EMOJI_KEY, "\uD83D\uDC4B") // this hardcoded emoji might be moved to NonTranslatableConstants eventually - .format().toString() - } - R.string.onboardingBubbleSessionIsEngineered -> { - Phrase.from(stringResource(item.stringId)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .format().toString() - } - R.string.onboardingBubbleCreatingAnAccountIsEasy -> { - Phrase.from(stringResource(item.stringId)) - .put(EMOJI_KEY, "\uD83D\uDC47") // this hardcoded emoji might be moved to NonTranslatableConstants eventually - .format().toString() - } - else -> { - stringResource(item.stringId) - } - } AnimateMessageText( - bubbleTxt, - item.isOutgoing + data = item ) } } @@ -195,67 +211,16 @@ internal fun LandingScreen( } @Composable -private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modifier = Modifier) { +private fun AnimateMessageText(data: MessageViewData, modifier: Modifier = Modifier) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { visible = true } - Box { - // TODO [SES-2077] Use LazyList itemAnimation when we update to compose 1.7 or so. - MessageText(text, isOutgoing, Modifier.alpha(0f)) - - AnimatedVisibility( - visible = visible, - enter = fadeIn(animationSpec = tween(durationMillis = 300)) + - slideInVertically(animationSpec = tween(durationMillis = 300)) { it } - ) { - MessageText(text, isOutgoing, modifier) - } - } -} - -@Composable -private fun MessageText(text: String, isOutgoing: Boolean, modifier: Modifier) { - Box(modifier = modifier then Modifier.fillMaxWidth()) { - MessageText( - text, - color = if (isOutgoing) LocalColors.current.accent else LocalColors.current.backgroundBubbleReceived, - textColor = if (isOutgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived, - modifier = Modifier.align(if (isOutgoing) Alignment.TopEnd else Alignment.TopStart) - ) - } -} - -@Composable -private fun MessageText( - text: String, - color: Color, - modifier: Modifier = Modifier, - textColor: Color = Color.Unspecified -) { - Box( - modifier = modifier.fillMaxWidth(0.666f) - .background(color = color, shape = MaterialTheme.shapes.small) + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(durationMillis = 300)) + + slideInVertically(animationSpec = tween(durationMillis = 300)) { it } ) { - Text( - text, - style = LocalType.current.large, - color = textColor, - modifier = Modifier.padding( - horizontal = LocalDimensions.current.smallSpacing, - vertical = LocalDimensions.current.xsSpacing - ) - ) + Message(data) } } -private data class TextData( - @StringRes val stringId: Int, - val isOutgoing: Boolean = false -) - -private val MESSAGES = listOf( - TextData(R.string.onboardingBubbleWelcomeToSession), - TextData(R.string.onboardingBubbleSessionIsEngineered, isOutgoing = true), - TextData(R.string.onboardingBubbleNoPhoneNumber), - TextData(R.string.onboardingBubbleCreatingAnAccountIsEasy, isOutgoing = true) -) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt index e364726874..e6f6c75c8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Colors.kt @@ -52,4 +52,5 @@ val disabledDark = Color(0xFFA1A2A1) val disabledLight = Color(0xFF6D6D6D) val blackAlpha40 = Color.Black.copy(alpha = 0.4f) +val blackAlpha12 = Color.Black.copy(alpha = 0.12f) val blackAlpha06 = Color.Black.copy(alpha = 0.06f) From 4e7fd2aed5d52cb755ee600492a4c5a9af62d427 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Sat, 5 Jul 2025 15:45:20 +1000 Subject: [PATCH 12/23] More compose messaging ui --- .../v3/compose/MessageComposables.kt | 335 +++++++++++++++--- 1 file changed, 294 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index b431585afd..d6f418eed2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -6,6 +6,7 @@ import android.net.Uri import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -36,6 +38,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -129,7 +132,9 @@ fun MessageContent( Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing)) } - Column { + Column( + horizontalAlignment = if(data.type.outgoing) Alignment.End else Alignment.Start + ) { if (data.displayName) { Text( modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing), @@ -141,36 +146,65 @@ fun MessageContent( Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) } - MessageBubble( - color = if (data.type.outgoing) LocalColors.current.accent - else LocalColors.current.backgroundBubbleReceived - ) { - Column { - // Display quote if there is one - if(data.quote != null){ - MessageQuote( - outgoing = data.type.outgoing, - quote = data.quote - ) - } + // There can be two bubbles in a message: First one contains quotes, links and message text + // The second one contains images, audio, video or documents + val hasFirstBubble = data.quote != null || data.link != null || data.type.text != null + val hasSecondBubble = data.type !is MessageType.Text + + // First bubble + if (hasFirstBubble) { + MessageBubble( + color = if (data.type.outgoing) LocalColors.current.accent + else LocalColors.current.backgroundBubbleReceived + ) { + Column { + // Display quote if there is one + if (data.quote != null) { + MessageQuote( + modifier = Modifier.padding(bottom = + if (data.link == null && data.type.text == null) + defaultMessageBubblePadding().calculateBottomPadding() + else 0.dp + ), + outgoing = data.type.outgoing, + quote = data.quote + ) + } - // display link data if any - if(data.link != null){ - MessageLink( - modifier = Modifier.padding(top = if(data.quote != null) LocalDimensions.current.xxsSpacing else 0.dp), - data = data.link, - outgoing = data.type.outgoing - ) + // display link data if any + if (data.link != null) { + MessageLink( + modifier = Modifier.padding(top = if (data.quote != null) LocalDimensions.current.xxsSpacing else 0.dp), + data = data.link, + outgoing = data.type.outgoing + ) + } + + if(data.type.text != null){ + // Text messages + MessageText( + modifier = Modifier.padding(defaultMessageBubblePadding()), + text = data.type.text!!, + outgoing = data.type.outgoing + ) + } } + } + } + + // Second bubble + if(hasSecondBubble){ + // add spacing if there is a first bubble + if(hasFirstBubble){ + Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + } + MessageBubble( + color = if (data.type.outgoing) LocalColors.current.accent + else LocalColors.current.backgroundBubbleReceived + ) { // Apply content based on message type when (data.type) { - // Text messages - is MessageType.Text -> MessageText( - modifier = Modifier.padding(defaultMessageBubblePadding()), - data = data.type - ) - // Document messages is MessageType.Document -> DocumentMessage( data = data.type @@ -183,11 +217,13 @@ fun MessageContent( // Media messages is MessageType.Media -> { - //todo CONVOv3 media message + } + + else -> {} } } - } + } } } @@ -337,14 +373,15 @@ fun MessageQuote( @Composable fun MessageText( - data: MessageType.Text, + text: AnnotatedString, + outgoing: Boolean, modifier: Modifier = Modifier ){ Text( modifier = modifier, - text = data.text, + text = text, style = LocalType.current.large, - color = getTextColor(data.outgoing), + color = getTextColor(outgoing), ) } @@ -517,6 +554,21 @@ fun AudioMessage( } } +@Composable +private fun MediaItems( + data: MessageType.Media, + modifier: Modifier = Modifier +){ + //todo CONVOv3 media items (1 vs 2 vs 3 items) + GlideImage( + contentScale = ContentScale.Crop, + modifier = Modifier + .aspectRatio(1f), + model =DecryptableStreamUriLoader.DecryptableUri(data.items[0].uri), + contentDescription = data.items[0].filename + ) +} + @Composable private fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived @@ -570,9 +622,11 @@ sealed interface MessageViewStatusIcon{ sealed class MessageType(){ abstract val outgoing: Boolean + abstract val text: AnnotatedString? + data class Text( override val outgoing: Boolean, - val text: AnnotatedString + override val text: AnnotatedString ): MessageType() data class Document( @@ -580,7 +634,8 @@ sealed class MessageType(){ val name: String, val size: String, val uri: String, - val loading: Boolean + val loading: Boolean, + override val text: AnnotatedString? = null ): MessageType() data class Audio( @@ -589,27 +644,29 @@ sealed class MessageType(){ val time: String, val uri: String, val progress: Float, - val audioState: MessageAudioState + val audioState: MessageAudioState, + override val text: AnnotatedString? = null ): MessageType() data class Media( override val outgoing: Boolean, val items: List, - val loading: Boolean + val loading: Boolean, + override val text: AnnotatedString? = null ): MessageType() } sealed class MessageMediaItem(){ - abstract val uri: String + abstract val uri: Uri abstract val filename: String data class Image( - override val uri: String, + override val uri: Uri, override val filename: String ): MessageMediaItem() data class Video( - override val uri: String, + override val uri: Uri, override val filename: String ): MessageMediaItem() } @@ -705,6 +762,27 @@ fun DocumentMessagePreview( loading = true )) ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = PreviewMessageData.document( + loading = true + )) + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = PreviewMessageData.document( + outgoing = false, + loading = true + )) + ) } } } @@ -761,6 +839,7 @@ fun QuoteMessagePreview( type = MessageType.Text(outgoing = true, AnnotatedString("Quoting an image")), quote = PreviewMessageData.quote(icon = PreviewMessageData.quoteImage()) )) + } } } @@ -862,6 +941,180 @@ fun AudioMessagePreview( } } +@Preview +@Composable +fun MediaMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.image(), PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.image(), PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + } + } +} + private object PreviewMessageData { // Common data @@ -877,7 +1130,7 @@ private object PreviewMessageData { ) = MessageType.Text(outgoing = outgoing, AnnotatedString(text)) fun document( - name: String = "Document", + name: String = "Document name", size: String = "5.4MB", outgoing: Boolean = true, loading: Boolean = false @@ -905,12 +1158,12 @@ private object PreviewMessageData { ) fun image() = MessageMediaItem.Image( - "", + "".toUri(), "" ) fun video() = MessageMediaItem.Video( - "", + "".toUri(), "" ) From ebd05e283b65a7f29cd7dce0733c8007f84853c1 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 1 Aug 2025 08:49:04 +1000 Subject: [PATCH 13/23] WIP media messages --- .../v3/compose/MessageComposables.kt | 101 +++--- .../conversation/v3/compose/MessageMedia.kt | 319 ++++++++++++++++++ 2 files changed, 358 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index d6f418eed2..9fbc0d903f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -6,7 +6,6 @@ import android.net.Uri import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -38,12 +36,12 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.core.net.toUri @@ -116,7 +114,8 @@ fun MessageBubble( @Composable fun MessageContent( data: MessageViewData, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + maxWidth: Dp ) { Column( modifier = modifier, @@ -147,7 +146,7 @@ fun MessageContent( } // There can be two bubbles in a message: First one contains quotes, links and message text - // The second one contains images, audio, video or documents + // The second one contains audio, document, images and video val hasFirstBubble = data.quote != null || data.link != null || data.type.text != null val hasSecondBubble = data.type !is MessageType.Text @@ -199,31 +198,34 @@ fun MessageContent( Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) } - MessageBubble( - color = if (data.type.outgoing) LocalColors.current.accent - else LocalColors.current.backgroundBubbleReceived - ) { - // Apply content based on message type - when (data.type) { - // Document messages - is MessageType.Document -> DocumentMessage( - data = data.type - ) - - // Audio messages - is MessageType.Audio -> AudioMessage( - data = data.type - ) + // images and videos are a special case and aren' actually surrounded in a visible bubble + if(data.type is MessageType.Media){ + MediaMessage( + data = data.type, + maxWidth = maxWidth + ) + } else { + MessageBubble( + color = if (data.type.outgoing) LocalColors.current.accent + else LocalColors.current.backgroundBubbleReceived + ) { + // Apply content based on message type + when (data.type) { + // Document messages + is MessageType.Document -> DocumentMessage( + data = data.type + ) - // Media messages - is MessageType.Media -> { + // Audio messages + is MessageType.Audio -> AudioMessage( + data = data.type + ) + else -> {} } - - else -> {} } } - } + } } } @@ -261,7 +263,8 @@ fun Message( .align(if (data.type.outgoing) Alignment.CenterEnd else Alignment.CenterStart) .widthIn(max = maxMessageWidth) .wrapContentWidth(), - data = data + data = data, + maxWidth = maxMessageWidth ) } } @@ -554,20 +557,7 @@ fun AudioMessage( } } -@Composable -private fun MediaItems( - data: MessageType.Media, - modifier: Modifier = Modifier -){ - //todo CONVOv3 media items (1 vs 2 vs 3 items) - GlideImage( - contentScale = ContentScale.Crop, - modifier = Modifier - .aspectRatio(1f), - model =DecryptableStreamUriLoader.DecryptableUri(data.items[0].uri), - contentDescription = data.items[0].filename - ) -} + @Composable private fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent @@ -656,21 +646,6 @@ sealed class MessageType(){ ): MessageType() } -sealed class MessageMediaItem(){ - abstract val uri: Uri - abstract val filename: String - - data class Image( - override val uri: Uri, - override val filename: String - ): MessageMediaItem() - - data class Video( - override val uri: Uri, - override val filename: String - ): MessageMediaItem() -} - sealed class MessageAudioState(){ data object Loading: MessageAudioState() data object Playing: MessageAudioState() @@ -966,7 +941,7 @@ fun MediaMessagePreview( author = "Toto", type = MessageType.Media( outgoing = false, - items = listOf(PreviewMessageData.image()), + items = listOf(PreviewMessageData.image(true)), loading = false ) )) @@ -1021,7 +996,7 @@ fun MediaMessagePreview( author = "Toto", type = MessageType.Media( outgoing = true, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(true), PreviewMessageData.image()), loading = false ) )) @@ -1115,7 +1090,7 @@ fun MediaMessagePreview( } } -private object PreviewMessageData { +object PreviewMessageData { // Common data val sampleAvatar = AvatarUIData(listOf(AvatarUIElement(name = "TO", color = primaryBlue))) @@ -1157,14 +1132,16 @@ private object PreviewMessageData { audioState = state ) - fun image() = MessageMediaItem.Image( + fun image(loading: Boolean = false) = MessageMediaItem.Image( "".toUri(), - "" + "", + loading = loading ) - fun video() = MessageMediaItem.Video( + fun video(loading: Boolean = false) = MessageMediaItem.Video( "".toUri(), - "" + "", + loading = loading ) fun quote( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt new file mode 100644 index 0000000000..5d32e785d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt @@ -0,0 +1,319 @@ +package org.thoughtcrime.securesms.conversation.v3.compose + +import android.net.Uri +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +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.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import okhttp3.internal.ws.MessageInflater +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors + +@Composable +fun MediaMessage( + data: MessageType.Media, + maxWidth: Dp, + modifier: Modifier = Modifier, +){ + Box( + modifier = modifier.clip(shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius)) + ) { + CALCULATE IMAGE SIZES - DYNAMIC FOR ONE BUT WITH A MIN + SET SIZE FOR 2 AND 3 + when (data.items.size) { + 1 -> { + MediaItem( + data = data.items[0], + maxWidth = maxWidth, + minSize = LocalDimensions.current.minMessageWidth + ) + } + + 2 -> { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + MediaItem( + data = data.items[0], + maxWidth = maxWidth, + minSize = LocalDimensions.current.minMessageWidth * 0.5f + ) + + MediaItem( + data = data.items[1], + maxWidth = maxWidth, + minSize = LocalDimensions.current.minMessageWidth * 0.5f + ) + } + } + + else -> { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + MediaItem( + data = data.items[0], + maxWidth = maxWidth, + minSize = LocalDimensions.current.minMessageWidth * 0.5f + ) + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + MediaItem( + data = data.items[1], + maxWidth = maxWidth, + minSize = LocalDimensions.current.minMessageWidth * 0.5f + ) + + MediaItem( + data = data.items[2], + maxWidth = maxWidth, + minSize = LocalDimensions.current.minMessageWidth * 0.5f + ) + } + } + } + } + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +private fun MediaItem( + data: MessageMediaItem, + modifier: Modifier = Modifier, + maxWidth: Dp, + minSize: Dp +){ + //todo CONVOv3 media items (1 vs 2 vs 3 items) + GlideImage( + contentScale = ContentScale.Crop, + modifier = modifier.widthIn(max = maxWidth, min = minSize) + .heightIn(max = maxWidth, min = minSize) // the image can only be as tall as the max width + .background(LocalColors.current.backgroundSecondary), + model = DecryptableStreamUriLoader.DecryptableUri(data.uri), + contentDescription = data.filename + ) +} + +@Preview +@Composable +fun MediaMessagePreviewLocal( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + .verticalScroll(rememberScrollState()) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.image(), PreviewMessageData.video()), + loading = false + ) + )) + + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.image(true)), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.image(), PreviewMessageData.video()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(true), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = true, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = MessageType.Media( + text = AnnotatedString("This also has text"), + outgoing = false, + items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), + loading = false + ) + )) + } + } +} + +sealed class MessageMediaItem(){ + abstract val uri: Uri + abstract val filename: String + abstract val loading: Boolean + + data class Image( + override val uri: Uri, + override val filename: String, + override val loading: Boolean, + ): MessageMediaItem() + + data class Video( + override val uri: Uri, + override val filename: String, + override val loading: Boolean, + ): MessageMediaItem() +} \ No newline at end of file From f5b9cdf41c080ee05130cb5b2cb149d454294818 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 4 Aug 2025 14:40:51 +1000 Subject: [PATCH 14/23] Media item sizing --- .../v3/compose/MessageComposables.kt | 21 +++-- .../conversation/v3/compose/MessageMedia.kt | 84 +++++++++++++------ 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 9fbc0d903f..492ad54661 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -68,7 +68,6 @@ import org.thoughtcrime.securesms.util.AvatarUIElement //todo CONVOv3 status animated icon for disappearing messages //todo CONVOv3 highlight effect (needs to work on all types and shapes (how should it work for combos like message + image? overall effect?) //todo CONVOv3 text formatting in bubble including mentions and links -//todo CONVOv3 images //todo CONVOv3 typing indicator //todo CONVOv3 long press views (overlay+message+recent reactions+menu) //todo CONVOv3 reactions @@ -1132,16 +1131,28 @@ object PreviewMessageData { audioState = state ) - fun image(loading: Boolean = false) = MessageMediaItem.Image( + fun image( + loading: Boolean = false, + width: Int = 100, + height: Int = 100, + ) = MessageMediaItem.Image( "".toUri(), "", - loading = loading + loading = loading, + width = width, + height = height ) - fun video(loading: Boolean = false) = MessageMediaItem.Video( + fun video( + loading: Boolean = false, + width: Int = 100, + height: Int = 100, + ) = MessageMediaItem.Video( "".toUri(), "", - loading = loading + loading = loading, + width = width, + height = height ) fun quote( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt index 5d32e785d2..59016013df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt @@ -13,7 +13,7 @@ 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.widthIn +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -28,7 +28,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage -import okhttp3.internal.ws.MessageInflater import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -45,58 +44,61 @@ fun MediaMessage( Box( modifier = modifier.clip(shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius)) ) { - CALCULATE IMAGE SIZES - DYNAMIC FOR ONE BUT WITH A MIN - SET SIZE FOR 2 AND 3 + val itemSpacing: Dp = 2.dp + when (data.items.size) { 1 -> { MediaItem( data = data.items[0], - maxWidth = maxWidth, - minSize = LocalDimensions.current.minMessageWidth + itemSize = MediaItemSize.AspectRatio( + minSize = LocalDimensions.current.minMessageWidth, + maxSize = maxWidth, + ), ) } 2 -> { Row( - horizontalArrangement = Arrangement.spacedBy(2.dp), + horizontalArrangement = Arrangement.spacedBy(itemSpacing), ) { + + val cellSize = maxWidth * 0.5f - itemSpacing * 0.5f + MediaItem( data = data.items[0], - maxWidth = maxWidth, - minSize = LocalDimensions.current.minMessageWidth * 0.5f + itemSize = MediaItemSize.SquareSize(size = cellSize), ) MediaItem( data = data.items[1], - maxWidth = maxWidth, - minSize = LocalDimensions.current.minMessageWidth * 0.5f + itemSize = MediaItemSize.SquareSize(size = cellSize), ) } } else -> { Row( - horizontalArrangement = Arrangement.spacedBy(2.dp), + horizontalArrangement = Arrangement.spacedBy(itemSpacing), ) { + val largeCellSize = maxWidth * 0.66f - itemSpacing * 0.5f + val smallCellSize = largeCellSize * 0.5f - itemSpacing * 0.5f + MediaItem( data = data.items[0], - maxWidth = maxWidth, - minSize = LocalDimensions.current.minMessageWidth * 0.5f + itemSize = MediaItemSize.SquareSize(size = largeCellSize), ) Column( - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(itemSpacing), ) { MediaItem( data = data.items[1], - maxWidth = maxWidth, - minSize = LocalDimensions.current.minMessageWidth * 0.5f + itemSize = MediaItemSize.SquareSize(size = smallCellSize), ) MediaItem( data = data.items[2], - maxWidth = maxWidth, - minSize = LocalDimensions.current.minMessageWidth * 0.5f + itemSize = MediaItemSize.SquareSize(size = smallCellSize), ) } } @@ -109,21 +111,41 @@ fun MediaMessage( @Composable private fun MediaItem( data: MessageMediaItem, + itemSize: MediaItemSize, modifier: Modifier = Modifier, - maxWidth: Dp, - minSize: Dp ){ - //todo CONVOv3 media items (1 vs 2 vs 3 items) + + var imageModifier: Modifier = modifier + .background(LocalColors.current.backgroundSecondary) + + when(itemSize){ + is MediaItemSize.SquareSize -> { + imageModifier = imageModifier.size(itemSize.size) + } + is MediaItemSize.AspectRatio -> { + val aspectRatio = data.width / data.height.toFloat() + val isLandscape = aspectRatio > 1f + + imageModifier = imageModifier.sizeIn( + maxWidth = itemSize.maxSize, minWidth = itemSize.minSize, + maxHeight = itemSize.maxSize, minHeight = itemSize.minSize + ).aspectRatio(aspectRatio, matchHeightConstraintsFirst = !isLandscape) + } + } + GlideImage( contentScale = ContentScale.Crop, - modifier = modifier.widthIn(max = maxWidth, min = minSize) - .heightIn(max = maxWidth, min = minSize) // the image can only be as tall as the max width - .background(LocalColors.current.backgroundSecondary), + modifier = imageModifier, model = DecryptableStreamUriLoader.DecryptableUri(data.uri), contentDescription = data.filename ) } +sealed interface MediaItemSize{ + data class SquareSize(val size: Dp): MediaItemSize + data class AspectRatio(val minSize: Dp, val maxSize: Dp): MediaItemSize +} + @Preview @Composable fun MediaMessagePreviewLocal( @@ -139,7 +161,10 @@ fun MediaMessagePreviewLocal( author = "Toto", type = MessageType.Media( outgoing = true, - items = listOf(PreviewMessageData.image()), + items = listOf(PreviewMessageData.image( + width = 50, + height = 100 + )), loading = false ) )) @@ -305,15 +330,22 @@ sealed class MessageMediaItem(){ abstract val filename: String abstract val loading: Boolean + abstract val width: Int + abstract val height: Int + data class Image( override val uri: Uri, override val filename: String, override val loading: Boolean, + override val width: Int, + override val height: Int, ): MessageMediaItem() data class Video( override val uri: Uri, override val filename: String, override val loading: Boolean, + override val width: Int, + override val height: Int, ): MessageMediaItem() } \ No newline at end of file From 4f4a2fc18808c4c20d04e93867c149b02b69cda4 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 19 Feb 2026 13:31:09 +1100 Subject: [PATCH 15/23] Tweaks --- .../securesms/conversation/v3/compose/MessageComposables.kt | 4 ++-- .../java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt | 2 +- .../main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 492ad54661..dbddfd4ec5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -328,7 +328,7 @@ fun MessageQuote( modifier = Modifier.fillMaxHeight() .background( color = blackAlpha06, - shape = RoundedCornerShape(LocalDimensions.current.shapeXXS) + shape = RoundedCornerShape(LocalDimensions.current.shapeXXSmall) ) .size(LocalDimensions.current.quoteIconSize), contentAlignment = Alignment.Center @@ -347,7 +347,7 @@ fun MessageQuote( contentScale = ContentScale.Crop, modifier = Modifier.background( color = blackAlpha06, - shape = RoundedCornerShape(LocalDimensions.current.shapeXXS) + shape = RoundedCornerShape(LocalDimensions.current.shapeXXSmall) ).size(LocalDimensions.current.quoteIconSize), model = DecryptableStreamUriLoader.DecryptableUri(quote.icon.uri), contentDescription = quote.icon.filename diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index 890df79db0..546b5422b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -47,7 +47,7 @@ data class Dimensions( val shapeSmall: Dp = 12.dp, val shapeMedium: Dp = 16.dp, - val messageCornerRadius: Dp = 19.dp, + val messageCornerRadius: Dp = 16.dp, val messageVerticalPadding: Dp = 10.dp, val maxContentWidth: Dp = 410.dp, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 6765aa5fc5..ac40f73d01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -73,7 +73,7 @@ val buttonShape = pillShape @Composable fun sessionShapes() = Shapes( - extraSmall = RoundedCornerShape(LocalDimensions.current.shapeXS), + extraSmall = RoundedCornerShape(LocalDimensions.current.shapeExtraSmall), small = RoundedCornerShape(LocalDimensions.current.shapeSmall), medium = RoundedCornerShape(LocalDimensions.current.shapeMedium) From 62836fd540adcb8a392803c322332e037740ff1e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 19 Feb 2026 13:32:32 +1100 Subject: [PATCH 16/23] new audio player todo --- .../securesms/conversation/v3/compose/MessageComposables.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index dbddfd4ec5..34d047e354 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -85,6 +85,7 @@ import org.thoughtcrime.securesms.util.AvatarUIElement //todo CONVOv3 inputbar quote/reply //todo CONVOv3 proper accessibility on overall message control //todo CONVOv3 new "read more" expandable feature +//todo CONVOv3 new audio player /** * Basic message building block: Bubble From b6d5557ca16a10291385951bc88e79a8fe677027 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 10:53:35 +1100 Subject: [PATCH 17/23] Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../securesms/conversation/v3/compose/MessageComposables.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 34d047e354..254d5d6a42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -585,7 +585,7 @@ data class MessageQuote( val icon: MessageQuoteIcon ) -sealed class MessageQuoteIcon(){ +sealed class MessageQuoteIcon { data object Bar: MessageQuoteIcon() data class Icon(@DrawableRes val icon: Int): MessageQuoteIcon() data class Image( From 907789dafc12c4e22c7fbbe940acc64fdaca5e99 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 10:53:49 +1100 Subject: [PATCH 18/23] Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../securesms/conversation/v3/compose/MessageMedia.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt index 59016013df..a164ef6389 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt @@ -325,7 +325,7 @@ fun MediaMessagePreviewLocal( } } -sealed class MessageMediaItem(){ +sealed class MessageMediaItem { abstract val uri: Uri abstract val filename: String abstract val loading: Boolean From c09bbc9b95e1fb686ab10914b07291965509f78f Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 10:59:07 +1100 Subject: [PATCH 19/23] Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../securesms/conversation/v3/compose/MessageComposables.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 254d5d6a42..7cc59d7730 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -725,7 +725,7 @@ fun DocumentMessagePreview( avatar = PreviewMessageData.sampleAvatar, type = PreviewMessageData.document( outgoing = false, - name = "Document with a really long name that should ellepsize once it reaches the max width" + name = "Document with a really long name that should ellipsize once it reaches the max width" ) )) From 8f4fee10265653d1d3abd941096d3d45c1c8116a Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 10:59:18 +1100 Subject: [PATCH 20/23] Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../securesms/conversation/v3/compose/MessageComposables.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 7cc59d7730..fe642ef128 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -900,7 +900,7 @@ fun AudioMessagePreview( type = PreviewMessageData.audio( outgoing = false, state = MessageAudioState.Loading, - name = "Audio with a really long name that should ellepsize once it reaches the max width" + name = "Audio with a really long name that should ellipsize once it reaches the max width" ) )) From d71c3b9cb1ec196ac2e7099c4450bb2564181baa Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 10:59:35 +1100 Subject: [PATCH 21/23] Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../securesms/conversation/v3/compose/MessageComposables.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index fe642ef128..24f3788bcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -1162,8 +1162,7 @@ object PreviewMessageData { icon: MessageQuoteIcon = MessageQuoteIcon.Bar ) = MessageQuote( title = title, - subtitle = subtitle - , + subtitle = subtitle, icon = icon ) From 1fff43b66f3a657816f361ef023503b3ef57d7b2 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 15:42:21 +1100 Subject: [PATCH 22/23] PR feedback + compose audio player --- .../conversation/v3/compose/AudioMessage.kt | 263 ++++++++++++++++++ .../v3/compose/MessageComposables.kt | 158 ++--------- .../securesms/onboarding/landing/Landing.kt | 2 +- 3 files changed, 291 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/AudioMessage.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/AudioMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/AudioMessage.kt new file mode 100644 index 0000000000..70543bf062 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/AudioMessage.kt @@ -0,0 +1,263 @@ +package org.thoughtcrime.securesms.conversation.v3.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Slider +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.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors + + +@Composable +fun AudioMessage( + data: Audio, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(vertical = LocalDimensions.current.smallSpacing), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxsSpacing) + ) { + + val textColor = getTextColor(data.outgoing) + + val (color1, color2, trackEmptyColor) = if (data.outgoing) { + arrayOf( + LocalColors.current.backgroundSecondary, // bg secondary + LocalColors.current.text, // text primary + LocalColors.current.backgroundSecondary.copy(alpha = 0.5f) + ) + } else { + arrayOf( + LocalColors.current.accent, // accent + LocalColors.current.background, // background primary + LocalColors.current.textSecondary // text secondary + + ) + } + + // Title + Text( + modifier = Modifier + .padding(start = LocalDimensions.current.smallSpacing, end = LocalDimensions.current.smallSpacing), + text = data.title, + style = LocalType.current.small.copy(fontStyle = FontStyle.Italic), + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + // play + seek + Row( + modifier = Modifier + .padding(horizontal = LocalDimensions.current.smallSpacing), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + PlayPauseButton( + isPlaying = data.isPlaying, + showLoader = data.showLoader, + bgColor = color1, + iconColor = color2, + onClick = { + //todo CONVOV3 implement + } + ) + + // Slider acts like SeekBar + val progress = + if (data.durationMs > 0) (data.positionMs.toFloat() / data.durationMs.toFloat()) + else 0f + + Slider( + modifier = Modifier.weight(1f), + value = progress.coerceIn(0f, 1f), + onValueChange = { + //todo CONVOV3 implement + }, + enabled = !data.showLoader, + valueRange = 0f..1f, + colors = androidx.compose.material3.SliderDefaults.colors( + thumbColor = color1, + activeTrackColor = color1, + inactiveTrackColor = trackEmptyColor + ) + ) + } + + // Bottom: speed chip + remaining + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = LocalDimensions.current.smallSpacing + 36.dp + LocalDimensions.current.smallSpacing, // aligns with slider start after play button + end = LocalDimensions.current.smallSpacing + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + PlaybackSpeedButton( + text = data.speedText, + bgColor = if (data.outgoing) color1 else color2, + textColor = if(data.outgoing) color2 else textColor, + onClick = { + //todo CONVOV3 implement + } + ) + + Text( + text = data.remainingText, + style = LocalType.current.small, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun PlayPauseButton( + isPlaying: Boolean, + showLoader: Boolean, + bgColor: Color, + iconColor: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + + Box( + modifier = modifier + .size(36.dp) + .clip(CircleShape) + .background(bgColor) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + if (showLoader) { + SmallCircularProgressIndicator(color = iconColor) + } else { + Image( + painter = painterResource( + id = if (isPlaying) R.drawable.pause else R.drawable.play + ), + contentDescription = null, + colorFilter = ColorFilter.tint(iconColor), + modifier = Modifier.size(16.dp) + ) + } + } +} + + +@Composable +private fun PlaybackSpeedButton( + text: String, + bgColor: Color, + textColor: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(LocalDimensions.current.shapeXXSmall)) + .background(bgColor) + .clickable(onClick = onClick) + .padding( + horizontal = LocalDimensions.current.xxsSpacing, + vertical = LocalDimensions.current.xxxsSpacing + ), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = LocalType.current.small, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Clip + ) + } +} + +data class Audio( + override val outgoing: Boolean, + override val text: AnnotatedString? = null, + val title: String, + val speedText: String, + val remainingText: String, + val durationMs: Long, // slider max reference + val positionMs: Long, // slider position + val bufferedPositionMs: Long = 0L, + val isPlaying: Boolean, + val showLoader: Boolean, +) : MessageType() + +@Preview +@Composable +fun AudioMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.audio() + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.audio( + outgoing = false, + title = "Audio with a really long name that should ellipsize once it reaches the max width", + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.audio( + playing = false + ) + )) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 24f3788bcf..75ee06c4a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -49,6 +49,11 @@ import com.bumptech.glide.integration.compose.CrossFade import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import network.loki.messenger.R +import org.session.libsession.utilities.Address +import org.session.libsignal.utilities.AccountId +import org.thoughtcrime.securesms.audio.model.AudioPlaybackState +import org.thoughtcrime.securesms.audio.model.PlayableAudio +import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator @@ -217,7 +222,7 @@ fun MessageContent( ) // Audio messages - is MessageType.Audio -> AudioMessage( + is Audio -> AudioMessage( data = data.type ) @@ -489,82 +494,13 @@ fun MessageLink( } } -@Composable -fun AudioMessage( - data: MessageType.Audio, - modifier: Modifier = Modifier -){ - Box( - modifier = modifier.width(160.dp) - .height(IntrinsicSize.Min), - ) { - // progress background - Box( - modifier = Modifier.fillMaxHeight() - .wrapContentWidth() - .fillMaxWidth(data.progress) - .background(blackAlpha12) - .align(Alignment.CenterStart) - ) - - // content - Row( - modifier = Modifier.padding(defaultMessageBubblePadding()), - verticalAlignment = Alignment.CenterVertically, - ) { - if(data.audioState == MessageAudioState.Loading){ - SmallCircularProgressIndicator(color = LocalColors.current.background) - } else { - Image( - painter = painterResource( - id = if (data.audioState == MessageAudioState.Paused) - R.drawable.exo_icon_play else R.drawable.exo_icon_pause - ), - contentDescription = null, - colorFilter = ColorFilter.tint(LocalColors.current.text), - modifier = Modifier.size(LocalDimensions.current.iconMedium) - .background( - color = LocalColors.current.background, - shape = MaterialTheme.shapes.medium - ) - .padding(2.dp) - ) - } - - Box( - modifier = Modifier.weight(1f) - .height(1.dp) - .background(LocalColors.current.background) - - ) - - Text( - modifier = Modifier.background( - color = LocalColors.current.background, - shape = MaterialTheme.shapes.medium - ) - .padding( - horizontal = LocalDimensions.current.xxsSpacing, - vertical = LocalDimensions.current.xxxsSpacing - ), - text = data.time, - style = LocalType.current.base, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = LocalColors.current.text - ) - } - } -} - - @Composable -private fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent +internal fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.textBubbleReceived @Composable -private fun defaultMessageBubblePadding() = PaddingValues( +internal fun defaultMessageBubblePadding() = PaddingValues( horizontal = LocalDimensions.current.smallSpacing, vertical = LocalDimensions.current.messageVerticalPadding ) @@ -628,16 +564,6 @@ sealed class MessageType(){ override val text: AnnotatedString? = null ): MessageType() - data class Audio( - override val outgoing: Boolean, - val name: String, - val time: String, - val uri: String, - val progress: Float, - val audioState: MessageAudioState, - override val text: AnnotatedString? = null - ): MessageType() - data class Media( override val outgoing: Boolean, val items: List, @@ -646,12 +572,6 @@ sealed class MessageType(){ ): MessageType() } -sealed class MessageAudioState(){ - data object Loading: MessageAudioState() - data object Playing: MessageAudioState() - data object Paused: MessageAudioState() -} - /*@PreviewScreenSizes*/ @Preview @Composable @@ -879,41 +799,10 @@ fun LinkMessagePreview( @Preview @Composable -fun AudioMessagePreview( +fun AudioMessagePreviewReuse( @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors ) { - PreviewTheme(colors) { - Column( - modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) - - ) { - Message(data = MessageViewData( - author = "Toto", - type = PreviewMessageData.audio() - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - avatar = PreviewMessageData.sampleAvatar, - type = PreviewMessageData.audio( - outgoing = false, - state = MessageAudioState.Loading, - name = "Audio with a really long name that should ellipsize once it reaches the max width" - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = PreviewMessageData.audio( - state = MessageAudioState.Paused - )) - ) - } - } + AudioMessagePreview(colors) } @Preview @@ -1118,18 +1007,25 @@ object PreviewMessageData { ) fun audio( - name: String = "Audio", - time: String = "1:23", outgoing: Boolean = true, - progress: Float = 0.3f, - state: MessageAudioState = MessageAudioState.Playing - ) = MessageType.Audio( + title: String = "Voice Message", + speedText: String = "1x", + remainingText: String = "0:20", + durationMs: Long = 83_000L, + positionMs: Long = 23_000L, + bufferedPositionMs: Long = 35_000L, + playing: Boolean = true, + showLoader: Boolean = false + ) = Audio( outgoing = outgoing, - name = name, - time = time, - uri = "", - progress = progress, - audioState = state + title = title, + speedText = speedText, + remainingText = remainingText, + durationMs = durationMs, + positionMs = positionMs, + bufferedPositionMs = bufferedPositionMs, + isPlaying = playing, + showLoader = showLoader, ) fun image( diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt index 08c5d94236..b0795d6455 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt @@ -70,7 +70,7 @@ internal fun LandingScreen( val listState = rememberLazyListState() val context = LocalContext.current - val messages = remember { + val messages = remember(context) { listOf( MessageViewData( type = MessageType.Text(text = AnnotatedString( From 570c4bc6665e8dcb2ffeccb09e2b4214dcbab773 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 20 Feb 2026 16:02:57 +1100 Subject: [PATCH 23/23] Removing Glide from the new Composables --- .../v3/compose/DocumentMessage.kt | 161 +++++++ .../v3/compose/MessageComposables.kt | 448 +----------------- .../conversation/v3/compose/MessageMedia.kt | 20 +- .../conversation/v3/compose/MessageQuote.kt | 175 +++++++ .../securesms/home/HomeActivity.kt | 9 - .../securesms/home/HomeAdapter.kt | 3 - 6 files changed, 363 insertions(+), 453 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/DocumentMessage.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageQuote.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/DocumentMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/DocumentMessage.kt new file mode 100644 index 0000000000..ce046bba2b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/DocumentMessage.kt @@ -0,0 +1,161 @@ +package org.thoughtcrime.securesms.conversation.v3.compose + +import androidx.compose.foundation.Image +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.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.fillMaxHeight +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.ui.theme.blackAlpha06 + + +@Composable +fun DocumentMessage( + data: Document, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.height(IntrinsicSize.Min), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing) + ) { + // icon box + Box( + modifier = Modifier + .fillMaxHeight() + .background(blackAlpha06) + .padding(horizontal = LocalDimensions.current.xsSpacing), + contentAlignment = Alignment.Center + ) { + if (data.loading) { + SmallCircularProgressIndicator(color = getTextColor(data.outgoing)) + } else { + Image( + painter = painterResource(id = R.drawable.ic_file), + contentDescription = null, + colorFilter = ColorFilter.tint(getTextColor(data.outgoing)), + modifier = Modifier + .align(Alignment.Center) + .size(LocalDimensions.current.iconMedium) + ) + } + } + + val padding = defaultMessageBubblePadding() + Column( + modifier = Modifier.padding( + top = padding.calculateTopPadding(), + bottom = padding.calculateBottomPadding(), + end = padding.calculateEndPadding(LocalLayoutDirection.current) + ) + ) { + Text( + text = data.name, + style = LocalType.current.large, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = getTextColor(data.outgoing) + ) + + Text( + text = data.size, + style = LocalType.current.small, + color = getTextColor(data.outgoing) + ) + } + } +} + +data class Document( + override val outgoing: Boolean, + val name: String, + val size: String, + val uri: String, + val loading: Boolean, + override val text: AnnotatedString? = null +) : MessageType() + +@Preview +@Composable +fun DocumentMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.document() + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.document( + outgoing = false, + name = "Document with a really long name that should ellipsize once it reaches the max width" + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.document( + loading = true + )) + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = PreviewMessageData.document( + loading = true + )) + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), + type = PreviewMessageData.document( + outgoing = false, + loading = true + )) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt index 75ee06c4a5..6d6750feb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -1,5 +1,3 @@ -@file:OptIn(ExperimentalGlideComposeApi::class) - package org.thoughtcrime.securesms.conversation.v3.compose import android.net.Uri @@ -10,12 +8,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -25,7 +20,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentWidth 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 @@ -34,7 +28,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -45,18 +39,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.core.net.toUri -import com.bumptech.glide.integration.compose.CrossFade -import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi -import com.bumptech.glide.integration.compose.GlideImage +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade import network.loki.messenger.R -import org.session.libsession.utilities.Address -import org.session.libsignal.utilities.AccountId -import org.thoughtcrime.securesms.audio.model.AudioPlaybackState -import org.thoughtcrime.securesms.audio.model.PlayableAudio -import org.thoughtcrime.securesms.database.model.MessageId -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.ui.components.Avatar -import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -64,7 +51,6 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.blackAlpha06 -import org.thoughtcrime.securesms.ui.theme.blackAlpha12 import org.thoughtcrime.securesms.ui.theme.bold import org.thoughtcrime.securesms.ui.theme.primaryBlue import org.thoughtcrime.securesms.util.AvatarUIData @@ -217,7 +203,7 @@ fun MessageContent( // Apply content based on message type when (data.type) { // Document messages - is MessageType.Document -> DocumentMessage( + is Document -> DocumentMessage( data = data.type ) @@ -306,79 +292,6 @@ fun MessageStatus( } } -@Composable -fun MessageQuote( - outgoing: Boolean, - quote: MessageQuote, - modifier: Modifier = Modifier -){ - Row( - modifier = modifier.height(IntrinsicSize.Min) - .padding(horizontal = LocalDimensions.current.xsSpacing) - .padding(top = LocalDimensions.current.xsSpacing), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing) - ) { - // icon - when(quote.icon){ - is MessageQuoteIcon.Bar -> { - Box( - modifier = Modifier.fillMaxHeight() - .background(color = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.accent) - .width(4.dp), - ) - } - - is MessageQuoteIcon.Icon -> { - Box( - modifier = Modifier.fillMaxHeight() - .background( - color = blackAlpha06, - shape = RoundedCornerShape(LocalDimensions.current.shapeXXSmall) - ) - .size(LocalDimensions.current.quoteIconSize), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(id = quote.icon.icon), - contentDescription = null, - colorFilter = ColorFilter.tint(getTextColor(outgoing)), - modifier = Modifier.align(Alignment.Center).size(LocalDimensions.current.iconMedium) - ) - } - } - - is MessageQuoteIcon.Image -> { - GlideImage( - contentScale = ContentScale.Crop, - modifier = Modifier.background( - color = blackAlpha06, - shape = RoundedCornerShape(LocalDimensions.current.shapeXXSmall) - ).size(LocalDimensions.current.quoteIconSize), - model = DecryptableStreamUriLoader.DecryptableUri(quote.icon.uri), - contentDescription = quote.icon.filename - ) - } - } - - Column{ - Text( - text = quote.title, - style = LocalType.current.base.bold(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = getTextColor(outgoing) - ) - - Text( - text = quote.subtitle, - style = LocalType.current.base, - color = getTextColor(outgoing) - ) - } - } -} - @Composable fun MessageText( text: AnnotatedString, @@ -393,60 +306,6 @@ fun MessageText( ) } -@Composable -fun DocumentMessage( - data: MessageType.Document, - modifier: Modifier = Modifier -){ - Row( - modifier = modifier.height(IntrinsicSize.Min), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing) - ) { - // icon box - Box( - modifier = Modifier.fillMaxHeight() - .background(blackAlpha06) - .padding(horizontal = LocalDimensions.current.xsSpacing), - contentAlignment = Alignment.Center - ) { - if(data.loading){ - SmallCircularProgressIndicator(color = getTextColor(data.outgoing)) - } else { - Image( - painter = painterResource(id = R.drawable.ic_file), - contentDescription = null, - colorFilter = ColorFilter.tint(getTextColor(data.outgoing)), - modifier = Modifier.align(Alignment.Center).size(LocalDimensions.current.iconMedium) - ) - } - } - - val padding = defaultMessageBubblePadding() - Column( - modifier = Modifier.padding( - top = padding.calculateTopPadding(), - bottom = padding.calculateBottomPadding(), - end = padding.calculateEndPadding(LocalLayoutDirection.current) - ) - ) { - Text( - text = data.name, - style = LocalType.current.large, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = getTextColor(data.outgoing) - ) - - Text( - text = data.size, - style = LocalType.current.small, - color = getTextColor(data.outgoing) - ) - } - } -} - @Composable fun MessageLink( data: MessageLinkData, @@ -470,12 +329,14 @@ fun MessageLink( modifier = Modifier.align(Alignment.Center) ) } else { - GlideImage( - model = data.imageUri, + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .crossfade(true) + .data(data.imageUri) + .build(), modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, contentDescription = null, - transition = CrossFade, ) } } @@ -555,15 +416,6 @@ sealed class MessageType(){ override val text: AnnotatedString ): MessageType() - data class Document( - override val outgoing: Boolean, - val name: String, - val size: String, - val uri: String, - val loading: Boolean, - override val text: AnnotatedString? = null - ): MessageType() - data class Media( override val outgoing: Boolean, val items: List, @@ -625,118 +477,18 @@ fun MessagePreview( @Preview @Composable -fun DocumentMessagePreview( +fun DocumentMessagePreviewReuse( @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors ) { - PreviewTheme(colors) { - Column( - modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) - - ) { - Message(data = MessageViewData( - author = "Toto", - type = PreviewMessageData.document() - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - avatar = PreviewMessageData.sampleAvatar, - type = PreviewMessageData.document( - outgoing = false, - name = "Document with a really long name that should ellipsize once it reaches the max width" - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = PreviewMessageData.document( - loading = true - )) - ) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), - type = PreviewMessageData.document( - loading = true - )) - ) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), - type = PreviewMessageData.document( - outgoing = false, - loading = true - )) - ) - } - } + DocumentMessagePreviewReuse(colors) } @Preview @Composable -fun QuoteMessagePreview( +fun QuoteMessagePreviewReuse( @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors ) { - PreviewTheme(colors) { - Column( - modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) - - ) { - Message(data = MessageViewData( - author = "Toto", - type = PreviewMessageData.text(outgoing = false, text="Quoting text"), - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = PreviewMessageData.text(text="Quoting text"), - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - avatar = PreviewMessageData.sampleAvatar, - type = PreviewMessageData.text(outgoing = false, text="Quoting a document"), - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Icon(R.drawable.ic_file)) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Text(outgoing = true, AnnotatedString("Quoting audio")), - quote = PreviewMessageData.quote( - title = "You", - subtitle = "Audio message", - icon = MessageQuoteIcon.Icon(R.drawable.ic_mic) - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Text(outgoing = true, AnnotatedString("Quoting an image")), - quote = PreviewMessageData.quote(icon = PreviewMessageData.quoteImage()) - )) - - } - } + QuoteMessagePreview(colors) } @Preview @@ -807,176 +559,10 @@ fun AudioMessagePreviewReuse( @Preview @Composable -fun MediaMessagePreview( +fun MediaMessagePreviewReuse( @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors ) { - PreviewTheme(colors) { - Column( - modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) - - ) { - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = true, - items = listOf(PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = false, - items = listOf(PreviewMessageData.image(true)), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = true, - items = listOf(PreviewMessageData.video()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = false, - items = listOf(PreviewMessageData.video()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = true, - items = listOf(PreviewMessageData.image(), PreviewMessageData.video()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = false, - items = listOf(PreviewMessageData.image(), PreviewMessageData.video()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = true, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(true), PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - outgoing = false, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - text = AnnotatedString("This also has text"), - outgoing = true, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - type = MessageType.Media( - text = AnnotatedString("This also has text"), - outgoing = false, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), - type = MessageType.Media( - outgoing = true, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), - type = MessageType.Media( - outgoing = false, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), - type = MessageType.Media( - text = AnnotatedString("This also has text"), - outgoing = true, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - Message(data = MessageViewData( - author = "Toto", - quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar), - type = MessageType.Media( - text = AnnotatedString("This also has text"), - outgoing = false, - items = listOf(PreviewMessageData.video(), PreviewMessageData.image(), PreviewMessageData.image()), - loading = false - ) - )) - } - } + MediaMessagePreview(colors) } object PreviewMessageData { @@ -998,7 +584,7 @@ object PreviewMessageData { size: String = "5.4MB", outgoing: Boolean = true, loading: Boolean = false - ) = MessageType.Document( + ) = Document( outgoing = outgoing, name = name, size = size, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt index a164ef6389..c317b7327d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio 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.sizeIn @@ -21,14 +20,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi -import com.bumptech.glide.integration.compose.GlideImage -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader +import coil3.compose.AsyncImage +import coil3.request.ImageRequest import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.PreviewTheme @@ -107,7 +106,6 @@ fun MediaMessage( } } -@OptIn(ExperimentalGlideComposeApi::class) @Composable private fun MediaItem( data: MessageMediaItem, @@ -133,11 +131,13 @@ private fun MediaItem( } } - GlideImage( + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(data.uri) + .build(), + contentDescription = data.filename, contentScale = ContentScale.Crop, - modifier = imageModifier, - model = DecryptableStreamUriLoader.DecryptableUri(data.uri), - contentDescription = data.filename + modifier = imageModifier ) } @@ -148,7 +148,7 @@ sealed interface MediaItemSize{ @Preview @Composable -fun MediaMessagePreviewLocal( +fun MediaMessagePreview( @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors ) { PreviewTheme(colors) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageQuote.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageQuote.kt new file mode 100644 index 0000000000..3d40d81f1b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageQuote.kt @@ -0,0 +1,175 @@ +package org.thoughtcrime.securesms.conversation.v3.compose + +import androidx.compose.foundation.Image +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.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.ui.theme.blackAlpha06 +import org.thoughtcrime.securesms.ui.theme.bold + +@Composable +fun MessageQuote( + outgoing: Boolean, + quote: MessageQuote, + modifier: Modifier = Modifier +){ + Row( + modifier = modifier.height(IntrinsicSize.Min) + .padding(horizontal = LocalDimensions.current.xsSpacing) + .padding(top = LocalDimensions.current.xsSpacing), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing) + ) { + // icon + when(quote.icon){ + is MessageQuoteIcon.Bar -> { + Box( + modifier = Modifier.fillMaxHeight() + .background(color = if(outgoing) LocalColors.current.textBubbleSent else LocalColors.current.accent) + .width(4.dp), + ) + } + + is MessageQuoteIcon.Icon -> { + Box( + modifier = Modifier.fillMaxHeight() + .background( + color = blackAlpha06, + shape = RoundedCornerShape(LocalDimensions.current.shapeXXSmall) + ) + .size(LocalDimensions.current.quoteIconSize), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = quote.icon.icon), + contentDescription = null, + colorFilter = ColorFilter.tint(getTextColor(outgoing)), + modifier = Modifier.align(Alignment.Center).size(LocalDimensions.current.iconMedium) + ) + } + } + + is MessageQuoteIcon.Image -> { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(quote.icon.uri) + .build(), + contentDescription = quote.icon.filename, + contentScale = ContentScale.Crop, + modifier = Modifier + .background( + color = blackAlpha06, + shape = RoundedCornerShape(LocalDimensions.current.shapeXXSmall) + ) + .size(LocalDimensions.current.quoteIconSize) + ) + } + } + + Column{ + Text( + text = quote.title, + style = LocalType.current.base.bold(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = getTextColor(outgoing) + ) + + Text( + text = quote.subtitle, + style = LocalType.current.base, + color = getTextColor(outgoing) + ) + } + } +} + +@Preview +@Composable +fun QuoteMessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(outgoing = false, text="Quoting text"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text(text="Quoting text"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Bar) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.text(outgoing = false, text="Quoting a document"), + quote = PreviewMessageData.quote(icon = MessageQuoteIcon.Icon(R.drawable.ic_file)) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Text(outgoing = true, AnnotatedString("Quoting audio")), + quote = PreviewMessageData.quote( + title = "You", + subtitle = "Audio message", + icon = MessageQuoteIcon.Icon(R.drawable.ic_mic) + ) + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + type = MessageType.Text(outgoing = true, AnnotatedString("Quoting an image")), + quote = PreviewMessageData.quote(icon = PreviewMessageData.quoteImage()) + )) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 088ac79634..167f6ab94f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -10,14 +10,11 @@ import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.activity.OnBackPressedCallback -import androidx.activity.compose.LocalActivity import androidx.activity.viewModels -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.EnterTransition import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.tween -import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -36,8 +33,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestManager import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers @@ -137,7 +132,6 @@ class HomeActivity : ScreenLockActionBarActivity(), private val TAG = "HomeActivity" private lateinit var binding: ActivityHomeBinding - private lateinit var glide: RequestManager @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase @@ -241,8 +235,6 @@ class HomeActivity : ScreenLockActionBarActivity(), setContentView(binding.root) // Set custom toolbar setSupportActionBar(binding.toolbar) - // Set up Glide - glide = Glide.with(this) // Set up toolbar buttons binding.profileButton.setThemedContent { val recipient by recipientRepository.observeSelf() @@ -336,7 +328,6 @@ class HomeActivity : ScreenLockActionBarActivity(), // Set up recycler view binding.globalSearchInputLayout.listener = this homeAdapter.setHasStableIds(true) - homeAdapter.glide = glide binding.conversationsRecyclerView.adapter = homeAdapter binding.globalSearchRecycler.adapter = globalSearchAdapter diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 35c9d01637..e1aaf085cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID -import com.bumptech.glide.RequestManager import network.loki.messenger.R import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import org.thoughtcrime.securesms.conversation.v2.messages.MessageFormatter @@ -42,8 +41,6 @@ class HomeAdapter( } } - lateinit var glide: RequestManager - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when (viewType) { ITEM_TYPE_MESSAGE_REQUESTS -> {