Timeline: add avatar and displayname (not dynamic...)

This commit is contained in:
ganfra 2022-11-08 16:09:24 +01:00
parent 560d2a3291
commit c8b8c08c99
14 changed files with 206 additions and 93 deletions

View file

@ -1,8 +1,11 @@
package io.element.android.x.features.messages
import io.element.android.x.designsystem.components.avatar.AvatarData
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.MessagesTimelineItemState
import io.element.android.x.matrix.core.UserId
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import kotlinx.coroutines.CoroutineDispatcher
@ -10,7 +13,7 @@ import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.MessageType
class MessageTimelineItemStateMapper(
private val myUserId: UserId,
private val client: MatrixClient,
private val room: MatrixRoom,
private val dispatcher: CoroutineDispatcher,
) {
@ -22,36 +25,7 @@ class MessageTimelineItemStateMapper(
val currentTimelineItem = timelineItems[index]
val timelineItemState = when (currentTimelineItem) {
is MatrixTimelineItem.Event -> {
val prevTimelineItem =
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
val nextTimelineItem =
timelineItems.getOrNull(index + 1) as? MatrixTimelineItem.Event
val currentSender = currentTimelineItem.event.sender()
val previousSender = prevTimelineItem?.event?.sender()
val nextSender = nextTimelineItem?.event?.sender()
val groupPosition = when {
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last
else -> MessagesItemGroupPosition.None
}
val messageType =
currentTimelineItem.event.content().asMessage()?.msgtype()
val contentStr = when (messageType) {
is MessageType.Emote -> messageType.content.body
is MessageType.Image -> messageType.content.body
is MessageType.Notice -> messageType.content.body
is MessageType.Text -> messageType.content.body
null -> null
}
MessagesTimelineItemState.MessageEvent(
id = currentTimelineItem.event.eventId() ?: "",
sender = currentTimelineItem.event.sender(),
content = contentStr,
isMine = currentTimelineItem.event.isOwn(),
groupPosition = groupPosition
)
buildMessageEvent(currentTimelineItem, index, timelineItems)
}
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
"virtual_item_$index"
@ -63,5 +37,72 @@ class MessageTimelineItemStateMapper(
messagesTimelineItemState
}
private suspend fun buildMessageEvent(
currentTimelineItem: MatrixTimelineItem.Event,
index: Int,
timelineItems: List<MatrixTimelineItem>
): MessagesTimelineItemState.MessageEvent {
val currentSender = currentTimelineItem.event.sender()
val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData =
loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl)
return MessagesTimelineItemState.MessageEvent(
id = currentTimelineItem.event.eventId() ?: "",
senderId = currentSender,
senderDisplayName = senderDisplayName,
senderAvatar = senderAvatarData,
content = currentTimelineItem.computeContent(),
isMine = currentTimelineItem.event.isOwn(),
groupPosition = groupPosition
)
}
private fun MatrixTimelineItem.Event.computeContent(): String? {
val messageType =
event.content().asMessage()?.msgtype()
return when (messageType) {
is MessageType.Emote -> messageType.content.body
is MessageType.Image -> messageType.content.body
is MessageType.Notice -> messageType.content.body
is MessageType.Text -> messageType.content.body
null -> null
}
}
private fun computeGroupPosition(
currentTimelineItem: MatrixTimelineItem.Event,
timelineItems: List<MatrixTimelineItem>,
index: Int
): MessagesItemGroupPosition {
val prevTimelineItem =
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
val nextTimelineItem =
timelineItems.getOrNull(index + 1) as? MatrixTimelineItem.Event
val currentSender = currentTimelineItem.event.sender()
val previousSender = prevTimelineItem?.event?.sender()
val nextSender = nextTimelineItem?.event?.sender()
return when {
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last
else -> MessagesItemGroupPosition.None
}
}
private suspend fun loadAvatarData(
name: String,
url: String?,
size: AvatarSize = AvatarSize.SMALL
): AvatarData {
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
}

View file

@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.Shape
import androidx.compose.ui.layout.LastBaseline
@ -29,6 +30,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.data.LogCompositions
@ -170,47 +172,51 @@ fun MessageEventRow(
.wrapContentHeight(),
contentAlignment = contentAlignment
) {
Row(modifier = modifier
.widthIn(max = 300.dp)
.clickable(
onClick = { },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
)) {
Row(
modifier = modifier
.widthIn(max = 300.dp)
) {
if (!messageEvent.isMine) {
Spacer(modifier = Modifier.width(16.dp))
}
Column {
if (messageEvent.showSenderInformation) {
MessageSenderInformation(messageEvent.sender, messageEvent.senderAvatar)
MessageSenderInformation(
messageEvent.safeSenderName,
messageEvent.senderAvatar,
Modifier.zIndex(1f)
)
}
MessageEventBubble(messageEvent)
MessageEventBubble(messageEvent, Modifier.zIndex(-1f))
}
if (messageEvent.isMine) {
Spacer(modifier = Modifier.width(16.dp))
}
}
}
if (messageEvent.groupPosition is MessagesItemGroupPosition.First) {
Spacer(modifier = Modifier.height(16.dp))
if (messageEvent.groupPosition.isNew()) {
Spacer(modifier = Modifier.height(8.dp))
} else {
Spacer(modifier = Modifier.height(2.dp))
}
}
@Composable
private fun MessageSenderInformation(sender: String, senderAvatar: AvatarData?) {
Row {
private fun MessageSenderInformation(
sender: String,
senderAvatar: AvatarData?,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
if (senderAvatar != null) {
Avatar(senderAvatar)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = sender,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.alignBy(LastBaseline)
.paddingFrom(LastBaseline, after = 8.dp)
)
}
}
@ -218,6 +224,7 @@ private fun MessageSenderInformation(sender: String, senderAvatar: AvatarData?)
@Composable
fun MessageEventBubble(
messageEvent: MessagesTimelineItemState.MessageEvent,
modifier: Modifier = Modifier,
) {
fun MessagesTimelineItemState.MessageEvent.bubbleShape(): Shape {
@ -247,10 +254,28 @@ fun MessageEventBubble(
} else {
Pair(Color.Transparent, BorderStroke(1.dp, MaterialTheme.colorScheme.surfaceVariant))
}
fun Modifier.offsetForItem(messageEvent: MessagesTimelineItemState.MessageEvent): Modifier {
return if (messageEvent.isMine) {
offset(y = -(12.dp))
} else {
offset(x = 20.dp, y = -(12.dp))
}
}
val bubbleShape = remember { messageEvent.bubbleShape() }
Surface(
modifier = Modifier.widthIn(min = 80.dp),
modifier = modifier
.widthIn(min = 80.dp)
.offsetForItem(messageEvent)
.clip(bubbleShape)
.clickable(
onClick = { },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
),
color = backgroundBubbleColor,
shape = messageEvent.bubbleShape(),
shape = bubbleShape,
border = border
) {
Text(

View file

@ -8,6 +8,7 @@ import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline
import kotlinx.coroutines.Dispatchers
@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
private const val PAGINATION_COUNT = 50
@ -38,7 +38,7 @@ class MessagesViewModel(
val client = matrix.activeClient()
val room = client.getRoom(state.roomId) ?: return null
val messageTimelineItemStateMapper =
MessageTimelineItemStateMapper(client.userId(), room, Dispatchers.Default)
MessageTimelineItemStateMapper(client, room, Dispatchers.Default)
return MessagesViewModel(
client,
room,
@ -84,20 +84,9 @@ class MessagesViewModel(
url: String?,
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val mediaContent = url?.let {
val mediaSource = mediaSourceFromUrl(it)
client.loadMediaThumbnailForSource(
mediaSource,
size.value.toLong(),
size.value.toLong()
)
}
return mediaContent?.fold(
{ it },
{ null }
).let { model ->
AvatarData(name.first().uppercase(), model, size)
}
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
override fun onCleared() {

View file

@ -6,4 +6,9 @@ sealed interface MessagesItemGroupPosition {
object Last : MessagesItemGroupPosition
object None : MessagesItemGroupPosition
fun isNew(): Boolean = when (this) {
First, None -> true
else -> false
}
}

View file

@ -9,18 +9,19 @@ sealed interface MessagesTimelineItemState {
data class MessageEvent(
val id: String = "",
val sender: String = "",
val senderAvatar: AvatarData? = null,
val senderId: String,
val senderDisplayName: String?,
val senderAvatar: AvatarData,
val content: String? = null,
val sentTime: String = "",
val isMine: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None
) : MessagesTimelineItemState {
val showSenderInformation: Boolean = when (groupPosition) {
MessagesItemGroupPosition.First, MessagesItemGroupPosition.None -> !isMine
else -> false
}
val showSenderInformation = groupPosition.isNew() && !isMine
val safeSenderName: String = senderDisplayName ?: senderId
}
}

View file

@ -9,6 +9,7 @@ import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.RoomSummary
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -103,20 +104,9 @@ class RoomListViewModel(initialState: RoomListViewState) :
url: String?,
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val mediaContent = url?.let {
val mediaSource = mediaSourceFromUrl(it)
client.loadMediaThumbnailForSource(
mediaSource,
size.value.toLong(),
size.value.toLong()
)
}
return mediaContent?.fold(
{ it },
{ null }
).let { model ->
AvatarData(name.first().uppercase(), model, size)
}
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
private fun handleLogout() {