Timeline: add avatar and displayname (not dynamic...)
This commit is contained in:
parent
560d2a3291
commit
c8b8c08c99
14 changed files with 206 additions and 93 deletions
|
|
@ -1,8 +1,11 @@
|
||||||
package io.element.android.x.features.messages
|
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.MessagesItemGroupPosition
|
||||||
import io.element.android.x.features.messages.model.MessagesTimelineItemState
|
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.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
import io.element.android.x.matrix.timeline.MatrixTimelineItem
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
|
@ -10,7 +13,7 @@ import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.MessageType
|
import org.matrix.rustcomponents.sdk.MessageType
|
||||||
|
|
||||||
class MessageTimelineItemStateMapper(
|
class MessageTimelineItemStateMapper(
|
||||||
private val myUserId: UserId,
|
private val client: MatrixClient,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val dispatcher: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
|
|
@ -22,36 +25,7 @@ class MessageTimelineItemStateMapper(
|
||||||
val currentTimelineItem = timelineItems[index]
|
val currentTimelineItem = timelineItems[index]
|
||||||
val timelineItemState = when (currentTimelineItem) {
|
val timelineItemState = when (currentTimelineItem) {
|
||||||
is MatrixTimelineItem.Event -> {
|
is MatrixTimelineItem.Event -> {
|
||||||
val prevTimelineItem =
|
buildMessageEvent(currentTimelineItem, index, timelineItems)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
is MatrixTimelineItem.Virtual -> MessagesTimelineItemState.Virtual(
|
||||||
"virtual_item_$index"
|
"virtual_item_$index"
|
||||||
|
|
@ -63,5 +37,72 @@ class MessageTimelineItemStateMapper(
|
||||||
messagesTimelineItemState
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.layout.LastBaseline
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import com.airbnb.mvrx.compose.collectAsState
|
import com.airbnb.mvrx.compose.collectAsState
|
||||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||||
import io.element.android.x.core.data.LogCompositions
|
import io.element.android.x.core.data.LogCompositions
|
||||||
|
|
@ -170,47 +172,51 @@ fun MessageEventRow(
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
contentAlignment = contentAlignment
|
contentAlignment = contentAlignment
|
||||||
) {
|
) {
|
||||||
Row(modifier = modifier
|
Row(
|
||||||
.widthIn(max = 300.dp)
|
modifier = modifier
|
||||||
.clickable(
|
.widthIn(max = 300.dp)
|
||||||
onClick = { },
|
) {
|
||||||
indication = rememberRipple(),
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
)) {
|
|
||||||
if (!messageEvent.isMine) {
|
if (!messageEvent.isMine) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
if (messageEvent.showSenderInformation) {
|
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) {
|
if (messageEvent.isMine) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (messageEvent.groupPosition is MessagesItemGroupPosition.First) {
|
if (messageEvent.groupPosition.isNew()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSenderInformation(sender: String, senderAvatar: AvatarData?) {
|
private fun MessageSenderInformation(
|
||||||
Row {
|
sender: String,
|
||||||
|
senderAvatar: AvatarData?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier) {
|
||||||
if (senderAvatar != null) {
|
if (senderAvatar != null) {
|
||||||
Avatar(senderAvatar)
|
Avatar(senderAvatar)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = sender,
|
text = sender,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.alignBy(LastBaseline)
|
.alignBy(LastBaseline)
|
||||||
.paddingFrom(LastBaseline, after = 8.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +224,7 @@ private fun MessageSenderInformation(sender: String, senderAvatar: AvatarData?)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageEventBubble(
|
fun MessageEventBubble(
|
||||||
messageEvent: MessagesTimelineItemState.MessageEvent,
|
messageEvent: MessagesTimelineItemState.MessageEvent,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun MessagesTimelineItemState.MessageEvent.bubbleShape(): Shape {
|
fun MessagesTimelineItemState.MessageEvent.bubbleShape(): Shape {
|
||||||
|
|
@ -247,10 +254,28 @@ fun MessageEventBubble(
|
||||||
} else {
|
} else {
|
||||||
Pair(Color.Transparent, BorderStroke(1.dp, MaterialTheme.colorScheme.surfaceVariant))
|
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(
|
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,
|
color = backgroundBubbleColor,
|
||||||
shape = messageEvent.bubbleShape(),
|
shape = bubbleShape,
|
||||||
border = border
|
border = border
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -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.features.messages.model.MessagesViewState
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
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.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.timeline.MatrixTimeline
|
import io.element.android.x.matrix.timeline.MatrixTimeline
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
|
||||||
|
|
||||||
private const val PAGINATION_COUNT = 50
|
private const val PAGINATION_COUNT = 50
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ class MessagesViewModel(
|
||||||
val client = matrix.activeClient()
|
val client = matrix.activeClient()
|
||||||
val room = client.getRoom(state.roomId) ?: return null
|
val room = client.getRoom(state.roomId) ?: return null
|
||||||
val messageTimelineItemStateMapper =
|
val messageTimelineItemStateMapper =
|
||||||
MessageTimelineItemStateMapper(client.userId(), room, Dispatchers.Default)
|
MessageTimelineItemStateMapper(client, room, Dispatchers.Default)
|
||||||
return MessagesViewModel(
|
return MessagesViewModel(
|
||||||
client,
|
client,
|
||||||
room,
|
room,
|
||||||
|
|
@ -84,20 +84,9 @@ class MessagesViewModel(
|
||||||
url: String?,
|
url: String?,
|
||||||
size: AvatarSize = AvatarSize.MEDIUM
|
size: AvatarSize = AvatarSize.MEDIUM
|
||||||
): AvatarData {
|
): AvatarData {
|
||||||
val mediaContent = url?.let {
|
val model = client.mediaResolver()
|
||||||
val mediaSource = mediaSourceFromUrl(it)
|
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
|
||||||
client.loadMediaThumbnailForSource(
|
return AvatarData(name, model, size)
|
||||||
mediaSource,
|
|
||||||
size.value.toLong(),
|
|
||||||
size.value.toLong()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return mediaContent?.fold(
|
|
||||||
{ it },
|
|
||||||
{ null }
|
|
||||||
).let { model ->
|
|
||||||
AvatarData(name.first().uppercase(), model, size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,9 @@ sealed interface MessagesItemGroupPosition {
|
||||||
object Last : MessagesItemGroupPosition
|
object Last : MessagesItemGroupPosition
|
||||||
object None : MessagesItemGroupPosition
|
object None : MessagesItemGroupPosition
|
||||||
|
|
||||||
|
fun isNew(): Boolean = when (this) {
|
||||||
|
First, None -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,19 @@ sealed interface MessagesTimelineItemState {
|
||||||
|
|
||||||
data class MessageEvent(
|
data class MessageEvent(
|
||||||
val id: String = "",
|
val id: String = "",
|
||||||
val sender: String = "",
|
val senderId: String,
|
||||||
val senderAvatar: AvatarData? = null,
|
val senderDisplayName: String?,
|
||||||
|
val senderAvatar: AvatarData,
|
||||||
val content: String? = null,
|
val content: String? = null,
|
||||||
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: Boolean = when (groupPosition) {
|
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||||
MessagesItemGroupPosition.First, MessagesItemGroupPosition.None -> !isMine
|
|
||||||
else -> false
|
val safeSenderName: String = senderDisplayName ?: senderId
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.features.roomlist.model.RoomListViewState
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
|
import io.element.android.x.matrix.media.MediaResolver
|
||||||
import io.element.android.x.matrix.room.RoomSummary
|
import io.element.android.x.matrix.room.RoomSummary
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -103,20 +104,9 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
url: String?,
|
url: String?,
|
||||||
size: AvatarSize = AvatarSize.MEDIUM
|
size: AvatarSize = AvatarSize.MEDIUM
|
||||||
): AvatarData {
|
): AvatarData {
|
||||||
val mediaContent = url?.let {
|
val model = client.mediaResolver()
|
||||||
val mediaSource = mediaSourceFromUrl(it)
|
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
|
||||||
client.loadMediaThumbnailForSource(
|
return AvatarData(name, model, size)
|
||||||
mediaSource,
|
|
||||||
size.value.toLong(),
|
|
||||||
size.value.toLong()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return mediaContent?.fold(
|
|
||||||
{ it },
|
|
||||||
{ null }
|
|
||||||
).let { model ->
|
|
||||||
AvatarData(name.first().uppercase(), model, size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLogout() {
|
private fun handleLogout() {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import io.element.android.x.designsystem.AvatarGradientEnd
|
import io.element.android.x.designsystem.AvatarGradientEnd
|
||||||
|
|
@ -27,7 +26,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
||||||
if (avatarData.model == null) {
|
if (avatarData.model == null) {
|
||||||
InitialsAvatar(
|
InitialsAvatar(
|
||||||
modifier = commonModifier,
|
modifier = commonModifier,
|
||||||
initials = avatarData.initials
|
initials = avatarData.name.first().uppercase()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ImageAvatar(
|
ImageAvatar(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class AvatarData(
|
data class AvatarData(
|
||||||
val initials: String = "",
|
val name: String = "",
|
||||||
val model: ByteArray? = null,
|
val model: ByteArray? = null,
|
||||||
val size: AvatarSize = AvatarSize.MEDIUM
|
val size: AvatarSize = AvatarSize.MEDIUM
|
||||||
) {
|
) {
|
||||||
|
|
@ -14,7 +14,7 @@ data class AvatarData(
|
||||||
|
|
||||||
other as AvatarData
|
other as AvatarData
|
||||||
|
|
||||||
if (initials != other.initials) return false
|
if (name != other.name) return false
|
||||||
if (model != null) {
|
if (model != null) {
|
||||||
if (other.model == null) return false
|
if (other.model == null) return false
|
||||||
if (!model.contentEquals(other.model)) return false
|
if (!model.contentEquals(other.model)) return false
|
||||||
|
|
@ -25,7 +25,7 @@ data class AvatarData(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = initials.hashCode()
|
var result = name.hashCode()
|
||||||
result = 31 * result + (model?.contentHashCode() ?: 0)
|
result = 31 * result + (model?.contentHashCode() ?: 0)
|
||||||
result = 31 * result + size.value
|
result = 31 * result + size.value
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package io.element.android.x.matrix
|
||||||
|
|
||||||
import io.element.android.x.core.data.CoroutineDispatchers
|
import io.element.android.x.core.data.CoroutineDispatchers
|
||||||
import io.element.android.x.matrix.core.UserId
|
import io.element.android.x.matrix.core.UserId
|
||||||
|
import io.element.android.x.matrix.media.MediaResolver
|
||||||
|
import io.element.android.x.matrix.media.RustMediaResolver
|
||||||
import io.element.android.x.matrix.room.MatrixRoom
|
import io.element.android.x.matrix.room.MatrixRoom
|
||||||
import io.element.android.x.matrix.room.RoomSummaryDataSource
|
import io.element.android.x.matrix.room.RoomSummaryDataSource
|
||||||
import io.element.android.x.matrix.room.RustRoomSummaryDataSource
|
import io.element.android.x.matrix.room.RustRoomSummaryDataSource
|
||||||
|
|
@ -64,6 +66,8 @@ class MatrixClient internal constructor(
|
||||||
)
|
)
|
||||||
private var slidingSyncObserverToken: StoppableSpawn? = null
|
private var slidingSyncObserverToken: StoppableSpawn? = null
|
||||||
|
|
||||||
|
private val mediaResolver = RustMediaResolver(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
client.setDelegate(clientDelegate)
|
client.setDelegate(clientDelegate)
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +97,8 @@ class MatrixClient internal constructor(
|
||||||
|
|
||||||
fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource
|
fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource
|
||||||
|
|
||||||
|
fun mediaResolver(): MediaResolver = mediaResolver
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
stopSync()
|
stopSync()
|
||||||
roomSummaryDataSource.close()
|
roomSummaryDataSource.close()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package io.element.android.x.matrix.core
|
package io.element.android.x.matrix.core
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class EventId(val value: String)
|
value class EventId(val value: String) : Serializable
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package io.element.android.x.matrix.core
|
package io.element.android.x.matrix.core
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class RoomId(val value: String)
|
value class RoomId(val value: String): Serializable
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package io.element.android.x.matrix.core
|
package io.element.android.x.matrix.core
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class UserId(val value: String)
|
value class UserId(val value: String): Serializable
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package io.element.android.x.matrix.media
|
||||||
|
|
||||||
|
import io.element.android.x.matrix.MatrixClient
|
||||||
|
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
||||||
|
|
||||||
|
interface MediaResolver {
|
||||||
|
|
||||||
|
sealed interface Kind {
|
||||||
|
data class Thumbnail(val width: Int, val height: Int) : Kind {
|
||||||
|
constructor(size: Int) : this(size, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Content : Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resolve(url: String?, kind: Kind): ByteArray?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class RustMediaResolver(private val client: MatrixClient) : MediaResolver {
|
||||||
|
|
||||||
|
override suspend fun resolve(url: String?, kind: MediaResolver.Kind): ByteArray? {
|
||||||
|
if (url.isNullOrEmpty()) return null
|
||||||
|
val mediaSource = mediaSourceFromUrl(url)
|
||||||
|
return when (kind) {
|
||||||
|
is MediaResolver.Kind.Content -> client.loadMediaContentForSource(mediaSource)
|
||||||
|
is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnailForSource(
|
||||||
|
mediaSource,
|
||||||
|
kind.width.toLong(),
|
||||||
|
kind.height.toLong()
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,13 @@ package io.element.android.x.matrix.room
|
||||||
|
|
||||||
import io.element.android.x.core.data.CoroutineDispatchers
|
import io.element.android.x.core.data.CoroutineDispatchers
|
||||||
import io.element.android.x.matrix.core.RoomId
|
import io.element.android.x.matrix.core.RoomId
|
||||||
|
import io.element.android.x.matrix.core.UserId
|
||||||
import io.element.android.x.matrix.timeline.MatrixTimeline
|
import io.element.android.x.matrix.timeline.MatrixTimeline
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
|
||||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||||
|
|
@ -53,5 +55,18 @@ class MatrixRoom(
|
||||||
return room.avatarUrl()
|
return room.avatarUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun userDisplayName(userId: String): Result<String?> =
|
||||||
|
withContext(coroutineDispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
room.memberDisplayName(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun userAvatarUrl(userId: String): Result<String?> =
|
||||||
|
withContext(coroutineDispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
room.memberAvatarUrl(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue