Исправлены typing и загрузки файлов, улучшен UI чатов
- Добавлена привязка загрузок файлов к аккаунту и путь сохранения в состоянии загрузки - Реализован экран Downloads с прогрессом и путём файла, плюс анимация открытия/закрытия - Исправлена логика typing-индикаторов в группах и ЛС (без ложных срабатываний) - Доработаны пузырьки групповых сообщений: Telegram-style аватар 42dp и отступы - Исправлено поведение кнопки прокрутки вниз в чате (без мигания при отправке, ближе к инпуту) - Убран Copy на экране Encryption Key группы
This commit is contained in:
@@ -782,8 +782,8 @@ fun ChatDetailScreen(
|
||||
listState.firstVisibleItemScrollOffset <= 12
|
||||
}
|
||||
}
|
||||
val showScrollToBottomButton by remember(messagesWithDates, isAtBottom) {
|
||||
derivedStateOf { messagesWithDates.isNotEmpty() && !isAtBottom }
|
||||
val showScrollToBottomButton by remember(messagesWithDates, isAtBottom, isSendingMessage) {
|
||||
derivedStateOf { messagesWithDates.isNotEmpty() && !isAtBottom && !isSendingMessage }
|
||||
}
|
||||
|
||||
// Telegram-style: Прокрутка ТОЛЬКО при новых сообщениях (не при пагинации)
|
||||
@@ -2551,7 +2551,7 @@ fun ChatDetailScreen(
|
||||
Modifier.align(Alignment.BottomEnd)
|
||||
.padding(
|
||||
end = 14.dp,
|
||||
bottom = if (isSystemAccount) 24.dp else 86.dp
|
||||
bottom = if (isSystemAccount) 24.dp else 16.dp
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
|
||||
@@ -242,9 +242,32 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
// 🔥 Сохраняем ссылки на обработчики для очистки в onCleared()
|
||||
// ВАЖНО: Должны быть определены ДО init блока!
|
||||
private val typingPacketHandler: (Packet) -> Unit = { packet ->
|
||||
private val typingPacketHandler: (Packet) -> Unit = typingPacketHandler@{ packet ->
|
||||
val typingPacket = packet as PacketTyping
|
||||
if (typingPacket.fromPublicKey == opponentKey) {
|
||||
val currentDialog = opponentKey?.trim().orEmpty()
|
||||
val currentAccount = myPublicKey?.trim().orEmpty()
|
||||
if (currentDialog.isBlank() || currentAccount.isBlank()) {
|
||||
return@typingPacketHandler
|
||||
}
|
||||
|
||||
val fromPublicKey = typingPacket.fromPublicKey.trim()
|
||||
val toPublicKey = typingPacket.toPublicKey.trim()
|
||||
if (fromPublicKey.isBlank() || toPublicKey.isBlank()) {
|
||||
return@typingPacketHandler
|
||||
}
|
||||
if (fromPublicKey.equals(currentAccount, ignoreCase = true)) {
|
||||
return@typingPacketHandler
|
||||
}
|
||||
|
||||
val shouldShowTyping =
|
||||
if (isGroupDialogKey(currentDialog)) {
|
||||
normalizeGroupId(toPublicKey).equals(normalizeGroupId(currentDialog), ignoreCase = true)
|
||||
} else {
|
||||
fromPublicKey.equals(currentDialog, ignoreCase = true) &&
|
||||
toPublicKey.equals(currentAccount, ignoreCase = true)
|
||||
}
|
||||
|
||||
if (shouldShowTyping) {
|
||||
showTypingIndicator()
|
||||
}
|
||||
}
|
||||
@@ -4163,7 +4186,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
// 📁 Для Saved Messages - не отправляем typing indicator
|
||||
if (opponent == sender || isGroupDialogKey(opponent)) {
|
||||
if (opponent.equals(sender, ignoreCase = true)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,28 @@ private fun isGroupDialogKey(value: String): Boolean {
|
||||
return normalized.startsWith("#group:") || normalized.startsWith("group:")
|
||||
}
|
||||
|
||||
private fun normalizeGroupDialogKey(value: String): String {
|
||||
val trimmed = value.trim()
|
||||
val normalized = trimmed.lowercase(Locale.ROOT)
|
||||
return when {
|
||||
normalized.startsWith("#group:") -> "#group:${trimmed.substringAfter(':').trim()}"
|
||||
normalized.startsWith("group:") -> "#group:${trimmed.substringAfter(':').trim()}"
|
||||
else -> trimmed
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTypingForDialog(dialogKey: String, typingDialogs: Set<String>): Boolean {
|
||||
if (typingDialogs.isEmpty()) return false
|
||||
if (isGroupDialogKey(dialogKey)) {
|
||||
val normalizedDialogKey = normalizeGroupDialogKey(dialogKey)
|
||||
return typingDialogs.any {
|
||||
isGroupDialogKey(it) &&
|
||||
normalizeGroupDialogKey(it).equals(normalizedDialogKey, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
return typingDialogs.any { it.equals(dialogKey.trim(), ignoreCase = true) }
|
||||
}
|
||||
|
||||
private val TELEGRAM_DIALOG_AVATAR_START = 10.dp
|
||||
private val TELEGRAM_DIALOG_TEXT_START = 72.dp
|
||||
private val TELEGRAM_DIALOG_AVATAR_SIZE = 54.dp
|
||||
@@ -442,9 +464,29 @@ fun ChatsListScreen(
|
||||
val syncInProgress by ProtocolManager.syncInProgress.collectAsState()
|
||||
val pendingDeviceVerification by ProtocolManager.pendingDeviceVerification.collectAsState()
|
||||
|
||||
// <EFBFBD> Active downloads tracking (for header indicator)
|
||||
val activeDownloads by com.rosetta.messenger.network.TransportManager.downloading.collectAsState()
|
||||
val hasActiveDownloads = activeDownloads.isNotEmpty()
|
||||
// 📥 Active FILE downloads tracking (account-scoped, excludes photo downloads)
|
||||
val currentAccountKey = remember(accountPublicKey) { accountPublicKey.trim() }
|
||||
val allFileDownloads by
|
||||
com.rosetta.messenger.network.FileDownloadManager.downloads.collectAsState()
|
||||
val accountFileDownloads = remember(allFileDownloads, currentAccountKey) {
|
||||
allFileDownloads.values
|
||||
.filter {
|
||||
it.accountPublicKey.equals(currentAccountKey, ignoreCase = true)
|
||||
}
|
||||
.sortedByDescending { it.progress }
|
||||
}
|
||||
val activeFileDownloads = remember(accountFileDownloads) {
|
||||
accountFileDownloads.filter {
|
||||
it.status == com.rosetta.messenger.network.FileDownloadStatus.QUEUED ||
|
||||
it.status ==
|
||||
com.rosetta.messenger.network.FileDownloadStatus
|
||||
.DOWNLOADING ||
|
||||
it.status ==
|
||||
com.rosetta.messenger.network.FileDownloadStatus
|
||||
.DECRYPTING
|
||||
}
|
||||
}
|
||||
val hasActiveDownloads = activeFileDownloads.isNotEmpty()
|
||||
|
||||
// <20>🔥 Пользователи, которые сейчас печатают
|
||||
val typingUsers by ProtocolManager.typingUsers.collectAsState()
|
||||
@@ -473,6 +515,7 @@ fun ChatsListScreen(
|
||||
|
||||
// 📬 Requests screen state
|
||||
var showRequestsScreen by remember { mutableStateOf(false) }
|
||||
var showDownloadsScreen by remember { mutableStateOf(false) }
|
||||
var isInlineRequestsTransitionLocked by remember { mutableStateOf(false) }
|
||||
var isRequestsRouteTapLocked by remember { mutableStateOf(false) }
|
||||
val inlineRequestsTransitionLockMs = 340L
|
||||
@@ -498,6 +541,10 @@ fun ChatsListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentAccountKey) {
|
||||
showDownloadsScreen = false
|
||||
}
|
||||
|
||||
// 📂 Accounts section expanded state (arrow toggle)
|
||||
var accountsSectionExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -535,8 +582,10 @@ fun ChatsListScreen(
|
||||
|
||||
// Back: drawer → закрыть, selection → сбросить
|
||||
// Когда ничего не открыто — НЕ перехватываем, система сама закроет приложение корректно
|
||||
BackHandler(enabled = isSelectionMode || drawerState.isOpen) {
|
||||
if (isSelectionMode) {
|
||||
BackHandler(enabled = showDownloadsScreen || isSelectionMode || drawerState.isOpen) {
|
||||
if (showDownloadsScreen) {
|
||||
showDownloadsScreen = false
|
||||
} else if (isSelectionMode) {
|
||||
selectedChatKeys = emptySet()
|
||||
} else if (drawerState.isOpen) {
|
||||
scope.launch { drawerState.close() }
|
||||
@@ -709,7 +758,7 @@ fun ChatsListScreen(
|
||||
) {
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = !showRequestsScreen,
|
||||
gesturesEnabled = !showRequestsScreen && !showDownloadsScreen,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
drawerContainerColor = Color.Transparent,
|
||||
@@ -1335,7 +1384,12 @@ fun ChatsListScreen(
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
key(isDarkTheme, showRequestsScreen, isSelectionMode) {
|
||||
key(
|
||||
isDarkTheme,
|
||||
showRequestsScreen,
|
||||
showDownloadsScreen,
|
||||
isSelectionMode
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = isSelectionMode,
|
||||
animationSpec = tween(200),
|
||||
@@ -1473,12 +1527,16 @@ fun ChatsListScreen(
|
||||
// ═══ NORMAL HEADER ═══
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
if (showRequestsScreen) {
|
||||
if (showRequestsScreen || showDownloadsScreen) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
setInlineRequestsVisible(
|
||||
false
|
||||
)
|
||||
if (showDownloadsScreen) {
|
||||
showDownloadsScreen = false
|
||||
} else {
|
||||
setInlineRequestsVisible(
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@@ -1557,7 +1615,14 @@ fun ChatsListScreen(
|
||||
}
|
||||
},
|
||||
title = {
|
||||
if (showRequestsScreen) {
|
||||
if (showDownloadsScreen) {
|
||||
Text(
|
||||
"Downloads",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
color = Color.White
|
||||
)
|
||||
} else if (showRequestsScreen) {
|
||||
Text(
|
||||
"Requests",
|
||||
fontWeight =
|
||||
@@ -1596,10 +1661,18 @@ fun ChatsListScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (!showRequestsScreen) {
|
||||
if (!showRequestsScreen && !showDownloadsScreen) {
|
||||
// 📥 Animated download indicator (Telegram-style)
|
||||
Box(
|
||||
modifier = androidx.compose.ui.Modifier.size(48.dp),
|
||||
modifier =
|
||||
androidx.compose.ui.Modifier
|
||||
.size(48.dp)
|
||||
.clickable(
|
||||
enabled = hasActiveDownloads,
|
||||
onClick = {
|
||||
showDownloadsScreen = true
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
com.rosetta.messenger.ui.components.AnimatedDownloadIndicator(
|
||||
@@ -1709,39 +1782,105 @@ fun ChatsListScreen(
|
||||
|
||||
val showSkeleton = isLoading
|
||||
|
||||
// 🎬 Animated content transition between main list and
|
||||
// requests
|
||||
AnimatedContent(
|
||||
targetState = showRequestsScreen,
|
||||
targetState = showDownloadsScreen,
|
||||
transitionSpec = {
|
||||
if (targetState) {
|
||||
// Opening requests: slide in from right
|
||||
// Opening downloads: slide from right with fade
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> fullWidth } + fadeIn(
|
||||
animationSpec =
|
||||
tween(
|
||||
280,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) { fullWidth ->
|
||||
fullWidth
|
||||
} + fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> -fullWidth / 4 } + fadeOut(
|
||||
animationSpec =
|
||||
tween(
|
||||
280,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) { fullWidth ->
|
||||
-fullWidth / 4
|
||||
} + fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
} else {
|
||||
// Closing requests: slide out to right
|
||||
// Closing downloads: slide back to right
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> -fullWidth / 4 } + fadeIn(
|
||||
animationSpec =
|
||||
tween(
|
||||
280,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) { fullWidth ->
|
||||
-fullWidth / 4
|
||||
} + fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> fullWidth } + fadeOut(
|
||||
animationSpec =
|
||||
tween(
|
||||
280,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) { fullWidth ->
|
||||
fullWidth
|
||||
} + fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
},
|
||||
label = "RequestsTransition"
|
||||
) { isRequestsScreen ->
|
||||
label = "DownloadsTransition"
|
||||
) { isDownloadsScreen ->
|
||||
if (isDownloadsScreen) {
|
||||
FileDownloadsScreen(
|
||||
downloads = activeFileDownloads,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
// 🎬 Animated content transition between main list and
|
||||
// requests
|
||||
AnimatedContent(
|
||||
targetState = showRequestsScreen,
|
||||
transitionSpec = {
|
||||
if (targetState) {
|
||||
// Opening requests: slide in from right
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> fullWidth } + fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> -fullWidth / 4 } + fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
} else {
|
||||
// Closing requests: slide out to right
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> -fullWidth / 4 } + fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> fullWidth } + fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
},
|
||||
label = "RequestsTransition"
|
||||
) { isRequestsScreen ->
|
||||
if (isRequestsScreen) {
|
||||
// 📬 Show Requests Screen with swipe-back
|
||||
Box(
|
||||
@@ -2054,13 +2193,14 @@ fun ChatsListScreen(
|
||||
)
|
||||
val isTyping by
|
||||
remember(
|
||||
dialog.opponentKey
|
||||
dialog.opponentKey,
|
||||
typingUsers
|
||||
) {
|
||||
derivedStateOf {
|
||||
typingUsers
|
||||
.contains(
|
||||
dialog.opponentKey
|
||||
)
|
||||
isTypingForDialog(
|
||||
dialog.opponentKey,
|
||||
typingUsers
|
||||
)
|
||||
}
|
||||
}
|
||||
val isSelectedDialog =
|
||||
@@ -2241,7 +2381,9 @@ fun ChatsListScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Close AnimatedContent
|
||||
} // Close AnimatedContent
|
||||
} // Close downloads/main content switch
|
||||
} // Close Downloads AnimatedContent
|
||||
|
||||
// Console button removed
|
||||
}
|
||||
@@ -3746,7 +3888,11 @@ fun DialogItemContent(
|
||||
MessageRepository.isSystemAccount(dialog.opponentKey)
|
||||
if (dialog.verified > 0 || isRosettaOfficial) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
VerifiedBadge(verified = if (dialog.verified > 0) dialog.verified else 1, size = 16)
|
||||
VerifiedBadge(
|
||||
verified = if (dialog.verified > 0) dialog.verified else 1,
|
||||
size = 16,
|
||||
modifier = Modifier.offset(y = (-1).dp)
|
||||
)
|
||||
}
|
||||
// 🔒 Красная иконка замочка для заблокированных пользователей
|
||||
if (isBlocked) {
|
||||
@@ -4561,6 +4707,136 @@ fun RequestsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileDownloadsScreen(
|
||||
downloads: List<com.rosetta.messenger.network.FileDownloadState>,
|
||||
isDarkTheme: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val background = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
||||
val card = if (isDarkTheme) Color(0xFF212123) else Color.White
|
||||
val primaryText = if (isDarkTheme) Color.White else Color(0xFF111111)
|
||||
val secondaryText = if (isDarkTheme) Color(0xFF9B9B9F) else Color(0xFF6E6E73)
|
||||
val divider = if (isDarkTheme) Color(0xFF2F2F31) else Color(0xFFE7E7EC)
|
||||
|
||||
if (downloads.isEmpty()) {
|
||||
Box(
|
||||
modifier = modifier.background(background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.Download,
|
||||
contentDescription = null,
|
||||
tint = secondaryText,
|
||||
modifier = Modifier.size(34.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = "No active file downloads",
|
||||
color = primaryText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "New file downloads will appear here.",
|
||||
color = secondaryText,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.background(background),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(items = downloads, key = { it.attachmentId }) { item ->
|
||||
Surface(
|
||||
color = card,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(12.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (isDarkTheme) Color(0xFF2B4E6E)
|
||||
else Color(0xFFDCEEFF)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.Download,
|
||||
contentDescription = null,
|
||||
tint = if (isDarkTheme) Color(0xFF8FC6FF) else Color(0xFF228BE6),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.fileName.ifBlank { "Unknown file" },
|
||||
color = primaryText,
|
||||
fontSize = 15.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = formatDownloadStatusText(item),
|
||||
color = secondaryText,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = item.progress.coerceIn(0f, 1f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(50)),
|
||||
color = if (isDarkTheme) Color(0xFF4DA6FF) else Color(0xFF228BE6),
|
||||
trackColor = if (isDarkTheme) Color(0xFF3A3A3D) else Color(0xFFD8D8DE)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Divider(color = divider, thickness = 0.5.dp)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = item.savedPath.ifBlank { "Storage path unavailable" },
|
||||
color = secondaryText,
|
||||
fontSize = 11.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDownloadStatusText(
|
||||
item: com.rosetta.messenger.network.FileDownloadState
|
||||
): String {
|
||||
val percent = (item.progress.coerceIn(0f, 1f) * 100).toInt().coerceIn(0, 100)
|
||||
return when (item.status) {
|
||||
com.rosetta.messenger.network.FileDownloadStatus.QUEUED -> "Queued"
|
||||
com.rosetta.messenger.network.FileDownloadStatus.DOWNLOADING -> "Downloading $percent%"
|
||||
com.rosetta.messenger.network.FileDownloadStatus.DECRYPTING -> "Decrypting"
|
||||
com.rosetta.messenger.network.FileDownloadStatus.DONE -> "Completed"
|
||||
com.rosetta.messenger.network.FileDownloadStatus.ERROR -> "Download failed"
|
||||
}
|
||||
}
|
||||
|
||||
/** 🎨 Enhanced Drawer Menu Item - пункт меню в стиле Telegram */
|
||||
@Composable
|
||||
fun DrawerMenuItemEnhanced(
|
||||
|
||||
@@ -1428,11 +1428,7 @@ fun GroupInfoScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
topSurfaceColor = topSurfaceColor,
|
||||
backgroundColor = backgroundColor,
|
||||
onBack = { showEncryptionPage = false },
|
||||
onCopy = {
|
||||
clipboardManager.setText(AnnotatedString(encryptionKey))
|
||||
Toast.makeText(context, "Encryption key copied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
onBack = { showEncryptionPage = false }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1570,8 +1566,7 @@ private fun GroupEncryptionKeyPage(
|
||||
isDarkTheme: Boolean,
|
||||
topSurfaceColor: Color,
|
||||
backgroundColor: Color,
|
||||
onBack: () -> Unit,
|
||||
onCopy: () -> Unit
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val safePeerTitle = peerTitle.ifBlank { "this group" }
|
||||
@@ -1614,14 +1609,6 @@ private fun GroupEncryptionKeyPage(
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(onClick = onCopy) {
|
||||
Text(
|
||||
text = "Copy",
|
||||
color = Color.White.copy(alpha = 0.9f),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Two-half layout like Telegram
|
||||
|
||||
@@ -395,6 +395,7 @@ fun MessageAttachments(
|
||||
attachment = attachment,
|
||||
chachaKey = chachaKey,
|
||||
privateKey = privateKey,
|
||||
currentUserPublicKey = currentUserPublicKey,
|
||||
isOutgoing = isOutgoing,
|
||||
isDarkTheme = isDarkTheme,
|
||||
timestamp = timestamp,
|
||||
@@ -1444,6 +1445,7 @@ fun FileAttachment(
|
||||
attachment: MessageAttachment,
|
||||
chachaKey: String,
|
||||
privateKey: String,
|
||||
currentUserPublicKey: String = "",
|
||||
isOutgoing: Boolean,
|
||||
isDarkTheme: Boolean,
|
||||
timestamp: java.util.Date,
|
||||
@@ -1557,6 +1559,7 @@ fun FileAttachment(
|
||||
downloadTag = downloadTag,
|
||||
chachaKey = chachaKey,
|
||||
privateKey = privateKey,
|
||||
accountPublicKey = currentUserPublicKey,
|
||||
fileName = fileName,
|
||||
savedFile = savedFile
|
||||
)
|
||||
|
||||
@@ -71,6 +71,7 @@ import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.ui.chats.models.*
|
||||
import com.rosetta.messenger.ui.chats.utils.*
|
||||
import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||
import com.rosetta.messenger.ui.components.AvatarImage
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.utils.AttachmentFileManager
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
@@ -532,6 +533,15 @@ fun MessageBubble(
|
||||
val combinedBackgroundColor =
|
||||
if (isSelected) selectionBackgroundColor else highlightBackgroundColor
|
||||
|
||||
val telegramIncomingAvatarSize = 42.dp
|
||||
val telegramIncomingAvatarLane = 48.dp
|
||||
val telegramIncomingAvatarInset = 6.dp
|
||||
val showIncomingGroupAvatar =
|
||||
isGroupChat &&
|
||||
!message.isOutgoing &&
|
||||
showTail &&
|
||||
senderPublicKey.isNotBlank()
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
@@ -579,6 +589,39 @@ fun MessageBubble(
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
if (!message.isOutgoing && isGroupChat) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.width(telegramIncomingAvatarLane)
|
||||
.height(telegramIncomingAvatarSize)
|
||||
.align(Alignment.Bottom),
|
||||
contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
if (showIncomingGroupAvatar) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.padding(
|
||||
start =
|
||||
telegramIncomingAvatarInset
|
||||
),
|
||||
contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
AvatarImage(
|
||||
publicKey = senderPublicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
size = telegramIncomingAvatarSize,
|
||||
isDarkTheme = isDarkTheme,
|
||||
displayName =
|
||||
senderName.ifBlank {
|
||||
senderPublicKey
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем - есть ли только фотки без текста
|
||||
val hasOnlyMedia =
|
||||
message.attachments.isNotEmpty() &&
|
||||
@@ -708,7 +751,8 @@ fun MessageBubble(
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(end = 12.dp)
|
||||
Modifier.align(Alignment.Bottom)
|
||||
.padding(end = 12.dp)
|
||||
.then(bubbleWidthModifier)
|
||||
.graphicsLayer {
|
||||
this.alpha = selectionAlpha
|
||||
|
||||
Reference in New Issue
Block a user