diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 8e29f3d25c..e8ed22a1fb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventSendStat import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlin.random.Random fun aTimelineState(timelineItems: ImmutableList = persistentListOf()) = TimelineState( @@ -102,6 +103,7 @@ internal fun aTimelineItemEvent( sendState: EventSendState = EventSendState.Sent(eventId), inReplyTo: InReplyTo? = null, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), + timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(isMine = isMine), ): TimelineItem.Event { return TimelineItem.Event( id = eventId.value, @@ -110,11 +112,7 @@ internal fun aTimelineItemEvent( senderId = UserId("@senderId:domain"), senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender), content = content, - reactionsState = TimelineItemReactions( - persistentListOf( - AggregatedReaction("👍", "1", isOnMyMessage = isMine) - ) - ), + reactionsState = timelineItemReactions, sentTime = "12:34", isMine = isMine, senderDisplayName = "Sender", @@ -125,6 +123,19 @@ internal fun aTimelineItemEvent( ) } +fun aTimelineItemReactions( + count: Int = 1, + isMine: Boolean = true, +): TimelineItemReactions { + return TimelineItemReactions( + reactions = buildList { + repeat(count) { + add(AggregatedReaction(key = "👍", count = (it + 1).toString(), isOnMyMessage = isMine)) + } + }.toPersistentList() + ) +} + internal fun aTimelineItemDebugInfo( model: String = "Rust(Model())", originalJson: String? = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index d86e41d54c..e743ba3b3c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -46,13 +46,17 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView +import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent @@ -73,6 +77,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import org.jsoup.Jsoup @Composable fun TimelineItemEventRow( @@ -228,6 +233,7 @@ private fun MessageEventBubbleContent( interactionSource = interactionSource, onClick = onMessageClick, onLongClick = onMessageLongClick, + extraPadding = event.toExtraPadding(), modifier = modifier, ) } @@ -437,3 +443,44 @@ private fun ContentToPreview() { } } } + +@Preview +@Composable +internal fun TimelineItemEventRowTimestampLightPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = + ElementPreviewLight { ContentTimestampToPreview(event) } + +@Preview +@Composable +internal fun TimelineItemEventRowTimestampDarkPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = + ElementPreviewDark { ContentTimestampToPreview(event) } + +@Composable +private fun ContentTimestampToPreview(event: TimelineItem.Event) { + Column { + val oldContent = event.content as TimelineItemTextContent + listOf( + "Text", + "Text longer but displayed on 1 line", + "Text which should be rendered on several lines", + ).forEach { str -> + listOf(false, true).forEach { useDocument -> + TimelineItemEventRow( + event = event.copy( + content = oldContent.copy( + body = str, + htmlDocument = if (useDocument) Jsoup.parse(str) else null, + ), + reactionsState = aTimelineItemReactions(count = 0), + senderDisplayName = if (useDocument) "Document case" else "Text case", + ), + isHighlighted = false, + onClick = {}, + onLongClick = {}, + onUserDataClick = {}, + inReplyToClick = {}, + onTimestampClicked = {}, + ) + } + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index 136568bf2e..cdf3d9112c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView +import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent @@ -66,6 +67,7 @@ fun TimelineItemStateEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, + extraPadding = noExtraPadding, modifier = Modifier.defaultTimelineContentPadding() ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index 735c4e8106..ec20b561bd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -17,10 +17,8 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -35,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt fun TimelineItemEventContentView( content: TimelineItemEventContent, interactionSource: MutableInteractionSource, + extraPadding: ExtraPadding, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier @@ -42,14 +41,17 @@ fun TimelineItemEventContentView( when (content) { is TimelineItemEncryptedContent -> TimelineItemEncryptedView( content = content, + extraPadding = extraPadding, modifier = modifier ) is TimelineItemRedactedContent -> TimelineItemRedactedView( content = content, + extraPadding = extraPadding, modifier = modifier ) is TimelineItemTextBasedContent -> TimelineItemTextView( content = content, + extraPadding = extraPadding, interactionSource = interactionSource, modifier = modifier, onTextClicked = onClick, @@ -57,6 +59,7 @@ fun TimelineItemEventContentView( ) is TimelineItemUnknownContent -> TimelineItemUnknownView( content = content, + extraPadding = extraPadding, modifier = modifier ) is TimelineItemImageContent -> TimelineItemImageView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt index 83015ecae4..9755377b39 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt @@ -31,12 +31,14 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineItemEncryptedView( content: TimelineItemEncryptedContent, + extraPadding: ExtraPadding, modifier: Modifier = Modifier ) { TimelineItemInformativeView( text = stringResource(id = CommonStrings.common_decryption_error), iconDescription = stringResource(id = CommonStrings.dialog_title_warning), icon = Icons.Default.Warning, + extraPadding = extraPadding, modifier = modifier ) } @@ -56,6 +58,7 @@ private fun ContentToPreview() { TimelineItemEncryptedView( content = TimelineItemEncryptedContent( data = UnableToDecryptContent.Data.Unknown - ) + ), + extraPadding = noExtraPadding ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt index 3b02e6804c..c2c63fb6c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt @@ -41,6 +41,7 @@ fun TimelineItemInformativeView( text: String, iconDescription: String, icon: ImageVector, + extraPadding: ExtraPadding, modifier: Modifier = Modifier ) { Row( @@ -58,7 +59,7 @@ fun TimelineItemInformativeView( fontStyle = FontStyle.Italic, color = MaterialTheme.colorScheme.secondary, fontSize = 14.sp, - text = text + extraPaddingTrick + text = text + extraPadding.str ) } } @@ -76,6 +77,7 @@ private fun ContentToPreview() { TimelineItemInformativeView( text = "Info", iconDescription = "", - icon = Icons.Default.Delete + icon = Icons.Default.Delete, + extraPadding = noExtraPadding, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt index 447c904dea..44917c7d5b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt @@ -30,12 +30,14 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineItemRedactedView( content: TimelineItemRedactedContent, + extraPadding: ExtraPadding, modifier: Modifier = Modifier ) { TimelineItemInformativeView( - text = stringResource(id = CommonStrings.common_message_removed) + extraPaddingTrick, + text = stringResource(id = CommonStrings.common_message_removed), iconDescription = stringResource(id = CommonStrings.common_message_removed), icon = Icons.Default.Delete, + extraPadding = extraPadding, modifier = modifier ) } @@ -52,5 +54,8 @@ internal fun TimelineItemRedactedViewDarkPreview() = @Composable private fun ContentToPreview() { - TimelineItemRedactedView(TimelineItemRedactedContent) + TimelineItemRedactedView( + TimelineItemRedactedContent, + extraPadding = noExtraPadding + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index 90efbd6cd9..f0083c96c8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -22,6 +22,9 @@ import android.text.util.Linkify.PHONE_NUMBERS import android.text.util.Linkify.WEB_URLS import androidx.compose.foundation.interaction.MutableInteractionSource 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.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -29,6 +32,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import androidx.core.text.util.LinkifyCompat import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -43,12 +47,25 @@ import io.element.android.libraries.designsystem.text.toAnnotatedString fun TimelineItemTextView( content: TimelineItemTextBasedContent, interactionSource: MutableInteractionSource, + extraPadding: ExtraPadding, modifier: Modifier = Modifier, onTextClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {}, ) { val htmlDocument = content.htmlDocument if (htmlDocument != null) { + // For now we ignore the extra padding for html content, so add some spacing + // below the content (as previous behavior) + Column(modifier = modifier) { + HtmlDocument( + document = htmlDocument, + modifier = Modifier, + onTextClicked = onTextClicked, + onTextLongClicked = onTextLongClicked, + interactionSource = interactionSource + ) + Spacer(Modifier.height(16.dp)) + } HtmlDocument( document = htmlDocument, modifier = modifier, @@ -61,7 +78,7 @@ fun TimelineItemTextView( val linkStyle = SpanStyle( color = LinkColor, ) - val styledText = remember(content.body) { content.body.linkify(linkStyle) + extraPaddingTrick.toAnnotatedString() } + val styledText = remember(content.body) { content.body.linkify(linkStyle) + extraPadding.str.toAnnotatedString() } ClickableLinkText( text = styledText, linkAnnotationTag = "URL", @@ -110,6 +127,10 @@ internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextB @Composable fun ContentToPreview(content: TimelineItemTextBasedContent) { - TimelineItemTextView(content, MutableInteractionSource()) + TimelineItemTextView( + content = content, + interactionSource = MutableInteractionSource(), + extraPadding = ExtraPadding("xxxxxxx"), + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt index beea9fb073..852428cb92 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt @@ -30,12 +30,14 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineItemUnknownView( content: TimelineItemUnknownContent, + extraPadding: ExtraPadding, modifier: Modifier = Modifier ) { TimelineItemInformativeView( - text = stringResource(id = CommonStrings.common_unsupported_event) + extraPaddingTrick, + text = stringResource(id = CommonStrings.common_unsupported_event), iconDescription = stringResource(id = CommonStrings.dialog_title_warning), icon = Icons.Default.Info, + extraPadding = extraPadding, modifier = modifier ) } @@ -52,5 +54,8 @@ internal fun TimelineItemUnknownViewDarkPreview() = @Composable private fun ContentToPreview() { - TimelineItemUnknownView(TimelineItemUnknownContent) + TimelineItemUnknownView( + content = TimelineItemUnknownContent, + extraPadding = noExtraPadding + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/Util.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/Util.kt index c261bbe0bf..4ef7687e2a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/Util.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/Util.kt @@ -16,6 +16,38 @@ package io.element.android.features.messages.impl.timeline.components.event +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState +import io.element.android.libraries.ui.strings.R + // Allow to not overlap the timestamp with the text, in the message bubble. // Compute the size of the worst case. -val extraPaddingTrick: String = " ".repeat(" (edited) 88:88 X ".length) +data class ExtraPadding(val str: String) + +val noExtraPadding = ExtraPadding("") + +/** + * See [io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView] for the related View. + * And https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1819%253A99506 for the design. + */ +@Composable +fun TimelineItem.Event.toExtraPadding(): ExtraPadding { + val formattedTime = sentTime + val hasMessageSendingFailed = sendState is EventSendState.SendingFailed + val isMessageEdited = (content as? TimelineItemTextBasedContent)?.isEdited.orFalse() + + var strLen = 2 + if (isMessageEdited) { + strLen += stringResource(id = R.string.common_edited_suffix).length + 2 + } + strLen += formattedTime.length + if (hasMessageSendingFailed) { + strLen += 3 + } + // A space and a few unbreakable spaces + return ExtraPadding(" " + "\u00A0".repeat(strLen)) +}