Try to improve timestamp rendering for media
This commit is contained in:
parent
c062d125ea
commit
6c941e02e3
2 changed files with 101 additions and 59 deletions
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -33,11 +34,13 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowDownward
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
import androidx.compose.material.icons.filled.Error
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
|
@ -51,6 +54,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.LastBaseline
|
import androidx.compose.ui.layout.LastBaseline
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -70,8 +74,11 @@ 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.bubble.BubbleState
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
|
@ -234,6 +241,7 @@ fun TimelineItemEventRow(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
||||||
Pair(Alignment.CenterEnd, Alignment.End)
|
Pair(Alignment.CenterEnd, Alignment.End)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -271,25 +279,7 @@ fun TimelineItemEventRow(
|
||||||
.zIndex(-1f)
|
.zIndex(-1f)
|
||||||
.widthIn(max = 320.dp)
|
.widthIn(max = 320.dp)
|
||||||
) {
|
) {
|
||||||
Column {
|
MessageEventLayout(event = event, onMessageClick = onClick, onMessageLongClick = onLongClick)
|
||||||
TimelineItemEventContentView(
|
|
||||||
content = event.content,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
)
|
|
||||||
TimestampView(
|
|
||||||
formattedTime = event.sentTime,
|
|
||||||
hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed,
|
|
||||||
isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse(),
|
|
||||||
onClick = {
|
|
||||||
// TODO trigger either resending the message or opening the message edition history. This will be implemented later
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
.align(Alignment.End),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TimelineItemReactionsView(
|
TimelineItemReactionsView(
|
||||||
reactionsState = event.reactionsState,
|
reactionsState = event.reactionsState,
|
||||||
|
|
@ -310,6 +300,92 @@ fun TimelineItemEventRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageEventLayout(
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
onMessageClick: () -> Unit,
|
||||||
|
onMessageLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val isTextBasedMessage = event.content is TimelineItemTextBasedContent
|
||||||
|
val isMediaMessage = event.content is TimelineItemImageContent
|
||||||
|
|| event.content is TimelineItemVideoContent
|
||||||
|
|| event.content is TimelineItemFileContent
|
||||||
|
val isStateMessage = event.content is TimelineItemStateContent
|
||||||
|
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContentView(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
TimelineItemEventContentView(
|
||||||
|
content = event.content,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onMessageClick,
|
||||||
|
onLongClick = onMessageLongClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimestampView(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val formattedTime = event.sentTime
|
||||||
|
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
|
||||||
|
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
||||||
|
val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null
|
||||||
|
Row(modifier = modifier.clickable(onClick = onMessageClick)) {
|
||||||
|
if (isMessageEdited) {
|
||||||
|
Text(
|
||||||
|
stringResource(StringR.string.common_edited_suffix),
|
||||||
|
style = ElementTextStyles.Regular.caption2,
|
||||||
|
color = tint ?: MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
formattedTime,
|
||||||
|
style = ElementTextStyles.Regular.caption2,
|
||||||
|
color = tint ?: MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
if (hasMessageSendingFailed && tint != null) {
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Icon(imageVector = Icons.Default.Error, contentDescription = "Error sending message", tint = tint, modifier = Modifier.size(15.dp, 18.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
isMediaMessage -> {
|
||||||
|
Box(modifier.wrapContentSize()) {
|
||||||
|
ContentView()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
.background(Color(0xFFF0F2F5), RoundedCornerShape(10.0.dp)) // TODO: add the color with its dark variant
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
) {
|
||||||
|
TimestampView(Modifier.padding(horizontal = 8.dp, vertical = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isTextBasedMessage -> {
|
||||||
|
Column {
|
||||||
|
ContentView(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 6.dp))
|
||||||
|
TimestampView(modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isStateMessage -> {
|
||||||
|
ContentView(modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemStateEventRow(
|
fun TimelineItemStateEventRow(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
|
@ -344,36 +420,6 @@ fun TimelineItemStateEventRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TimestampView(
|
|
||||||
formattedTime: String,
|
|
||||||
isMessageEdited: Boolean,
|
|
||||||
hasMessageSendingFailed: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null
|
|
||||||
Row(modifier = modifier.clickable(onClick = onClick)) {
|
|
||||||
if (isMessageEdited) {
|
|
||||||
Text(
|
|
||||||
stringResource(StringR.string.common_edited_suffix),
|
|
||||||
style = ElementTextStyles.Regular.caption2,
|
|
||||||
color = tint ?: MaterialTheme.colorScheme.secondary,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
formattedTime,
|
|
||||||
style = ElementTextStyles.Regular.caption1,
|
|
||||||
color = tint ?: MaterialTheme.colorScheme.secondary,
|
|
||||||
)
|
|
||||||
if (hasMessageSendingFailed && tint != null) {
|
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
|
||||||
Icon(imageVector = Icons.Default.Error, contentDescription = "Error sending message", tint = tint, modifier = Modifier.size(15.dp, 18.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSenderInformation(
|
private fun MessageSenderInformation(
|
||||||
sender: String,
|
sender: String,
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
|
||||||
private fun Modifier.defaultContentPadding(): Modifier = padding(
|
|
||||||
horizontal = 12.dp, vertical = 6.dp
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventContentView(
|
fun TimelineItemEventContentView(
|
||||||
content: TimelineItemEventContent,
|
content: TimelineItemEventContent,
|
||||||
|
|
@ -46,22 +42,22 @@ fun TimelineItemEventContentView(
|
||||||
when (content) {
|
when (content) {
|
||||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier.defaultContentPadding()
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier.defaultContentPadding()
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||||
content = content,
|
content = content,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier = modifier.defaultContentPadding(),
|
modifier = modifier,
|
||||||
onTextClicked = onClick,
|
onTextClicked = onClick,
|
||||||
onTextLongClicked = onLongClick
|
onTextLongClicked = onLongClick
|
||||||
)
|
)
|
||||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier.defaultContentPadding()
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemImageContent -> TimelineItemImageView(
|
is TimelineItemImageContent -> TimelineItemImageView(
|
||||||
content = content,
|
content = content,
|
||||||
|
|
@ -73,11 +69,11 @@ fun TimelineItemEventContentView(
|
||||||
)
|
)
|
||||||
is TimelineItemFileContent -> TimelineItemFileView(
|
is TimelineItemFileContent -> TimelineItemFileView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier.defaultContentPadding()
|
modifier = modifier
|
||||||
)
|
)
|
||||||
is TimelineItemStateContent -> TimelineItemStateView(
|
is TimelineItemStateContent -> TimelineItemStateView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier.defaultContentPadding()
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue