feat: add private chat info card
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:
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user