fix: round avatar
This commit is contained in:
@@ -7,11 +7,13 @@ package ru.risdeveau.pixeldragon.ui.item
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
||||
@@ -22,17 +24,19 @@ fun Avatar(
|
||||
fallbackName: String,
|
||||
fallbackColor: Color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
) {
|
||||
val avatarModifier = modifier.clip(CircleShape)
|
||||
|
||||
if (url != null) {
|
||||
MXCImage(
|
||||
mxcUrl = url,
|
||||
modifier = modifier,
|
||||
modifier = avatarModifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = fallbackName,
|
||||
showProgress = false,
|
||||
)
|
||||
} else {
|
||||
AvatarPlaceholder(
|
||||
modifier,
|
||||
avatarModifier,
|
||||
fallbackName,
|
||||
fallbackColor,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ package ru.risdeveau.pixeldragon.ui.item
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import io.github.rabehx.iconsax.Iconsax
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -24,16 +23,20 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import de.connect2x.trixnity.client.media
|
||||
import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
|
||||
import io.github.rabehx.iconsax.Iconsax
|
||||
import io.github.rabehx.iconsax.outline.Warning2
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
enum class ImageLoadState {
|
||||
Loading, Success, Error
|
||||
}
|
||||
|
||||
private val mxcImageByteCache = ConcurrentHashMap<String, ByteArray>()
|
||||
|
||||
@Composable
|
||||
fun MXCImage(
|
||||
mxcUrl: String,
|
||||
@@ -43,16 +46,26 @@ fun MXCImage(
|
||||
showProgress: Boolean = true
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var imageLoadState by remember { mutableStateOf(ImageLoadState.Loading) }
|
||||
var imageBytes by remember { mutableStateOf<ByteArray?>(null) }
|
||||
val cachedBytes = remember(mxcUrl) { mxcImageByteCache[mxcUrl] }
|
||||
var imageLoadState by remember(mxcUrl) {
|
||||
mutableStateOf(if (cachedBytes != null) ImageLoadState.Success else ImageLoadState.Loading)
|
||||
}
|
||||
var imageBytes by remember(mxcUrl) { mutableStateOf(cachedBytes) }
|
||||
|
||||
val progressFlow = remember { MutableStateFlow<FileTransferProgress?>(null) }
|
||||
val progressFlow = remember(mxcUrl) { MutableStateFlow<FileTransferProgress?>(null) }
|
||||
|
||||
LaunchedEffect(mxcUrl) {
|
||||
if (mxcUrl.isBlank()) {
|
||||
imageLoadState = ImageLoadState.Error
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
mxcImageByteCache[mxcUrl]?.let { bytes ->
|
||||
imageBytes = bytes
|
||||
imageLoadState = ImageLoadState.Success
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
imageLoadState = ImageLoadState.Loading
|
||||
progressFlow.value = null
|
||||
|
||||
@@ -66,6 +79,7 @@ fun MXCImage(
|
||||
onSuccess = { media ->
|
||||
val bytes = media.toByteArray()
|
||||
if (bytes != null) {
|
||||
mxcImageByteCache[mxcUrl] = bytes
|
||||
imageBytes = bytes
|
||||
ImageLoadState.Success
|
||||
} else {
|
||||
@@ -87,6 +101,8 @@ fun MXCImage(
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(imageBytes)
|
||||
.memoryCacheKey(mxcUrl)
|
||||
.diskCacheKey(mxcUrl)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +88,10 @@ import de.connect2x.trixnity.client.room.message.text
|
||||
import de.connect2x.trixnity.client.room.toFlowList
|
||||
import de.connect2x.trixnity.client.store.TimelineEvent
|
||||
import de.connect2x.trixnity.client.store.eventId
|
||||
import de.connect2x.trixnity.clientserverapi.model.user.avatarUrl
|
||||
import de.connect2x.trixnity.clientserverapi.model.user.displayName
|
||||
import de.connect2x.trixnity.core.model.RoomId
|
||||
import de.connect2x.trixnity.core.model.UserId
|
||||
import de.connect2x.trixnity.core.model.events.ClientEvent
|
||||
import de.connect2x.trixnity.core.model.events.m.FullyReadEventContent
|
||||
import de.connect2x.trixnity.core.model.events.m.room.AvatarEventContent
|
||||
@@ -114,10 +117,12 @@ import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.safety.Safelist
|
||||
import ru.risdeveau.pixeldragon.client
|
||||
import ru.risdeveau.pixeldragon.ui.item.Avatar
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
@@ -365,7 +370,11 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||
}
|
||||
.zIndex(1f),
|
||||
) {
|
||||
AvatarPlaceholder(avatarState.senderLabel)
|
||||
TimelineAvatar(
|
||||
modifier = Modifier.size(TIMELINE_AVATAR_SIZE),
|
||||
senderId = avatarState.senderId,
|
||||
senderLabel = avatarState.senderLabel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,6 +458,7 @@ private data class MessageTimelineItem(
|
||||
|
||||
private data class StickyAvatarState(
|
||||
val groupKey: String,
|
||||
val senderId: String?,
|
||||
val senderLabel: String,
|
||||
val yPx: Int,
|
||||
)
|
||||
@@ -677,7 +687,11 @@ private fun MessageGroupRow(
|
||||
if (hideInlineAvatar) {
|
||||
Spacer(Modifier.size(TIMELINE_AVATAR_SIZE))
|
||||
} else {
|
||||
AvatarPlaceholder(group.senderLabel)
|
||||
TimelineAvatar(
|
||||
modifier = Modifier.size(TIMELINE_AVATAR_SIZE),
|
||||
senderId = group.senderId,
|
||||
senderLabel = group.senderLabel,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
@@ -699,6 +713,61 @@ private fun MessageGroupRow(
|
||||
}
|
||||
}
|
||||
|
||||
private val timelineAvatarProfileCache = ConcurrentHashMap<String, TimelineAvatarProfile>()
|
||||
|
||||
private data class TimelineAvatarProfile(
|
||||
val displayName: String,
|
||||
val avatarUrl: String?,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun TimelineAvatar(
|
||||
modifier: Modifier = Modifier,
|
||||
senderId: String?,
|
||||
senderLabel: String,
|
||||
) {
|
||||
val fallbackProfile = remember(senderId, senderLabel) {
|
||||
TimelineAvatarProfile(senderLabel.takeUnless { it.isBlank() } ?: "?", null)
|
||||
}
|
||||
var profile by remember(senderId, senderLabel) {
|
||||
mutableStateOf(senderId?.let(timelineAvatarProfileCache::get) ?: fallbackProfile)
|
||||
}
|
||||
|
||||
LaunchedEffect(senderId, senderLabel) {
|
||||
val id = senderId ?: run {
|
||||
profile = fallbackProfile
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
timelineAvatarProfileCache[id]?.let { cached ->
|
||||
profile = cached
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val fetchedProfile = client?.api?.user
|
||||
?.getProfile(UserId(id))
|
||||
?.getOrNull()
|
||||
|
||||
val resolvedProfile = TimelineAvatarProfile(
|
||||
displayName = fetchedProfile
|
||||
?.displayName
|
||||
?.takeUnless { it.isBlank() }
|
||||
?: senderLabel.takeUnless { it.isBlank() }
|
||||
?: "?",
|
||||
avatarUrl = fetchedProfile?.avatarUrl,
|
||||
)
|
||||
|
||||
timelineAvatarProfileCache[id] = resolvedProfile
|
||||
profile = resolvedProfile
|
||||
}
|
||||
|
||||
Avatar(
|
||||
modifier = modifier,
|
||||
url = profile.avatarUrl,
|
||||
fallbackName = profile.displayName,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBubble(
|
||||
item: MessageTimelineItem,
|
||||
@@ -744,27 +813,6 @@ private fun MessageBubble(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarPlaceholder(senderLabel: String) {
|
||||
val initial = senderLabel.trim().firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = initial,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBody(
|
||||
content: RoomMessageEventContent,
|
||||
@@ -821,6 +869,7 @@ private fun calculateStickyAvatarState(
|
||||
|
||||
StickyAvatarState(
|
||||
groupKey = group.key,
|
||||
senderId = group.senderId,
|
||||
senderLabel = group.senderLabel,
|
||||
yPx = pinnedAvatarTop.coerceAtLeast(groupTop),
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ composeBom = "2026.03.00"
|
||||
navigationCompose = "2.9.7"
|
||||
room = "2.6.1"
|
||||
splittiesFunPackAndroidBase = "3.0.0"
|
||||
trixnityClient = "5.2.0"
|
||||
trixnityClient = "5.5.2"
|
||||
unifiedPushConnector = "3.3.2"
|
||||
uiUnit = "1.10.6"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user