Timeline: start handling multiple contents
This commit is contained in:
parent
dc39d061b8
commit
6e20c4455f
16 changed files with 240 additions and 27 deletions
|
|
@ -4,6 +4,7 @@ import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
||||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||||
|
import io.element.android.x.features.messages.model.content.*
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.media.MediaResolver
|
import io.element.android.x.matrix.media.MediaResolver
|
||||||
import io.element.android.x.matrix.room.MatrixRoom
|
import io.element.android.x.matrix.room.MatrixRoom
|
||||||
|
|
@ -61,15 +62,30 @@ class MessageTimelineItemStateMapper(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MatrixTimelineItem.Event.computeContent(): String? {
|
private fun MatrixTimelineItem.Event.computeContent(): MessagesTimelineItemContent {
|
||||||
val messageType =
|
val content = event.content()
|
||||||
event.content().asMessage()?.msgtype()
|
content.asUnableToDecrypt()?.let { encryptedMessage ->
|
||||||
return when (messageType) {
|
return MessagesTimelineItemEncryptedContent(encryptedMessage)
|
||||||
is MessageType.Emote -> messageType.content.body
|
}
|
||||||
is MessageType.Image -> messageType.content.body
|
if (content.isRedactedMessage()) {
|
||||||
is MessageType.Notice -> messageType.content.body
|
return MessagesTimelineItemRedactedContent
|
||||||
is MessageType.Text -> messageType.content.body
|
}
|
||||||
null -> null
|
val contentAsMessage = content.asMessage()
|
||||||
|
return when (val messageType = contentAsMessage?.msgtype()) {
|
||||||
|
is MessageType.Emote -> MessagesTimelineItemEmoteContent(
|
||||||
|
body = messageType.content.body,
|
||||||
|
formattedBody = messageType.content.formatted
|
||||||
|
)
|
||||||
|
is MessageType.Image -> MessagesTimelineItemUnknownContent
|
||||||
|
is MessageType.Notice -> MessagesTimelineItemNoticeContent(
|
||||||
|
body = messageType.content.body,
|
||||||
|
formattedBody = messageType.content.formatted
|
||||||
|
)
|
||||||
|
is MessageType.Text -> MessagesTimelineItemTextContent(
|
||||||
|
body = messageType.content.body,
|
||||||
|
formattedBody = messageType.content.formatted
|
||||||
|
)
|
||||||
|
null -> MessagesTimelineItemUnknownContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,17 @@ import com.airbnb.mvrx.compose.mavericksViewModel
|
||||||
import io.element.android.x.core.data.LogCompositions
|
import io.element.android.x.core.data.LogCompositions
|
||||||
import io.element.android.x.core.data.StableCharSequence
|
import io.element.android.x.core.data.StableCharSequence
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.x.features.messages.components.MessagesTimelineItemEncryptedView
|
||||||
|
import io.element.android.x.features.messages.components.MessagesTimelineItemRedactedView
|
||||||
|
import io.element.android.x.features.messages.components.MessagesTimelineItemTextView
|
||||||
|
import io.element.android.x.features.messages.components.MessagesTimelineItemUnknownView
|
||||||
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
|
||||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
||||||
import io.element.android.x.features.messages.model.MessagesViewState
|
import io.element.android.x.features.messages.model.MessagesViewState
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
|
||||||
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
|
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
|
||||||
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
|
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
|
||||||
import io.element.android.x.textcomposer.TextComposer
|
import io.element.android.x.textcomposer.TextComposer
|
||||||
|
|
@ -231,7 +239,32 @@ fun MessageEventRow(
|
||||||
Modifier.zIndex(1f)
|
Modifier.zIndex(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MessageEventBubble(messageEvent, Modifier.zIndex(-1f))
|
MessageEventBubble(
|
||||||
|
groupPosition = messageEvent.groupPosition,
|
||||||
|
isMine = messageEvent.isMine,
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(-1f)
|
||||||
|
) {
|
||||||
|
val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
|
when (messageEvent.content) {
|
||||||
|
is MessagesTimelineItemEncryptedContent -> MessagesTimelineItemEncryptedView(
|
||||||
|
content = messageEvent.content,
|
||||||
|
modifier = contentModifier
|
||||||
|
)
|
||||||
|
is MessagesTimelineItemRedactedContent -> MessagesTimelineItemRedactedView(
|
||||||
|
content = messageEvent.content,
|
||||||
|
modifier = contentModifier
|
||||||
|
)
|
||||||
|
is MessagesTimelineItemTextBasedContent -> MessagesTimelineItemTextView(
|
||||||
|
content = messageEvent.content,
|
||||||
|
modifier = contentModifier
|
||||||
|
)
|
||||||
|
is MessagesTimelineItemUnknownContent -> MessagesTimelineItemUnknownView(
|
||||||
|
content = messageEvent.content,
|
||||||
|
modifier = contentModifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (messageEvent.isMine) {
|
if (messageEvent.isMine) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
@ -267,10 +300,12 @@ private fun MessageSenderInformation(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageEventBubble(
|
fun MessageEventBubble(
|
||||||
messageEvent: MessagesTimelineItemState.MessageEvent,
|
groupPosition: MessagesItemGroupPosition,
|
||||||
|
isMine: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
fun MessagesTimelineItemState.MessageEvent.bubbleShape(): Shape {
|
fun bubbleShape(): Shape {
|
||||||
return when (groupPosition) {
|
return when (groupPosition) {
|
||||||
MessagesItemGroupPosition.First -> if (isMine) {
|
MessagesItemGroupPosition.First -> if (isMine) {
|
||||||
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
||||||
|
|
@ -297,15 +332,15 @@ fun MessageEventBubble(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Modifier.offsetForItem(messageEvent: MessagesTimelineItemState.MessageEvent): Modifier {
|
fun Modifier.offsetForItem(): Modifier {
|
||||||
return if (messageEvent.isMine) {
|
return if (isMine) {
|
||||||
offset(y = -(12.dp))
|
offset(y = -(12.dp))
|
||||||
} else {
|
} else {
|
||||||
offset(x = 20.dp, y = -(12.dp))
|
offset(x = 20.dp, y = -(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val (backgroundBubbleColor, border) = if (messageEvent.isMine) {
|
val (backgroundBubbleColor, border) = if (isMine) {
|
||||||
Pair(MaterialTheme.colorScheme.surfaceVariant, null)
|
Pair(MaterialTheme.colorScheme.surfaceVariant, null)
|
||||||
} else {
|
} else {
|
||||||
Pair(
|
Pair(
|
||||||
|
|
@ -313,12 +348,11 @@ fun MessageEventBubble(
|
||||||
BorderStroke(1.dp, MaterialTheme.colorScheme.surfaceVariant)
|
BorderStroke(1.dp, MaterialTheme.colorScheme.surfaceVariant)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val bubbleShape = bubbleShape()
|
||||||
val bubbleShape = messageEvent.bubbleShape()
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.widthIn(min = 80.dp)
|
.widthIn(min = 80.dp)
|
||||||
.offsetForItem(messageEvent)
|
.offsetForItem()
|
||||||
.clip(bubbleShape)
|
.clip(bubbleShape)
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = { },
|
onClick = { },
|
||||||
|
|
@ -327,13 +361,9 @@ fun MessageEventBubble(
|
||||||
),
|
),
|
||||||
color = backgroundBubbleColor,
|
color = backgroundBubbleColor,
|
||||||
shape = bubbleShape,
|
shape = bubbleShape,
|
||||||
border = border
|
border = border,
|
||||||
) {
|
content = content
|
||||||
Text(
|
)
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
|
||||||
text = messageEvent.content ?: "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package io.element.android.x.features.messages.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Warning
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesTimelineItemEncryptedView(
|
||||||
|
content: MessagesTimelineItemEncryptedContent,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
MessagesTimelineItemInformativeView(
|
||||||
|
text = "Decryption error",
|
||||||
|
iconDescription = "Warning",
|
||||||
|
icon = Icons.Default.Warning,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package io.element.android.x.features.messages.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesTimelineItemInformativeView(
|
||||||
|
text: String,
|
||||||
|
iconDescription: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
contentDescription = iconDescription
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
fontStyle = FontStyle.Italic,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
text = text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package io.element.android.x.features.messages.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesTimelineItemRedactedView(
|
||||||
|
content: MessagesTimelineItemRedactedContent,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
MessagesTimelineItemInformativeView(
|
||||||
|
text = "This message has been deleted",
|
||||||
|
iconDescription = "Delete",
|
||||||
|
icon = Icons.Default.Delete,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.element.android.x.features.messages.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesTimelineItemTextView(
|
||||||
|
content: MessagesTimelineItemTextBasedContent,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(modifier) {
|
||||||
|
Text(text = content.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package io.element.android.x.features.messages.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesTimelineItemUnknownView(
|
||||||
|
content: MessagesTimelineItemUnknownContent,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
MessagesTimelineItemInformativeView(
|
||||||
|
text = "Event not handled by EAX",
|
||||||
|
iconDescription = "Info",
|
||||||
|
icon = Icons.Default.Info,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package io.element.android.x.features.messages.model
|
package io.element.android.x.features.messages.model
|
||||||
|
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent
|
||||||
|
|
||||||
sealed interface MessagesTimelineItemState {
|
sealed interface MessagesTimelineItemState {
|
||||||
data class Virtual(
|
data class Virtual(
|
||||||
|
|
@ -12,10 +13,10 @@ sealed interface MessagesTimelineItemState {
|
||||||
val senderId: String,
|
val senderId: String,
|
||||||
val senderDisplayName: String?,
|
val senderDisplayName: String?,
|
||||||
val senderAvatar: AvatarData,
|
val senderAvatar: AvatarData,
|
||||||
val content: String? = null,
|
val content: MessagesTimelineItemContent,
|
||||||
val sentTime: String = "",
|
val sentTime: String = "",
|
||||||
val isMine: Boolean = false,
|
val isMine: Boolean = false,
|
||||||
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None
|
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
|
||||||
) : MessagesTimelineItemState {
|
) : MessagesTimelineItemState {
|
||||||
|
|
||||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
sealed interface MessagesTimelineItemContent
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||||
|
|
||||||
|
data class MessagesTimelineItemEmoteContent(
|
||||||
|
override val body: String,
|
||||||
|
override val formattedBody: FormattedBody?,
|
||||||
|
) : MessagesTimelineItemTextBasedContent
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.EncryptedMessage
|
||||||
|
|
||||||
|
data class MessagesTimelineItemEncryptedContent(
|
||||||
|
val encryptedMessage: EncryptedMessage
|
||||||
|
) : MessagesTimelineItemContent
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||||
|
|
||||||
|
data class MessagesTimelineItemNoticeContent(
|
||||||
|
override val body: String,
|
||||||
|
override val formattedBody: FormattedBody?,
|
||||||
|
) : MessagesTimelineItemTextBasedContent
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
object MessagesTimelineItemRedactedContent : MessagesTimelineItemContent
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||||
|
|
||||||
|
sealed interface MessagesTimelineItemTextBasedContent : MessagesTimelineItemContent {
|
||||||
|
val body: String
|
||||||
|
val formattedBody: FormattedBody?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||||
|
|
||||||
|
data class MessagesTimelineItemTextContent(
|
||||||
|
override val body: String,
|
||||||
|
override val formattedBody: FormattedBody?,
|
||||||
|
) : MessagesTimelineItemTextBasedContent
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package io.element.android.x.features.messages.model.content
|
||||||
|
|
||||||
|
object MessagesTimelineItemUnknownContent : MessagesTimelineItemContent
|
||||||
Loading…
Add table
Add a link
Reference in a new issue