Синхронизированы анимации чата и Requests с Telegram, добавлен shimmer и исправлено копирование текста
This commit is contained in:
@@ -17,8 +17,13 @@ import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.SizeTransform
|
||||
@@ -53,6 +58,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
@@ -119,6 +125,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val groupMembersCountCache = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
||||
|
||||
private data class IncomingRunAvatarAccumulator(
|
||||
val senderPublicKey: String,
|
||||
val senderDisplayName: String,
|
||||
@@ -515,11 +523,15 @@ fun ChatDetailScreen(
|
||||
val chatsListViewModel: ChatsListViewModel = viewModel()
|
||||
val dialogsList by chatsListViewModel.dialogs.collectAsState()
|
||||
val groupRepository = remember { GroupRepository.getInstance(context) }
|
||||
val groupMembersCacheKey =
|
||||
remember(user.publicKey, currentUserPublicKey) {
|
||||
"${currentUserPublicKey.trim()}::${user.publicKey.trim()}"
|
||||
}
|
||||
var groupAdminKeys by remember(user.publicKey, currentUserPublicKey) {
|
||||
mutableStateOf<Set<String>>(emptySet())
|
||||
}
|
||||
var groupMembersCount by remember(user.publicKey, currentUserPublicKey) {
|
||||
mutableStateOf<Int?>(null)
|
||||
var groupMembersCount by remember(groupMembersCacheKey) {
|
||||
mutableStateOf<Int?>(groupMembersCountCache[groupMembersCacheKey])
|
||||
}
|
||||
var mentionCandidates by remember(user.publicKey, currentUserPublicKey) {
|
||||
mutableStateOf<List<MentionCandidate>>(emptyList())
|
||||
@@ -540,14 +552,25 @@ fun ChatDetailScreen(
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
val cachedMembersCount = groupMembersCountCache[groupMembersCacheKey]
|
||||
groupMembersCount = cachedMembersCount
|
||||
|
||||
val members = withContext(Dispatchers.IO) {
|
||||
groupRepository.requestGroupMembers(user.publicKey).orEmpty()
|
||||
groupRepository.requestGroupMembers(user.publicKey)
|
||||
}
|
||||
if (members == null) {
|
||||
groupAdminKeys = emptySet()
|
||||
mentionCandidates = emptyList()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
val normalizedMembers =
|
||||
members.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
groupMembersCount = normalizedMembers.size
|
||||
if (normalizedMembers.isNotEmpty()) {
|
||||
groupMembersCountCache[groupMembersCacheKey] = normalizedMembers.size
|
||||
}
|
||||
|
||||
val adminKey = normalizedMembers.firstOrNull().orEmpty()
|
||||
groupAdminKeys =
|
||||
@@ -1006,9 +1029,13 @@ fun ChatDetailScreen(
|
||||
val isRosettaOfficial = user.title.equals("Rosetta", ignoreCase = true) ||
|
||||
user.username.equals("rosetta", ignoreCase = true) ||
|
||||
isSystemAccount
|
||||
val groupMembersSubtitleCount = groupMembersCount ?: 0
|
||||
val groupMembersSubtitleCount = groupMembersCount
|
||||
val groupMembersSubtitle =
|
||||
"$groupMembersSubtitleCount member${if (groupMembersSubtitleCount == 1) "" else "s"}"
|
||||
if (groupMembersSubtitleCount == null) {
|
||||
""
|
||||
} else {
|
||||
"$groupMembersSubtitleCount member${if (groupMembersSubtitleCount == 1) "" else "s"}"
|
||||
}
|
||||
val chatSubtitle =
|
||||
when {
|
||||
isSavedMessages -> "Notes"
|
||||
@@ -1249,19 +1276,10 @@ fun ChatDetailScreen(
|
||||
extractCopyableMessageText(
|
||||
msg
|
||||
)
|
||||
if (messageText.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
val time =
|
||||
SimpleDateFormat(
|
||||
"HH:mm",
|
||||
Locale.getDefault()
|
||||
)
|
||||
.format(
|
||||
msg.timestamp
|
||||
)
|
||||
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n$messageText"
|
||||
}
|
||||
messageText
|
||||
.takeIf {
|
||||
it.isNotBlank()
|
||||
}
|
||||
}
|
||||
.joinToString(
|
||||
"\n\n"
|
||||
@@ -1582,6 +1600,10 @@ fun ChatDetailScreen(
|
||||
isDarkTheme =
|
||||
isDarkTheme
|
||||
)
|
||||
} else if (isGroupChat &&
|
||||
groupMembersCount == null
|
||||
) {
|
||||
GroupMembersSubtitleSkeleton()
|
||||
} else {
|
||||
Text(
|
||||
text =
|
||||
@@ -3597,6 +3619,53 @@ fun ChatDetailScreen(
|
||||
} // Закрытие outer Box
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupMembersSubtitleSkeleton() {
|
||||
val transition = rememberInfiniteTransition(label = "groupMembersSkeleton")
|
||||
val shimmerProgress by
|
||||
transition.animateFloat(
|
||||
initialValue = -1f,
|
||||
targetValue = 2f,
|
||||
animationSpec =
|
||||
infiniteRepeatable(
|
||||
animation = tween(1200, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "groupMembersSkeletonShimmer"
|
||||
)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier =
|
||||
Modifier.padding(top = 2.dp)
|
||||
.width(76.dp)
|
||||
.height(12.dp)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val widthPx = with(density) { maxWidth.toPx() }.coerceAtLeast(1f)
|
||||
val gradientWidthPx = with(density) { 44.dp.toPx() }
|
||||
val shimmerX = shimmerProgress * widthPx
|
||||
|
||||
val shimmerBrush =
|
||||
Brush.linearGradient(
|
||||
colorStops =
|
||||
arrayOf(
|
||||
0f to Color.White.copy(alpha = 0.22f),
|
||||
0.5f to Color.White.copy(alpha = 0.56f),
|
||||
1f to Color.White.copy(alpha = 0.22f)
|
||||
),
|
||||
start = Offset(shimmerX - gradientWidthPx, 0f),
|
||||
end = Offset(shimmerX, 0f)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(shimmerBrush)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TiledChatWallpaper(
|
||||
wallpaperResId: Int,
|
||||
|
||||
@@ -2171,39 +2171,102 @@ fun ChatsListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (requestsCount > 0) {
|
||||
item(
|
||||
key =
|
||||
"requests_section"
|
||||
item(key = "requests_section") {
|
||||
val isRequestsSectionVisible =
|
||||
requestsCount > 0 &&
|
||||
isRequestsVisible
|
||||
AnimatedVisibility(
|
||||
visible =
|
||||
isRequestsSectionVisible,
|
||||
enter =
|
||||
slideInVertically(
|
||||
initialOffsetY = {
|
||||
fullHeight ->
|
||||
-fullHeight /
|
||||
3
|
||||
},
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis =
|
||||
260,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
expandVertically(
|
||||
expandFrom =
|
||||
Alignment
|
||||
.Top,
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis =
|
||||
260,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
fadeIn(
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis =
|
||||
180
|
||||
),
|
||||
initialAlpha =
|
||||
0.7f
|
||||
),
|
||||
exit =
|
||||
slideOutVertically(
|
||||
targetOffsetY = {
|
||||
fullHeight ->
|
||||
-fullHeight /
|
||||
3
|
||||
},
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis =
|
||||
220,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
shrinkVertically(
|
||||
shrinkTowards =
|
||||
Alignment
|
||||
.Top,
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis =
|
||||
220,
|
||||
easing =
|
||||
FastOutSlowInEasing
|
||||
)
|
||||
) +
|
||||
fadeOut(
|
||||
animationSpec =
|
||||
tween(
|
||||
durationMillis =
|
||||
140
|
||||
)
|
||||
)
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isRequestsVisible,
|
||||
enter = expandVertically(
|
||||
animationSpec = tween(250, easing = FastOutSlowInEasing)
|
||||
) + fadeIn(animationSpec = tween(200)),
|
||||
exit = shrinkVertically(
|
||||
animationSpec = tween(250, easing = FastOutSlowInEasing)
|
||||
) + fadeOut(animationSpec = tween(200))
|
||||
) {
|
||||
Column {
|
||||
RequestsSection(
|
||||
count =
|
||||
requestsCount,
|
||||
requests =
|
||||
requests,
|
||||
isDarkTheme =
|
||||
isDarkTheme,
|
||||
onClick = {
|
||||
openRequestsRouteSafely()
|
||||
}
|
||||
)
|
||||
Divider(
|
||||
color =
|
||||
dividerColor,
|
||||
thickness =
|
||||
0.5.dp
|
||||
)
|
||||
}
|
||||
Column {
|
||||
RequestsSection(
|
||||
count =
|
||||
requestsCount,
|
||||
requests =
|
||||
requests,
|
||||
isDarkTheme =
|
||||
isDarkTheme,
|
||||
onClick = {
|
||||
openRequestsRouteSafely()
|
||||
}
|
||||
)
|
||||
Divider(
|
||||
color =
|
||||
dividerColor,
|
||||
thickness =
|
||||
0.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user