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/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 new file mode 100644 index 0000000000..6d6750feb5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageComposables.kt @@ -0,0 +1,661 @@ +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.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.PaddingValues +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.size +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.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.LocalContext +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.dp +import androidx.compose.ui.unit.max +import androidx.core.net.toUri +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import network.loki.messenger.R +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.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 + +//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 typing indicator +//todo CONVOv3 long press views (overlay+message+recent reactions+menu) +//todo CONVOv3 reactions +//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 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 +//todo CONVOv3 new audio player + +/** + * 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) + ) + .clip(shape = RoundedCornerShape(LocalDimensions.current.messageCornerRadius)) + ) { + content() + } +} + +/** + * All the content of a message: Bubble with its internal content, avatar, status + */ +@Composable +fun MessageContent( + data: MessageViewData, + modifier: Modifier = Modifier, + maxWidth: Dp +) { + Column( + modifier = modifier, + ) { + 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( + horizontalAlignment = if(data.type.outgoing) Alignment.End else Alignment.Start + ) { + if (data.displayName) { + Text( + modifier = Modifier.padding(start = LocalDimensions.current.smallSpacing), + text = data.author, + style = LocalType.current.base.bold(), + color = LocalColors.current.text + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) + } + + // There can be two bubbles in a message: First one contains quotes, links and message text + // 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 + + // 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 + ) + } + + 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)) + } + + // 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 Document -> DocumentMessage( + data = data.type + ) + + // Audio messages + is Audio -> AudioMessage( + data = data.type + ) + + else -> {} + } + } + } + } + } + } + + // 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 + ) + } + } +} + +/** + * The overall Message composable + * This controls the width and position of the message as a whole + */ +@Composable +fun Message( + data: MessageViewData, + modifier: Modifier = Modifier +) { + BoxWithConstraints( + modifier = modifier.fillMaxWidth() + ) { + val maxMessageWidth = max( + LocalDimensions.current.minMessageWidth, + this.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, + maxWidth = maxMessageWidth + ) + } +} + +@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( + text: AnnotatedString, + outgoing: Boolean, + modifier: Modifier = Modifier +){ + Text( + modifier = modifier, + text = text, + style = LocalType.current.large, + color = getTextColor(outgoing), + ) +} + +@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 { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .crossfade(true) + .data(data.imageUri) + .build(), + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } + } + + 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 +internal fun getTextColor(outgoing: Boolean) = if(outgoing) LocalColors.current.textBubbleSent +else LocalColors.current.textBubbleReceived + +@Composable +internal fun defaultMessageBubblePadding() = PaddingValues( + horizontal = LocalDimensions.current.smallSpacing, + vertical = LocalDimensions.current.messageVerticalPadding +) + +data class MessageViewData( + val type: MessageType, + val author: String, + val displayName: Boolean = false, + val avatar: AvatarUIData? = null, + val status: MessageViewStatus? = null, + val quote: MessageQuote? = null, + val link: MessageLinkData? = 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: Uri, + val filename: String + ): MessageQuoteIcon() +} + +data class MessageViewStatus( + val name: String, + 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 +} + +sealed class MessageType(){ + abstract val outgoing: Boolean + abstract val text: AnnotatedString? + + data class Text( + override val outgoing: Boolean, + override val text: AnnotatedString + ): MessageType() + + data class Media( + override val outgoing: Boolean, + val items: List, + val loading: Boolean, + override val text: AnnotatedString? = null + ): MessageType() +} + +/*@PreviewScreenSizes*/ +@Preview +@Composable +fun MessagePreview( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + Column( + modifier = Modifier.fillMaxSize().padding(LocalDimensions.current.spacing) + + ) { + Message(data = MessageViewData( + author = "Toto", + type = PreviewMessageData.text() + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + 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( + 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 + )) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + Message(data = MessageViewData( + author = "Toto", + avatar = PreviewMessageData.sampleAvatar, + type = PreviewMessageData.text( + outgoing = false, + text = "Hello" + ) + )) + } + } +} + +@Preview +@Composable +fun DocumentMessagePreviewReuse( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + DocumentMessagePreviewReuse(colors) +} + +@Preview +@Composable +fun QuoteMessagePreviewReuse( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + QuoteMessagePreview(colors) +} + +@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" + ) + )) + } + } +} + +@Preview +@Composable +fun AudioMessagePreviewReuse( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + AudioMessagePreview(colors) +} + +@Preview +@Composable +fun MediaMessagePreviewReuse( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + MediaMessagePreview(colors) +} + +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 name", + size: String = "5.4MB", + outgoing: Boolean = true, + loading: Boolean = false + ) = Document( + outgoing = outgoing, + name = name, + size = size, + loading = loading, + uri = "" + ) + + fun audio( + outgoing: Boolean = true, + 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, + title = title, + speedText = speedText, + remainingText = remainingText, + durationMs = durationMs, + positionMs = positionMs, + bufferedPositionMs = bufferedPositionMs, + isPlaying = playing, + showLoader = showLoader, + ) + + fun image( + loading: Boolean = false, + width: Int = 100, + height: Int = 100, + ) = MessageMediaItem.Image( + "".toUri(), + "", + loading = loading, + width = width, + height = height + ) + + fun video( + loading: Boolean = false, + width: Int = 100, + height: Int = 100, + ) = MessageMediaItem.Video( + "".toUri(), + "", + loading = loading, + width = width, + height = height + ) + + fun quote( + title: String = "Toto", + subtitle: String = "This is a quote", + icon: MessageQuoteIcon = MessageQuoteIcon.Bar + ) = MessageQuote( + title = title, + subtitle = subtitle, + 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/conversation/v3/compose/MessageMedia.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt new file mode 100644 index 0000000000..c317b7327d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/MessageMedia.kt @@ -0,0 +1,351 @@ +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +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.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 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 +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)) + ) { + val itemSpacing: Dp = 2.dp + + when (data.items.size) { + 1 -> { + MediaItem( + data = data.items[0], + itemSize = MediaItemSize.AspectRatio( + minSize = LocalDimensions.current.minMessageWidth, + maxSize = maxWidth, + ), + ) + } + + 2 -> { + Row( + horizontalArrangement = Arrangement.spacedBy(itemSpacing), + ) { + + val cellSize = maxWidth * 0.5f - itemSpacing * 0.5f + + MediaItem( + data = data.items[0], + itemSize = MediaItemSize.SquareSize(size = cellSize), + ) + + MediaItem( + data = data.items[1], + itemSize = MediaItemSize.SquareSize(size = cellSize), + ) + } + } + + else -> { + Row( + 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], + itemSize = MediaItemSize.SquareSize(size = largeCellSize), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(itemSpacing), + ) { + MediaItem( + data = data.items[1], + itemSize = MediaItemSize.SquareSize(size = smallCellSize), + ) + + MediaItem( + data = data.items[2], + itemSize = MediaItemSize.SquareSize(size = smallCellSize), + ) + } + } + } + } + } +} + +@Composable +private fun MediaItem( + data: MessageMediaItem, + itemSize: MediaItemSize, + modifier: Modifier = Modifier, +){ + + 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) + } + } + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(data.uri) + .build(), + contentDescription = data.filename, + contentScale = ContentScale.Crop, + modifier = imageModifier + ) +} + +sealed interface MediaItemSize{ + data class SquareSize(val size: Dp): MediaItemSize + data class AspectRatio(val minSize: Dp, val maxSize: Dp): MediaItemSize +} + +@Preview +@Composable +fun MediaMessagePreview( + @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( + width = 50, + height = 100 + )), + 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 + + 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 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 -> { 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 d2c589a1b6..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 @@ -1,13 +1,10 @@ package org.thoughtcrime.securesms.onboarding.landing -import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInVertically -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.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -17,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,9 +24,9 @@ import androidx.compose.runtime.retain.retain import androidx.compose.runtime.setValue 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 @@ -40,12 +36,14 @@ 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.components.BorderlessHtmlButton import org.thoughtcrime.securesms.ui.TCPolicyDialog import org.thoughtcrime.securesms.ui.components.AccentFillButton import org.thoughtcrime.securesms.ui.components.AccentOutlineButton -import org.thoughtcrime.securesms.ui.components.BorderlessHtmlButton import org.thoughtcrime.securesms.ui.qaTag -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 @@ -70,6 +68,40 @@ internal fun LandingScreen( ) { var count by remember { mutableStateOf(0) } val listState = rememberLazyListState() + val context = LocalContext.current + + val messages = remember(context) { + 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 retain { mutableStateOf(false) } @@ -83,7 +115,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) @@ -102,7 +134,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, @@ -113,35 +145,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 ) } } @@ -181,66 +190,17 @@ 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 retain { mutableStateOf(false) } - LaunchedEffect(Unit) { visible = true } - Box { - 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) - ) - } -} + LaunchedEffect(Unit) { visible = true } -@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 fb69a66575..1e607ddaf5 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,5 @@ val disabledLight = Color(0xFFA1A2A1) val disabledDark = 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) 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 ba6e34e410..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 @@ -23,11 +23,13 @@ data class Dimensions( val minItemButtonHeight: Dp = 60.dp, val minButtonWidth: Dp = 160.dp, val minSmallButtonWidth: Dp = 50.dp, + val minMessageWidth: Dp = 200.dp, val indicatorHeight: Dp = 4.dp, val borderStroke: Dp = 1.dp, + val iconStatus: Dp = 12.dp, val iconXXSmall: Dp = 10.dp, val iconXSmall: Dp = 14.dp, val iconSmall: Dp = 20.dp, @@ -45,7 +47,13 @@ data class Dimensions( val shapeSmall: Dp = 12.dp, val shapeMedium: Dp = 16.dp, + val messageCornerRadius: Dp = 16.dp, + val messageVerticalPadding: Dp = 10.dp, + val maxContentWidth: Dp = 410.dp, + + val quoteIconSize: Dp = 40.dp, + val maxDialogWidth: Dp = 560.dp, val maxTooltipWidth: Dp = 280.dp, 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 fa3733bc9b..7506f55ed3 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 @@ -122,9 +122,9 @@ data class ClassicDark(override val accent: 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 textOnAccent = Color.Black @@ -144,9 +144,9 @@ data class ClassicLight(override val accent: Color = primaryGreen) : ThemeColors 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 textOnAccent = Color.Black @@ -166,9 +166,9 @@ data class OceanDark(override val accent: 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 textOnAccent = Color.Black @@ -188,7 +188,7 @@ data class OceanLight(override val accent: 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 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 aa2ef6f7cc..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 @@ -14,8 +14,6 @@ import androidx.compose.runtime.staticCompositionLocalOf 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 diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml index 4cc261e508..04a29c8c82 100644 --- a/app/src/main/res/layout/view_visible_message.xml +++ b/app/src/main/res/layout/view_visible_message.xml @@ -118,6 +118,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" />