Профиль и группы: фиксированные табы, fast-scroll с датой и Apple Emoji

This commit is contained in:
2026-03-06 18:05:17 +05:00
parent e9944b3c67
commit 8bce15cc19
7 changed files with 459 additions and 62 deletions

View File

@@ -2919,14 +2919,15 @@ fun ChatItem(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
AppleEmojiText(
text = chat.name,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
overflow = android.text.TextUtils.TruncateAt.END,
modifier = Modifier.weight(1f),
enableLinks = false
)
if (isMuted) {
@@ -3722,13 +3723,14 @@ fun DialogItemContent(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
AppleEmojiText(
text = displayName,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
overflow = android.text.TextUtils.TruncateAt.END,
enableLinks = false
)
if (isGroupDialog) {
Spacer(modifier = Modifier.width(5.dp))

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -30,6 +31,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed as gridItemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
@@ -82,7 +87,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
@@ -124,6 +128,7 @@ import com.rosetta.messenger.ui.chats.components.ImageViewerScreen
import com.rosetta.messenger.ui.chats.components.ViewableImage
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.SharedMediaFastScrollOverlay
import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.icons.TelegramIcons
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -133,9 +138,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.core.view.WindowCompat
import org.json.JSONArray
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.roundToInt
private enum class GroupInfoTab(val title: String) {
MEMBERS("Members"),
@@ -370,6 +379,8 @@ fun GroupInfoScreen(
var showImageViewer by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
var imageViewerInitialIndex by rememberSaveable(dialogPublicKey) { mutableStateOf(0) }
var imageViewerSourceBounds by remember(dialogPublicKey) { mutableStateOf<ImageSourceBounds?>(null) }
val groupMediaGridState = rememberLazyGridState()
var groupMediaFastScrollHintDismissed by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
val groupTitle = remember(groupEntity, groupUser.title) {
groupEntity?.title?.trim().takeUnless { it.isNullOrBlank() }
@@ -1134,55 +1145,95 @@ fun GroupInfoScreen(
} else {
val mediaColumns = 3
val mediaSpacing = 1.dp
val mediaScreenWidth = LocalConfiguration.current.screenWidthDp.dp
val mediaCellSize =
(mediaScreenWidth - mediaSpacing * (mediaColumns - 1)) / mediaColumns
val mediaIndexedRows = remember(groupMediaItems) {
groupMediaItems.chunked(mediaColumns).mapIndexed { idx, row -> idx to row }
val mediaRowsCount = remember(groupMediaItems.size) {
ceil(groupMediaItems.size / mediaColumns.toFloat()).toInt().coerceAtLeast(1)
}
val mediaFastScrollVisible by remember(groupMediaItems.size, groupMediaGridState) {
derivedStateOf {
val visibleItems = groupMediaGridState.layoutInfo.visibleItemsInfo
if (visibleItems.isEmpty()) return@derivedStateOf false
val cellHeight = visibleItems.first().size.height
val viewportHeight = groupMediaGridState.layoutInfo.viewportEndOffset -
groupMediaGridState.layoutInfo.viewportStartOffset
mediaRowsCount * cellHeight > viewportHeight
}
}
val mediaFastScrollProgress by remember(groupMediaItems.size, groupMediaGridState) {
derivedStateOf {
val visibleItems = groupMediaGridState.layoutInfo.visibleItemsInfo
if (visibleItems.isEmpty()) return@derivedStateOf 0f
val cellHeight = visibleItems.first().size.height
if (cellHeight <= 0) return@derivedStateOf 0f
val viewportHeight = groupMediaGridState.layoutInfo.viewportEndOffset -
groupMediaGridState.layoutInfo.viewportStartOffset
val totalHeight = mediaRowsCount * cellHeight
val maxScroll = (totalHeight - viewportHeight).coerceAtLeast(1)
val firstRow = groupMediaGridState.firstVisibleItemIndex / mediaColumns
val scrollY = firstRow * cellHeight + groupMediaGridState.firstVisibleItemScrollOffset
(scrollY.toFloat() / maxScroll.toFloat()).coerceIn(0f, 1f)
}
}
val mediaFastScrollMonthLabel by remember(groupMediaItems, groupMediaGridState.firstVisibleItemIndex) {
derivedStateOf {
if (groupMediaItems.isEmpty()) return@derivedStateOf ""
val index = groupMediaGridState.firstVisibleItemIndex.coerceIn(0, groupMediaItems.lastIndex)
formatMediaMonthLabel(groupMediaItems[index].timestamp)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(mediaSpacing)
) {
items(mediaIndexedRows, key = { (idx, _) -> "group_media_row_$idx" }) { (rowIdx, rowMedia) ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(mediaSpacing)
) {
rowMedia.forEachIndexed { colIdx, mediaItem ->
val globalIndex = rowIdx * mediaColumns + colIdx
Box(
modifier = Modifier
.size(mediaCellSize)
.clip(RoundedCornerShape(0.dp))
.background(if (isDarkTheme) Color(0xFF141416) else Color(0xFFE6E7EB))
) {
ImageAttachment(
attachment = mediaItem.attachment,
chachaKey = mediaItem.chachaKey,
privateKey = currentUserPrivateKey,
senderPublicKey = mediaItem.senderPublicKey,
isOutgoing = mediaItem.senderPublicKey == currentUserPublicKey,
isDarkTheme = isDarkTheme,
timestamp = Date(mediaItem.timestamp),
showTimeOverlay = false,
fillMaxSize = true,
onImageClick = { _, bounds ->
imageViewerInitialIndex = globalIndex
imageViewerSourceBounds = bounds
showImageViewer = true
}
)
}
}
repeat(mediaColumns - rowMedia.size) {
Spacer(modifier = Modifier.size(mediaCellSize))
Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid(
columns = GridCells.Fixed(mediaColumns),
state = groupMediaGridState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 20.dp),
horizontalArrangement = Arrangement.spacedBy(mediaSpacing),
verticalArrangement = Arrangement.spacedBy(mediaSpacing)
) {
gridItemsIndexed(groupMediaItems, key = { _, item -> item.key }) { index, mediaItem ->
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(0.dp))
.background(if (isDarkTheme) Color(0xFF141416) else Color(0xFFE6E7EB))
) {
ImageAttachment(
attachment = mediaItem.attachment,
chachaKey = mediaItem.chachaKey,
privateKey = currentUserPrivateKey,
senderPublicKey = mediaItem.senderPublicKey,
isOutgoing = mediaItem.senderPublicKey == currentUserPublicKey,
isDarkTheme = isDarkTheme,
timestamp = Date(mediaItem.timestamp),
showTimeOverlay = false,
fillMaxSize = true,
onImageClick = { _, bounds ->
imageViewerInitialIndex = index
imageViewerSourceBounds = bounds
showImageViewer = true
}
)
}
}
}
SharedMediaFastScrollOverlay(
visible = mediaFastScrollVisible,
progress = mediaFastScrollProgress,
monthLabel = mediaFastScrollMonthLabel,
isDarkTheme = isDarkTheme,
showHint = mediaFastScrollVisible && !groupMediaFastScrollHintDismissed,
onHintDismissed = { groupMediaFastScrollHintDismissed = true },
onDragProgressChanged = { fraction ->
if (groupMediaItems.isEmpty()) return@SharedMediaFastScrollOverlay
val targetRow = ((mediaRowsCount - 1) * fraction).roundToInt()
val targetIndex = (targetRow * mediaColumns).coerceIn(0, groupMediaItems.lastIndex)
scope.launch {
groupMediaGridState.scrollToItem(targetIndex)
}
}
)
}
}
}
@@ -1868,6 +1919,12 @@ private fun shortPublicKey(publicKey: String): String {
return "${trimmed.take(6)}...${trimmed.takeLast(4)}"
}
private fun formatMediaMonthLabel(timestamp: Long): String {
return runCatching {
SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(Date(timestamp))
}.getOrElse { "" }
}
private fun decryptStoredMessageText(encryptedText: String, privateKey: String): String {
if (encryptedText.isBlank()) return ""
if (privateKey.isBlank()) return encryptedText