mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Add avatars to chat list
This commit is contained in:
@@ -42,6 +42,35 @@ object ProfileCache {
|
||||
* Fills or refreshes a lightweight profile from a public chat [Message] (username, avatar URL).
|
||||
* Skips when a full API profile is already stored ([UserProfile.isClientPreviewOnly] is false).
|
||||
*/
|
||||
/**
|
||||
* Seeds or refreshes a lightweight profile from a DM conversations list [User].
|
||||
* Skips when a full `/user/...` profile is already cached.
|
||||
*/
|
||||
fun mergeFromDmUser(user: User) {
|
||||
val existing = get(user.id)
|
||||
if (existing != null && !existing.isClientPreviewOnly) return
|
||||
put(
|
||||
UserProfile(
|
||||
id = user.id,
|
||||
username = user.username.ifBlank { existing?.username.orEmpty() },
|
||||
displayName = existing?.displayName?.takeIf { it.isNotBlank() }
|
||||
?: user.username.takeIf { it.isNotBlank() }
|
||||
?: existing?.username.orEmpty(),
|
||||
profilePicture = user.profile_picture?.takeIf { it.isNotBlank() }
|
||||
?: existing?.profilePicture,
|
||||
bio = existing?.bio,
|
||||
online = user.online,
|
||||
lastSeen = user.last_seen.takeIf { it.isNotBlank() } ?: existing?.lastSeen,
|
||||
createdAt = user.created_at.takeIf { it.isNotBlank() } ?: existing?.createdAt,
|
||||
verified = existing?.verified,
|
||||
suspended = existing?.suspended,
|
||||
suspensionReason = existing?.suspensionReason,
|
||||
deleted = existing?.deleted,
|
||||
isClientPreviewOnly = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun mergePreviewFromPublicMessage(message: Message) {
|
||||
val uid = message.user_id
|
||||
if (uid <= 0) return
|
||||
|
||||
@@ -6,9 +6,14 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -26,6 +31,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -33,6 +39,7 @@ import ru.fromchat.Res
|
||||
import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.ConnectionStateStore
|
||||
import ru.fromchat.api.ConnectionStatus
|
||||
import ru.fromchat.api.ProfileCache
|
||||
import ru.fromchat.api.db.CachedConversation
|
||||
import ru.fromchat.api.db.MessageCacheStore
|
||||
import ru.fromchat.chat_last_mesaage
|
||||
@@ -40,6 +47,32 @@ import ru.fromchat.public_chat
|
||||
import ru.fromchat.net.NetworkConnectivity
|
||||
import ru.fromchat.ui.ConnectingEllipsis
|
||||
import ru.fromchat.ui.LocalNavController
|
||||
import ru.fromchat.ui.chat.Avatar
|
||||
|
||||
@Composable
|
||||
private fun ChatRowAvatar(
|
||||
profilePictureUrl: String?,
|
||||
displayNameForInitials: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier
|
||||
.size(40.dp)
|
||||
.clickable(
|
||||
interactionSource = interaction,
|
||||
indication = LocalIndication.current,
|
||||
onClick = onClick
|
||||
)
|
||||
) {
|
||||
Avatar(
|
||||
profilePictureUrl = profilePictureUrl,
|
||||
displayName = displayNameForInitials,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -49,6 +82,10 @@ fun ChatsTab() {
|
||||
val connectionStatus by ConnectionStateStore.status.collectAsState()
|
||||
val online by NetworkConnectivity.isOnline.collectAsState(initial = true)
|
||||
var dmConversations by remember { mutableStateOf<List<CachedConversation>>(emptyList()) }
|
||||
var publicListAvatarUserId by remember { mutableStateOf<Int?>(null) }
|
||||
var publicListAvatarUrl by remember { mutableStateOf<String?>(null) }
|
||||
var publicListAvatarLabel by remember { mutableStateOf<String?>(null) }
|
||||
var publicLastMessagePreview by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Load cached DM conversations first for instant offline display.
|
||||
@@ -56,11 +93,28 @@ fun ChatsTab() {
|
||||
dmConversations = MessageCacheStore.loadCachedDmConversations()
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val last = MessageCacheStore.loadRecentPublicMessages(1).lastOrNull()
|
||||
publicLastMessagePreview = last?.content?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (last != null && last.user_id > 0) {
|
||||
ProfileCache.mergePreviewFromPublicMessage(last)
|
||||
publicListAvatarUserId = last.user_id
|
||||
publicListAvatarUrl = last.profile_picture
|
||||
publicListAvatarLabel = last.username.takeIf { it.isNotBlank() }
|
||||
?: ProfileCache.get(last.user_id)?.displayName?.takeIf { it.isNotBlank() }
|
||||
} else {
|
||||
publicListAvatarUserId = null
|
||||
publicListAvatarUrl = null
|
||||
publicListAvatarLabel = null
|
||||
}
|
||||
}
|
||||
|
||||
// Then refresh from network and update cache + state.
|
||||
runCatching {
|
||||
ApiClient.getDmConversations()
|
||||
}.onSuccess { conversations ->
|
||||
runCatching {
|
||||
conversations.forEach { ProfileCache.mergeFromDmUser(it.user) }
|
||||
MessageCacheStore.replaceDmConversations(conversations)
|
||||
dmConversations = MessageCacheStore.loadCachedDmConversations()
|
||||
}
|
||||
@@ -130,11 +184,41 @@ fun ChatsTab() {
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
val publicChatTitle = stringResource(Res.string.public_chat)
|
||||
LazyColumn(contentPadding = innerPadding) {
|
||||
item {
|
||||
val publicUid = publicListAvatarUserId
|
||||
val publicPic = publicListAvatarUrl
|
||||
val publicLabel = publicListAvatarLabel?.takeIf { it.isNotBlank() } ?: publicChatTitle
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(Res.string.public_chat)) },
|
||||
supportingContent = { Text(stringResource(Res.string.chat_last_mesaage)) },
|
||||
leadingContent = {
|
||||
ChatRowAvatar(
|
||||
profilePictureUrl = publicPic,
|
||||
displayNameForInitials = publicLabel,
|
||||
onClick = {
|
||||
when {
|
||||
publicUid != null && publicUid > 0 ->
|
||||
navController.navigate("profile/$publicUid")
|
||||
else -> navController.navigate("chats/publicChat")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = publicChatTitle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
val preview = publicLastMessagePreview
|
||||
Text(
|
||||
text = preview ?: stringResource(Res.string.chat_last_mesaage),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
navController.navigate("chats/publicChat")
|
||||
}
|
||||
@@ -143,11 +227,30 @@ fun ChatsTab() {
|
||||
|
||||
items(dmConversations.size) { index ->
|
||||
val conv = dmConversations[index]
|
||||
val cached = ProfileCache.get(conv.otherUserId)
|
||||
val avatarUrl = cached?.profilePicture
|
||||
val avatarLabel = cached?.displayName?.takeIf { it.isNotBlank() }
|
||||
?: cached?.username?.takeIf { it.isNotBlank() }
|
||||
?: conv.displayName.ifBlank { "User ${conv.otherUserId}" }
|
||||
val preview = conv.lastMessagePreview ?: "Direct messages"
|
||||
ListItem(
|
||||
headlineContent = { Text(conv.displayName.ifBlank { "User ${conv.otherUserId}" }) },
|
||||
supportingContent = {
|
||||
val preview = conv.lastMessagePreview ?: "Direct messages"
|
||||
Text(preview)
|
||||
leadingContent = {
|
||||
ChatRowAvatar(
|
||||
profilePictureUrl = avatarUrl,
|
||||
displayNameForInitials = avatarLabel,
|
||||
onClick = {
|
||||
if (conv.otherUserId != 0) {
|
||||
navController.navigate("profile/${conv.otherUserId}")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = preview,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (conv.unreadCount > 0) {
|
||||
|
||||
@@ -299,12 +299,14 @@ fun ProfileScreen(
|
||||
userId = profile.id
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "@${profile.username}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (profile.username.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "@${profile.username}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
Row(
|
||||
@@ -418,21 +420,23 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
Category(Modifier.padding(top = 16.dp), title = "Details") {
|
||||
ListItem(
|
||||
headline = "Username",
|
||||
supportingText = profile.username,
|
||||
leadingContent = {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AlternateEmail,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
divider = true,
|
||||
dividerColor = CategoryDefaults.dividerColor,
|
||||
dividerThickness = CategoryDefaults.dividerThickness
|
||||
)
|
||||
if (profile.username.isNotBlank()) {
|
||||
ListItem(
|
||||
headline = "Username",
|
||||
supportingText = profile.username,
|
||||
leadingContent = {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AlternateEmail,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
divider = true,
|
||||
dividerColor = CategoryDefaults.dividerColor,
|
||||
dividerThickness = CategoryDefaults.dividerThickness
|
||||
)
|
||||
}
|
||||
|
||||
if (!profile.bio.isNullOrBlank()) {
|
||||
ListItem(
|
||||
|
||||
Reference in New Issue
Block a user