feat: add private chat info card
Some checks failed
Android CI / android (push) Failing after 5m42s
Android Release / release (push) Has started running
CI / test (push) Has been cancelled

Show a compact Telegram-like counterpart card inside private chats.

Reuse existing chat data for avatar, username, and status instead of adding new profile fetches.

Display contact and blocked badges when the relationship state is already known.
This commit is contained in:
2026-04-05 19:57:52 +03:00
parent f012733df3
commit b3c121b8bc
5 changed files with 130 additions and 0 deletions

View File

@@ -612,6 +612,7 @@ private fun ChatScreen(
state.isCounterpartRelationshipResolved && state.isCounterpartRelationshipResolved &&
!state.isCounterpartContact && !state.isCounterpartContact &&
!state.isCounterpartBlocked !state.isCounterpartBlocked
val showPrivateInfoCard = isPrivateChat && state.counterpartUserId != null
val canShowMembersTab = state.chatType.equals("group", ignoreCase = true) || val canShowMembersTab = state.chatType.equals("group", ignoreCase = true) ||
(state.chatType.equals("channel", ignoreCase = true) && state.canManageMembers) (state.chatType.equals("channel", ignoreCase = true) && state.canManageMembers)
val timelineItems = remember(state.messages, context) { buildChatTimelineItems(state.messages, context) } val timelineItems = remember(state.messages, context) { buildChatTimelineItems(state.messages, context) }
@@ -1165,6 +1166,26 @@ private fun ChatScreen(
} }
} }
} }
AnimatedVisibility(
visible = showPrivateInfoCard,
enter = fadeIn(animationSpec = tween(180)) + slideInVertically(
initialOffsetY = { -it / 4 },
animationSpec = tween(180),
),
exit = fadeOut(animationSpec = tween(120)) + slideOutVertically(
targetOffsetY = { -it / 4 },
animationSpec = tween(120),
),
) {
PrivateChatInfoCard(
title = state.counterpartName?.takeIf { it.isNotBlank() } ?: state.chatTitle,
username = state.counterpartUsername,
avatarUrl = state.chatAvatarUrl,
statusText = state.baseChatSubtitle.takeIf { it.isNotBlank() },
isContact = state.isCounterpartContact,
isBlocked = state.isCounterpartBlocked,
)
}
val strip = topAudioStrip val strip = topAudioStrip
if (strip != null) { if (strip != null) {
Row( Row(
@@ -2242,6 +2263,103 @@ private enum class ChatViewerMediaType {
Video, Video,
} }
@Composable
private fun PrivateChatInfoCard(
title: String,
username: String?,
avatarUrl: String?,
statusText: String?,
isContact: Boolean,
isBlocked: Boolean,
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.78f),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (!avatarUrl.isNullOrBlank()) {
AsyncImage(
model = avatarUrl,
contentDescription = "Private chat avatar",
modifier = Modifier
.size(54.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
} else {
Box(
modifier = Modifier
.size(54.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.18f)),
contentAlignment = Alignment.Center,
) {
Text(
text = title.firstOrNull()?.uppercase() ?: "?",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = title.ifBlank { stringResource(id = R.string.chat_unknown_user) },
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
)
username?.takeIf { it.isNotBlank() }?.let {
Text(
text = "@$it",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
)
}
statusText?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
if (isContact) {
AssistChip(
onClick = {},
enabled = false,
label = { Text(stringResource(id = R.string.chat_private_info_contact)) },
)
}
if (isBlocked) {
AssistChip(
onClick = {},
enabled = false,
label = { Text(stringResource(id = R.string.chat_private_info_blocked)) },
)
}
}
}
}
}
private data class ChatViewerMediaItem( private data class ChatViewerMediaItem(
val url: String, val url: String,
val type: ChatViewerMediaType, val type: ChatViewerMediaType,

View File

@@ -778,6 +778,10 @@ class ChatViewModel @Inject constructor(
it.copy( it.copy(
chatType = chat.type, chatType = chat.type,
counterpartUserId = chat.counterpartUserId, counterpartUserId = chat.counterpartUserId,
counterpartName = chat.counterpartName,
counterpartUsername = chat.counterpartUsername,
counterpartIsOnline = chat.counterpartIsOnline,
counterpartLastSeenAt = chat.counterpartLastSeenAt,
isCounterpartRelationshipResolved = if (chat.type.equals("private", ignoreCase = true) && chat.counterpartUserId != null) { isCounterpartRelationshipResolved = if (chat.type.equals("private", ignoreCase = true) && chat.counterpartUserId != null) {
it.isCounterpartRelationshipResolved it.isCounterpartRelationshipResolved
} else { } else {

View File

@@ -19,6 +19,10 @@ data class MessageUiState(
val chatAvatarUrl: String? = null, val chatAvatarUrl: String? = null,
val chatType: String = "", val chatType: String = "",
val counterpartUserId: Long? = null, val counterpartUserId: Long? = null,
val counterpartName: String? = null,
val counterpartUsername: String? = null,
val counterpartIsOnline: Boolean? = null,
val counterpartLastSeenAt: String? = null,
val chatRole: String? = null, val chatRole: String? = null,
val chatMuted: Boolean = false, val chatMuted: Boolean = false,
val chatUnreadCount: Int = 0, val chatUnreadCount: Int = 0,

View File

@@ -142,6 +142,8 @@
<string name="chat_unknown_user_banner_subtitle">Можно добавить этого человека в контакты или заблокировать прямо из чата.</string> <string name="chat_unknown_user_banner_subtitle">Можно добавить этого человека в контакты или заблокировать прямо из чата.</string>
<string name="chat_unknown_user_add_contact">Добавить контакт</string> <string name="chat_unknown_user_add_contact">Добавить контакт</string>
<string name="chat_unknown_user_block">Заблокировать</string> <string name="chat_unknown_user_block">Заблокировать</string>
<string name="chat_private_info_contact">Контакт</string>
<string name="chat_private_info_blocked">Заблокирован</string>
<string name="chat_type_group">группа</string> <string name="chat_type_group">группа</string>
<string name="chat_type_channel">канал</string> <string name="chat_type_channel">канал</string>
<string name="chat_info_tab_media">Медиа</string> <string name="chat_info_tab_media">Медиа</string>

View File

@@ -142,6 +142,8 @@
<string name="chat_unknown_user_banner_subtitle">You can add this person to contacts or block them right from the chat.</string> <string name="chat_unknown_user_banner_subtitle">You can add this person to contacts or block them right from the chat.</string>
<string name="chat_unknown_user_add_contact">Add contact</string> <string name="chat_unknown_user_add_contact">Add contact</string>
<string name="chat_unknown_user_block">Block user</string> <string name="chat_unknown_user_block">Block user</string>
<string name="chat_private_info_contact">Contact</string>
<string name="chat_private_info_blocked">Blocked</string>
<string name="chat_type_group">group</string> <string name="chat_type_group">group</string>
<string name="chat_type_channel">channel</string> <string name="chat_type_channel">channel</string>
<string name="chat_info_tab_media">Media</string> <string name="chat_info_tab_media">Media</string>