From 698a67a9239b503f4e384c267d1a55492012e9c5 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 16:10:28 +0700 Subject: [PATCH 01/10] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81:=20=D0=BF=D1=83?= =?UTF-8?q?=D1=88-=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BD=D0=B5=20=D1=87=D0=B8=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D1=81=D1=8C=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cancelAll() в onResume — сбрасывает все уведомления и бейдж при открытии приложения - showSimpleNotification принимает senderPublicKey и использует тот же ID что cancelNotificationForChat — теперь fallback-уведомления тоже удаляются при входе в чат Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/rosetta/messenger/MainActivity.kt | 2 ++ .../push/RosettaFirebaseMessagingService.kt | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 07ff4cd..8e39a70 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -423,6 +423,8 @@ class MainActivity : FragmentActivity() { super.onResume() // 🔥 Приложение стало видимым - отключаем уведомления com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = true + // 🔔 Сбрасываем все уведомления из шторки при открытии приложения + (getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager).cancelAll() // ⚡ На возврате в приложение пробуем мгновенный reconnect без ожидания backoff. ProtocolManager.reconnectNowIfNeeded("activity_onResume") } diff --git a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt index ae24d72..18bd950 100644 --- a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt +++ b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt @@ -142,7 +142,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { lower.contains("body") } if (!handledMessageData && !isReadEvent && looksLikeMessagePayload) { - showSimpleNotification(senderName, messagePreview) + showSimpleNotification(senderName, messagePreview, senderPublicKey) handledMessageData = true } } @@ -218,13 +218,13 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { } /** Показать простое уведомление */ - private fun showSimpleNotification(title: String, body: String) { + private fun showSimpleNotification(title: String, body: String, senderPublicKey: String? = null) { // 🔥 Не показываем уведомление если приложение открыто if (isAppInForeground || !areNotificationsEnabled()) { return } // Dedup: suppress duplicate pushes within DEDUP_WINDOW_MS - val dedupKey = "__simple__" + val dedupKey = senderPublicKey?.trim()?.ifEmpty { null } ?: "__simple__" val now = System.currentTimeMillis() val lastTs = lastNotifTimestamps[dedupKey] if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) { @@ -234,8 +234,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { createNotificationChannel() - // Deterministic ID — duplicates replace each other instead of stacking - val notifId = (title + body).hashCode() and 0x7FFFFFFF + // Используем sender-based ID если известен ключ — чтобы cancelNotificationForChat мог убрать уведомление + val notifId = if (!senderPublicKey.isNullOrBlank()) { + getNotificationIdForChat(senderPublicKey.trim()) + } else { + (title + body).hashCode() and 0x7FFFFFFF + } val intent = Intent(this, MainActivity::class.java).apply { From 5e04301539bc75075077b281d0cd4779e3bc5618 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 16:17:08 +0700 Subject: [PATCH 02/10] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D0=B4=D0=BE=20?= =?UTF-8?q?1.2.1=20(versionCode=2023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 202290e..d96c24f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown" // ═══════════════════════════════════════════════════════════ // Rosetta versioning — bump here on each release // ═══════════════════════════════════════════════════════════ -val rosettaVersionName = "1.2.0" -val rosettaVersionCode = 22 // Increment on each release +val rosettaVersionName = "1.2.1" +val rosettaVersionCode = 23 // Increment on each release android { namespace = "com.rosetta.messenger" From 02e95ccd2b719454daeae89cf9498bd364e66bcd Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 17:45:47 +0700 Subject: [PATCH 03/10] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=20shimmer=20skeleton=20=D0=B4=D0=BB=D1=8F=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=D0=BD=D0=BE=D0=B9=20=D0=B8=20=D1=81=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9=20=D1=82=D0=B5=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatsListSkeleton: - Sweep shimmer (градиент слева направо) вместо простого lerp - Время перенесено в правый верхний угол (как в реальных чатах) - Разные ширины имени/превью для естественного вида - Цвета приведены к iOS/Telegram-стилю (2C2C2E/48484A / E5E5EA/FFFFFF) MessageSkeletonList: - Пузыри чередуются left/right (incoming/outgoing) — раньше все были слева - Высоты пузырей 36–68dp вместо 64–128dp (реалистичные размеры) - Outgoing bubble shape с nearRadius снизу-справа - Группы сообщений с одной стороны имитируют реальный чат Co-Authored-By: Claude Sonnet 4.6 --- .../messenger/ui/chats/ChatsListScreen.kt | 126 +++++++++++------- .../chats/components/ChatDetailComponents.kt | 115 ++++++++-------- 2 files changed, 136 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 2f066f6..fa29f26 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -2886,37 +2886,62 @@ private fun DeviceResolveDialog( @Composable private fun ChatsListSkeleton(isDarkTheme: Boolean) { val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) - val shimmerBase = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0) - val shimmerHighlight = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFF0F0F0) + val shimmerBase = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE5E5EA) + val shimmerHighlight = if (isDarkTheme) Color(0xFF48484A) else Color(0xFFFFFFFF) - val transition = rememberInfiniteTransition(label = "shimmer") - val shimmerProgress by - transition.animateFloat( - initialValue = 0f, - targetValue = 1f, - animationSpec = - infiniteRepeatable( - animation = - tween(durationMillis = 1200, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "shimmerAlpha" + BoxWithConstraints(modifier = Modifier.fillMaxSize().background(backgroundColor)) { + val density = LocalDensity.current + val maxWidthPx = with(density) { maxWidth.toPx() }.coerceAtLeast(1f) + val gradientWidthPx = with(density) { 280.dp.toPx() } + + val transition = rememberInfiniteTransition(label = "shimmer") + val shimmerX by transition.animateFloat( + initialValue = -gradientWidthPx, + targetValue = maxWidthPx + gradientWidthPx, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1400, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "shimmerX" ) - val shimmerColor = lerp(shimmerBase, shimmerHighlight, shimmerProgress) - LazyColumn( - modifier = Modifier.fillMaxSize().background(backgroundColor), - userScrollEnabled = false - ) { - items(10) { - SkeletonDialogItem(shimmerColor = shimmerColor, isDarkTheme = isDarkTheme) + val shimmerBrush = Brush.linearGradient( + colorStops = arrayOf( + 0f to shimmerBase, + 0.4f to shimmerHighlight, + 0.6f to shimmerHighlight, + 1f to shimmerBase + ), + start = Offset(shimmerX - gradientWidthPx / 2f, 0f), + end = Offset(shimmerX + gradientWidthPx / 2f, 0f) + ) + + val nameWidths = remember { + floatArrayOf(0.38f, 0.52f, 0.44f, 0.61f, 0.35f, 0.48f, 0.56f, 0.41f, 0.63f, 0.39f) + } + val msgWidths = remember { + floatArrayOf(0.72f, 0.55f, 0.82f, 0.64f, 0.78f, 0.51f, 0.69f, 0.87f, 0.60f, 0.74f) + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + userScrollEnabled = false + ) { + items(10) { index -> + SkeletonDialogItem( + shimmerBrush = shimmerBrush, + nameWidth = nameWidths[index], + messageWidth = msgWidths[index], + isDarkTheme = isDarkTheme + ) + } } } } @Composable -private fun SkeletonDialogItem(shimmerColor: Color, isDarkTheme: Boolean) { - val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) +private fun SkeletonDialogItem(shimmerBrush: Brush, nameWidth: Float, messageWidth: Float, isDarkTheme: Boolean) { + val dividerColor = if (isDarkTheme) Color(0xFF38383A) else Color(0xFFE5E5EA) Row( modifier = @@ -2932,41 +2957,42 @@ private fun SkeletonDialogItem(shimmerColor: Color, isDarkTheme: Boolean) { modifier = Modifier.size(TELEGRAM_DIALOG_AVATAR_SIZE) .clip(CircleShape) - .background(shimmerColor) + .background(shimmerBrush) ) Spacer(modifier = Modifier.width(TELEGRAM_DIALOG_AVATAR_GAP)) - Column(modifier = Modifier.weight(1f)) { - // Name placeholder + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(7.dp)) { + // Name + time row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = + Modifier.fillMaxWidth(nameWidth) + .height(15.dp) + .clip(RoundedCornerShape(6.dp)) + .background(shimmerBrush) + ) + Box( + modifier = + Modifier.width(34.dp) + .height(11.dp) + .clip(RoundedCornerShape(4.dp)) + .background(shimmerBrush) + ) + } + // Message preview placeholder Box( modifier = - Modifier.fillMaxWidth(0.45f) - .height(16.dp) - .clip(RoundedCornerShape(4.dp)) - .background(shimmerColor) - ) - Spacer(modifier = Modifier.height(8.dp)) - // Message placeholder - Box( - modifier = - Modifier.fillMaxWidth(0.7f) - .height(14.dp) - .clip(RoundedCornerShape(4.dp)) - .background(shimmerColor) + Modifier.fillMaxWidth(messageWidth) + .height(13.dp) + .clip(RoundedCornerShape(5.dp)) + .background(shimmerBrush) ) } - - Spacer(modifier = Modifier.width(8.dp)) - - // Time placeholder - Box( - modifier = - Modifier.width(36.dp) - .height(12.dp) - .clip(RoundedCornerShape(4.dp)) - .background(shimmerColor) - ) } Divider( color = dividerColor, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 20ed071..eb12a90 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -2596,19 +2596,40 @@ fun MessageSkeletonList( label = "telegramSkeletonProgress" ) - // Telegram-style deterministic pseudo-randomness. - val widthRandom = remember { floatArrayOf(0.18f, 0.74f, 0.32f, 0.61f, 0.27f, 0.84f, 0.49f, 0.12f) } - val heightRandom = remember { floatArrayOf(0.25f, 0.68f, 0.14f, 0.52f, 0.37f, 0.79f, 0.29f, 0.57f) } + // Deterministic pseudo-random widths (fraction of bubbleMaxWidth) + val widthRandom = remember { + floatArrayOf(0.55f, 0.72f, 0.38f, 0.65f, 0.48f, 0.80f, 0.42f, 0.68f, + 0.35f, 0.75f, 0.58f, 0.44f, 0.70f, 0.52f, 0.82f, 0.40f, + 0.63f, 0.77f, 0.46f, 0.60f, 0.34f, 0.71f, 0.50f, 0.66f) + } + // Realistic message bubble heights in dp (single-line to ~3-line) + val heightRandom = remember { + intArrayOf(44, 52, 40, 60, 36, 56, 44, 48, 64, 40, 52, 44, + 36, 68, 52, 44, 60, 36, 48, 56, 40, 64, 44, 52) + } + // false = incoming (left), true = outgoing (right) + val sidePattern = remember { + booleanArrayOf(false, false, true, true, true, false, true, + false, false, true, true, false, true, true, false, + false, false, true, true, true, false, false, true, true) + } - val bubbleShape = - remember { - RoundedCornerShape( - topStart = TelegramBubbleSpec.bubbleRadius, - topEnd = TelegramBubbleSpec.bubbleRadius, - bottomStart = TelegramBubbleSpec.nearRadius, - bottomEnd = TelegramBubbleSpec.bubbleRadius - ) - } + val incomingShape = remember { + RoundedCornerShape( + topStart = TelegramBubbleSpec.bubbleRadius, + topEnd = TelegramBubbleSpec.bubbleRadius, + bottomStart = TelegramBubbleSpec.nearRadius, + bottomEnd = TelegramBubbleSpec.bubbleRadius + ) + } + val outgoingShape = remember { + RoundedCornerShape( + topStart = TelegramBubbleSpec.bubbleRadius, + topEnd = TelegramBubbleSpec.bubbleRadius, + bottomStart = TelegramBubbleSpec.bubbleRadius, + bottomEnd = TelegramBubbleSpec.nearRadius + ) + } BoxWithConstraints(modifier = modifier.fillMaxSize()) { val density = LocalDensity.current @@ -2616,37 +2637,33 @@ fun MessageSkeletonList( val gradientWidthPx = with(density) { 200.dp.toPx() } val shimmerX = shimmerProgress * maxWidthPx - val color0 = - if (isDarkTheme) Color(0x36FFFFFF) else Color(0x1F000000) - val color1 = - if (isDarkTheme) Color(0x1CFFFFFF) else Color(0x12000000) - val outlineColor = - if (isDarkTheme) Color(0x40FFFFFF) else Color(0x24FFFFFF) + val color0 = if (isDarkTheme) Color(0x36FFFFFF) else Color(0x1F000000) + val color1 = if (isDarkTheme) Color(0x1CFFFFFF) else Color(0x12000000) + val outlineColor = if (isDarkTheme) Color(0x40FFFFFF) else Color(0x24FFFFFF) val shimmerBrush = Brush.linearGradient( - colorStops = - arrayOf( - 0f to color1, - 0.4f to color0, - 0.6f to color0, - 1f to color1 - ), + colorStops = arrayOf( + 0f to color1, + 0.4f to color0, + 0.6f to color0, + 1f to color1 + ), start = Offset(shimmerX - gradientWidthPx, 0f), end = Offset(shimmerX, 0f) ) val bottomInset = 58.dp - val containerWidth = this@BoxWithConstraints.maxWidth - val bubbleMaxWidth = (containerWidth * 0.8f) - if (isGroupChat) 42.dp else 0.dp + val avatarSize = 36.dp val avatarGap = 6.dp + val containerWidth = this@BoxWithConstraints.maxWidth + val bubbleMaxWidth = containerWidth * 0.78f - if (isGroupChat) (avatarSize + avatarGap) else 0.dp val availableHeight = (maxHeight - bottomInset).coerceAtLeast(0.dp) var usedHeight = 0.dp var rowCount = 0 - while (rowCount < 24 && usedHeight < availableHeight) { - val randomHeight = heightRandom[rowCount % heightRandom.size] - usedHeight += (64.dp + (randomHeight * 64f).dp + 3.dp) + while (rowCount < heightRandom.size && usedHeight < availableHeight) { + usedHeight += (heightRandom[rowCount].dp + 4.dp) rowCount++ } if (rowCount < 6) rowCount = 6 @@ -2655,49 +2672,37 @@ fun MessageSkeletonList( modifier = Modifier.align(Alignment.BottomStart) .fillMaxWidth() - .padding(start = 3.dp, end = 8.dp, bottom = bottomInset), - verticalArrangement = Arrangement.spacedBy(3.dp) + .padding(horizontal = 8.dp, bottom = bottomInset), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { repeat(rowCount) { index -> - val lineWidth = - minOf( - bubbleMaxWidth, - 42.dp + - containerWidth * - (0.4f + - widthRandom[index % widthRandom.size] * - 0.35f) - ) - val lineHeight = - 64.dp + - (heightRandom[index % heightRandom.size] * 64f).dp + val isOutgoing = sidePattern[index % sidePattern.size] + val bubbleWidth = bubbleMaxWidth * widthRandom[index % widthRandom.size] + val bubbleHeight = heightRandom[index % heightRandom.size].dp + val shape = if (isOutgoing) outgoingShape else incomingShape Row( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start, verticalAlignment = Alignment.Bottom ) { - if (isGroupChat) { + if (!isOutgoing && isGroupChat) { Box( modifier = - Modifier.size(42.dp) + Modifier.size(avatarSize) .clip(CircleShape) .background(shimmerBrush) .border(1.dp, outlineColor, CircleShape) ) Spacer(modifier = Modifier.width(avatarGap)) } - Box( modifier = - Modifier.width(lineWidth) - .height(lineHeight) - .clip(bubbleShape) + Modifier.width(bubbleWidth) + .height(bubbleHeight) + .clip(shape) .background(shimmerBrush) - .border( - width = 1.dp, - color = outlineColor, - shape = bubbleShape - ) + .border(1.dp, outlineColor, shape) ) } } From c6c0ade1b35894cbf44928f52fdc046319464c6b Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 17:52:05 +0700 Subject: [PATCH 04/10] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86=D0=B8=D0=B8:=20padding(hor?= =?UTF-8?q?izontal,=20bottom)=20=E2=86=92=20padding(start,=20end,=20bottom?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../messenger/ui/chats/components/ChatDetailComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index eb12a90..3d45db4 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -2672,7 +2672,7 @@ fun MessageSkeletonList( modifier = Modifier.align(Alignment.BottomStart) .fillMaxWidth() - .padding(horizontal = 8.dp, bottom = bottomInset), + .padding(start = 8.dp, end = 8.dp, bottom = bottomInset), verticalArrangement = Arrangement.spacedBy(4.dp) ) { repeat(rowCount) { index -> From 200d6987eb94e6a06c5025f4f0ae22322fa0a696 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 18:12:04 +0700 Subject: [PATCH 05/10] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81:=20=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BA=D0=B0=20Install=20Update=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=8F=D0=B3=D0=B8=D0=B2=D0=B0=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=8C=20=D0=BD=D0=B0=20=D0=B2=D0=B5=D1=81=D1=8C=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убран лишний Box-обёртка с contentAlignment вокруг Row — Row теперь сам является кликабельным элементом с фиксированной height(50.dp). Убран Spacer внутри Box который конфликтовал с измерением высоты. Co-Authored-By: Claude Sonnet 4.6 --- .../messenger/ui/chats/ChatsListScreen.kt | 93 +++++++++---------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index fa29f26..3eb848d 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -1289,7 +1289,7 @@ fun ChatsListScreen( } if (showUpdateBanner) { - Box( + Row( modifier = Modifier .fillMaxWidth() .height(50.dp) @@ -1311,60 +1311,53 @@ fun ChatsListScreen( UpdateManager.downloadAndInstall(context) else -> {} } - }, - contentAlignment = Alignment.CenterStart + } + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = when (curUpdate) { - is UpdateState.Downloading -> TablerIcons.X - else -> TablerIcons.Download - }, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(22.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) + Icon( + imageVector = when (curUpdate) { + is UpdateState.Downloading -> TablerIcons.X + else -> TablerIcons.Download + }, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = when (curUpdate) { + is UpdateState.Downloading -> + "Downloading... ${curUpdate.progress}%" + is UpdateState.ReadyToInstall -> + "Install Update" + is UpdateState.UpdateAvailable -> + "Update Rosetta" + else -> "" + }, + color = Color.White, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f) + ) + if (curUpdate is UpdateState.UpdateAvailable) { Text( - text = when (curUpdate) { - is UpdateState.Downloading -> - "Downloading... ${curUpdate.progress}%" - is UpdateState.ReadyToInstall -> - "Install Update" - is UpdateState.UpdateAvailable -> - "Update Rosetta" - else -> "" - }, - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f) + text = curUpdate.version, + color = Color.White.copy(alpha = 0.8f), + fontSize = 13.sp, + fontWeight = FontWeight.Medium + ) + } + if (curUpdate is UpdateState.Downloading) { + CircularProgressIndicator( + progress = curUpdate.progress / 100f, + modifier = Modifier.size(20.dp), + color = Color.White, + trackColor = Color.White.copy(alpha = 0.3f), + strokeWidth = 2.dp ) - if (curUpdate is UpdateState.UpdateAvailable) { - Text( - text = curUpdate.version, - color = Color.White.copy(alpha = 0.8f), - fontSize = 13.sp, - fontWeight = FontWeight.Medium - ) - } - if (curUpdate is UpdateState.Downloading) { - CircularProgressIndicator( - progress = curUpdate.progress / 100f, - modifier = Modifier.size(20.dp), - color = Color.White, - trackColor = Color.White.copy(alpha = 0.3f), - strokeWidth = 2.dp - ) - } } } - Spacer(modifier = Modifier.height(12.dp)) } } } From a3fdc9559b95abc9c63cd50e69c646fd9fa43e21 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 18:14:55 +0700 Subject: [PATCH 06/10] =?UTF-8?q?Revert=20"=D0=A4=D0=B8=D0=BA=D1=81:=20?= =?UTF-8?q?=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20Install=20Update=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D1=82=D1=8F=D0=B3=D0=B8=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=8C=20=D0=BD=D0=B0=20=D0=B2=D0=B5=D1=81=D1=8C=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=80=D0=B0=D0=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 200d6987eb94e6a06c5025f4f0ae22322fa0a696. --- .../messenger/ui/chats/ChatsListScreen.kt | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 3eb848d..fa29f26 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -1289,7 +1289,7 @@ fun ChatsListScreen( } if (showUpdateBanner) { - Row( + Box( modifier = Modifier .fillMaxWidth() .height(50.dp) @@ -1311,53 +1311,60 @@ fun ChatsListScreen( UpdateManager.downloadAndInstall(context) else -> {} } - } - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically + }, + contentAlignment = Alignment.CenterStart ) { - Icon( - imageVector = when (curUpdate) { - is UpdateState.Downloading -> TablerIcons.X - else -> TablerIcons.Download - }, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(22.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = when (curUpdate) { - is UpdateState.Downloading -> - "Downloading... ${curUpdate.progress}%" - is UpdateState.ReadyToInstall -> - "Install Update" - is UpdateState.UpdateAvailable -> - "Update Rosetta" - else -> "" - }, - color = Color.White, - fontSize = 15.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.weight(1f) - ) - if (curUpdate is UpdateState.UpdateAvailable) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = when (curUpdate) { + is UpdateState.Downloading -> TablerIcons.X + else -> TablerIcons.Download + }, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) Text( - text = curUpdate.version, - color = Color.White.copy(alpha = 0.8f), - fontSize = 13.sp, - fontWeight = FontWeight.Medium - ) - } - if (curUpdate is UpdateState.Downloading) { - CircularProgressIndicator( - progress = curUpdate.progress / 100f, - modifier = Modifier.size(20.dp), + text = when (curUpdate) { + is UpdateState.Downloading -> + "Downloading... ${curUpdate.progress}%" + is UpdateState.ReadyToInstall -> + "Install Update" + is UpdateState.UpdateAvailable -> + "Update Rosetta" + else -> "" + }, color = Color.White, - trackColor = Color.White.copy(alpha = 0.3f), - strokeWidth = 2.dp + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f) ) + if (curUpdate is UpdateState.UpdateAvailable) { + Text( + text = curUpdate.version, + color = Color.White.copy(alpha = 0.8f), + fontSize = 13.sp, + fontWeight = FontWeight.Medium + ) + } + if (curUpdate is UpdateState.Downloading) { + CircularProgressIndicator( + progress = curUpdate.progress / 100f, + modifier = Modifier.size(20.dp), + color = Color.White, + trackColor = Color.White.copy(alpha = 0.3f), + strokeWidth = 2.dp + ) + } } } + Spacer(modifier = Modifier.height(12.dp)) } } } From 74d5db3f053dde4c70eb79258b71cb48ea9e5ad9 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 17 Mar 2026 18:20:12 +0700 Subject: [PATCH 07/10] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=87=D0=BD=D0=BE=D0=B9=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rosetta/messenger/update/UpdateManager.kt | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt b/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt index fd3c5e2..e1eb9f5 100644 --- a/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt +++ b/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt @@ -298,6 +298,12 @@ object UpdateManager { } context.startActivity(intent) + // Сбрасываем состояние — установщик запущен, баннер больше не нужен + activeApkPath = null + activeApkVersion = null + persistState(context) + _updateState.value = UpdateState.Idle + _downloadProgress.value = 0 } catch (e: Exception) { Log.e(TAG, "Failed to launch installer", e) _updateState.value = UpdateState.Error("Failed to open installer: ${e.message}") @@ -390,8 +396,19 @@ object UpdateManager { if (!restoredPath.isNullOrBlank()) { val apk = File(restoredPath) if (apk.exists()) { - _downloadProgress.value = 100 - _updateState.value = UpdateState.ReadyToInstall(apk.absolutePath) + // Показываем баннер только если сохранённая версия НОВЕЕ текущей + val isNewer = !restoredVersion.isNullOrBlank() && + compareVersions(restoredVersion, appVersion) > 0 + if (isNewer) { + _downloadProgress.value = 100 + _updateState.value = UpdateState.ReadyToInstall(apk.absolutePath) + } else { + // APK уже устарел (юзер обновился или скачал ту же версию) — удаляем + apk.delete() + activeApkPath = null + activeApkVersion = null + persistState(context) + } } else { activeApkPath = null activeApkVersion = null From 5d8984ab912f723a82ef661ba3a9c736c065c45e Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 18 Mar 2026 20:09:33 +0500 Subject: [PATCH 08/10] =?UTF-8?q?=D0=A1=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D1=87=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=20=D0=B8=20Requests=20=D1=81=20Telegram,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20shimmer=20=D0=B8?= =?UTF-8?q?=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rosetta/messenger/MainActivity.kt | 2 + .../messenger/ui/chats/ChatDetailScreen.kt | 105 ++++++++++++--- .../messenger/ui/chats/ChatsListScreen.kt | 127 +++++++++++++----- .../ui/components/SwipeBackContainer.kt | 87 ++++++++++-- 4 files changed, 260 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 8e39a70..32b7ab0 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -49,6 +49,7 @@ import com.rosetta.messenger.ui.chats.SearchScreen import com.rosetta.messenger.ui.components.OptimizedEmojiCache import com.rosetta.messenger.ui.components.SwipeBackBackgroundEffect import com.rosetta.messenger.ui.components.SwipeBackContainer +import com.rosetta.messenger.ui.components.SwipeBackEnterAnimation import com.rosetta.messenger.ui.crashlogs.CrashLogsScreen import com.rosetta.messenger.ui.onboarding.OnboardingScreen import com.rosetta.messenger.ui.settings.BackupScreen @@ -1029,6 +1030,7 @@ fun MainScreen( isDarkTheme = isDarkTheme, layer = 1, swipeEnabled = !isChatSwipeLocked, + enterAnimation = SwipeBackEnterAnimation.SlideFromRight, propagateBackgroundProgress = false ) { selectedUser?.let { currentChatUser -> diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 53cdcf0..d4afcfe 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -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() + 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>(emptySet()) } - var groupMembersCount by remember(user.publicKey, currentUserPublicKey) { - mutableStateOf(null) + var groupMembersCount by remember(groupMembersCacheKey) { + mutableStateOf(groupMembersCountCache[groupMembersCacheKey]) } var mentionCandidates by remember(user.publicKey, currentUserPublicKey) { mutableStateOf>(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, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index fa29f26..d6e8ec8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -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 + ) } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/SwipeBackContainer.kt b/app/src/main/java/com/rosetta/messenger/ui/components/SwipeBackContainer.kt index cf7c8f7..3c40cfd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/SwipeBackContainer.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/SwipeBackContainer.kt @@ -1,6 +1,7 @@ package com.rosetta.messenger.ui.components import android.content.Context +import android.view.animation.DecelerateInterpolator import android.view.inputmethod.InputMethodManager import androidx.compose.animation.core.* import androidx.compose.foundation.background @@ -19,15 +20,21 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.isActive +import kotlin.coroutines.coroutineContext // Telegram's CubicBezierInterpolator(0.25, 0.1, 0.25, 1.0) private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f) +private val TelegramOpenDecelerateEasing = + Easing { input -> DecelerateInterpolator(1.5f).getInterpolation(input) } // Swipe-back thresholds tuned for ultra-light navigation. private const val COMPLETION_THRESHOLD = 0.05f // 5% of screen width private const val FLING_VELOCITY_THRESHOLD = 35f // px/s private const val ANIMATION_DURATION_ENTER = 300 +private const val ANIMATION_DURATION_ENTER_TELEGRAM_CHAT = 150 private const val ANIMATION_DURATION_EXIT = 200 private const val EDGE_ZONE_DP = 320 private const val EDGE_ZONE_WIDTH_FRACTION = 0.85f @@ -36,6 +43,42 @@ private const val HORIZONTAL_DOMINANCE_RATIO = 1.05f private const val BACKGROUND_MIN_SCALE = 0.97f private const val BACKGROUND_PARALLAX_DP = 4 +enum class SwipeBackEnterAnimation { + Fade, + SlideFromRight +} + +private suspend fun runTelegramChatEnterAnimation( + alphaAnimatable: Animatable, + enterOffsetAnimatable: Animatable, + startOffsetPx: Float +) { + var progress = 0f + var lastFrameTimeMs = withFrameNanos { it } / 1_000_000L + + while (coroutineContext.isActive && progress < 1f) { + val nowMs = withFrameNanos { it } / 1_000_000L + var dtMs = nowMs - lastFrameTimeMs + if (dtMs > 40L && progress == 0f) { + dtMs = 0L + } else if (dtMs > 18L) { + dtMs = 18L + } + lastFrameTimeMs = nowMs + + progress = + (progress + (dtMs / ANIMATION_DURATION_ENTER_TELEGRAM_CHAT.toFloat())) + .coerceIn(0f, 1f) + + val interpolated = TelegramOpenDecelerateEasing.transform(progress).coerceIn(0f, 1f) + alphaAnimatable.snapTo(interpolated) + enterOffsetAnimatable.snapTo(startOffsetPx * (1f - interpolated)) + } + + alphaAnimatable.snapTo(1f) + enterOffsetAnimatable.snapTo(0f) +} + /** * Telegram-style swipe back container (optimized) * @@ -89,6 +132,7 @@ fun SwipeBackContainer( swipeEnabled: Boolean = true, propagateBackgroundProgress: Boolean = true, deferToChildren: Boolean = false, + enterAnimation: SwipeBackEnterAnimation = SwipeBackEnterAnimation.Fade, content: @Composable () -> Unit ) { // 🚀 Lazy composition: skip ALL setup until the screen is opened for the first time. @@ -103,10 +147,12 @@ fun SwipeBackContainer( val edgeZonePx = with(density) { EDGE_ZONE_DP.dp.toPx() } val effectiveEdgeZonePx = maxOf(edgeZonePx, screenWidthPx * EDGE_ZONE_WIDTH_FRACTION) val backgroundParallaxPx = with(density) { BACKGROUND_PARALLAX_DP.dp.toPx() } + val chatEnterOffsetPx = with(density) { 48.dp.toPx() } val containerId = remember { System.identityHashCode(Any()).toLong() } // Animation state for swipe (used only for swipe animations, not during drag) val offsetAnimatable = remember { Animatable(0f) } + val enterOffsetAnimatable = remember { Animatable(0f) } // Alpha animation for fade-in entry val alphaAnimatable = remember { Animatable(0f) } @@ -129,8 +175,15 @@ fun SwipeBackContainer( val view = LocalView.current val focusManager = LocalFocusManager.current - // Current offset: use drag offset during drag, animatable otherwise - val currentOffset = if (isDragging) dragOffset else offsetAnimatable.value + // Current offset: use drag offset during drag, animatable otherwise + optional enter slide + val baseOffset = if (isDragging) dragOffset else offsetAnimatable.value + val enterOffset = + if (isAnimatingIn && enterAnimation == SwipeBackEnterAnimation.SlideFromRight) { + enterOffsetAnimatable.value + } else { + 0f + } + val currentOffset = baseOffset + enterOffset // Current alpha: use animatable during fade animations, otherwise 1 val currentAlpha = if (isAnimatingIn || isAnimatingOut) alphaAnimatable.value else 1f @@ -186,17 +239,29 @@ fun SwipeBackContainer( isAnimatingIn = true try { offsetAnimatable.snapTo(0f) // No slide for entry + dragOffset = 0f alphaAnimatable.snapTo(0f) - alphaAnimatable.animateTo( - targetValue = 1f, - animationSpec = - tween( - durationMillis = ANIMATION_DURATION_ENTER, - easing = FastOutSlowInEasing - ) - ) + if (enterAnimation == SwipeBackEnterAnimation.SlideFromRight) { + enterOffsetAnimatable.snapTo(chatEnterOffsetPx) + runTelegramChatEnterAnimation( + alphaAnimatable = alphaAnimatable, + enterOffsetAnimatable = enterOffsetAnimatable, + startOffsetPx = chatEnterOffsetPx + ) + } else { + enterOffsetAnimatable.snapTo(0f) + alphaAnimatable.animateTo( + targetValue = 1f, + animationSpec = + tween( + durationMillis = ANIMATION_DURATION_ENTER, + easing = FastOutSlowInEasing + ) + ) + } } finally { isAnimatingIn = false + enterOffsetAnimatable.snapTo(0f) } } else if (!isVisible && shouldShow) { // Animate out: fade-out (when triggered by button, not swipe) @@ -231,7 +296,7 @@ fun SwipeBackContainer( } ) { // Scrim (dimming layer behind the screen) - only when swiping - if (currentOffset > 0f) { + if (!isAnimatingIn && currentOffset > 0f) { Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = scrimAlpha))) } From 8b488128656cfee63a4a5fcd604f2684d92283eb Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 18 Mar 2026 20:15:44 +0500 Subject: [PATCH 09/10] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B1=D0=BB=D0=BE=D0=BA=20Add=20Members=20?= =?UTF-8?q?=D0=B8=20Encryption=20Key=20=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messenger/ui/chats/GroupInfoScreen.kt | 103 ++++++++++-------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt index 6597dd2..dbd4784 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt @@ -44,6 +44,9 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll +import compose.icons.TablerIcons +import compose.icons.tablericons.Code +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack @@ -1158,75 +1161,81 @@ fun GroupInfoScreen( } } + Spacer(modifier = Modifier.height(8.dp)) Surface( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), color = sectionColor, - shape = RoundedCornerShape(0.dp) + shape = RoundedCornerShape(12.dp) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { openInviteSharePicker() } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.PersonAdd, - contentDescription = null, - tint = accentColor - ) - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "Add Members", - color = accentColor, - fontSize = 17.sp, - fontWeight = FontWeight.Medium - ) - } - } - - Surface( - modifier = Modifier.fillMaxWidth(), - color = sectionColor, - shape = RoundedCornerShape(0.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { openEncryptionKey() } - .padding(horizontal = 16.dp, vertical = 11.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Column { + // Add Members + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { openInviteSharePicker() } + .padding(horizontal = 16.dp, vertical = 13.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Add Members", + color = primaryText, + fontSize = 16.sp, + modifier = Modifier.weight(1f) + ) Icon( - imageVector = Icons.Default.Lock, + imageVector = Icons.Default.PersonAdd, contentDescription = null, tint = accentColor, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(22.dp) ) - Spacer(modifier = Modifier.width(12.dp)) + } + + Divider( + color = borderColor, + thickness = 0.5.dp, + modifier = Modifier.padding(start = 16.dp) + ) + + // Encryption Key + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { openEncryptionKey() } + .padding(horizontal = 16.dp, vertical = 13.dp), + verticalAlignment = Alignment.CenterVertically + ) { Text( text = "Encryption Key", color = primaryText, fontSize = 16.sp, - fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f) ) if (encryptionKeyLoading) { CircularProgressIndicator( - modifier = Modifier.size(16.dp), + modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = accentColor ) + } else { + val identiconKey = encryptionKey.ifBlank { dialogPublicKey } + Box( + modifier = Modifier + .size(34.dp) + .clip(RoundedCornerShape(6.dp)) + ) { + TelegramStyleIdenticon( + keyRender = identiconKey, + size = 34.dp, + isDarkTheme = isDarkTheme + ) + } } } - Spacer(modifier = Modifier.height(3.dp)) - Text( - text = "Tap to view key used for secure group communication", - color = secondaryText, - fontSize = 12.sp - ) } } + Spacer(modifier = Modifier.height(8.dp)) TabRow( selectedTabIndex = GroupInfoTab.entries.indexOf(selectedTab), From fbae1283caaa0d9887ccc09cbfa6192548a5c8f3 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 18 Mar 2026 20:46:43 +0500 Subject: [PATCH 10/10] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=81=D1=82=D0=B8=D0=BB=D1=8C=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8E=20=D0=B2=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B0?= =?UTF-8?q?=D1=85=20=D0=BA=20=D0=B5=D0=B4=D0=B8=D0=BD=D0=BE=D0=BC=D1=83=20?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BB=D1=8E=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messenger/ui/chats/GroupInfoScreen.kt | 148 ++++++++++++------ 1 file changed, 100 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt index dbd4784..ffa455a 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt @@ -44,8 +44,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll -import compose.icons.TablerIcons -import compose.icons.tablericons.Code import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -61,10 +59,10 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Tab @@ -93,6 +91,7 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager @@ -110,6 +109,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.PopupProperties import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants @@ -1010,52 +1010,71 @@ fun GroupInfoScreen( tint = Color.White ) } - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } + val menuBgColor = if (isDarkTheme) Color(0xFF272829) else Color.White + val menuTextColor = if (isDarkTheme) Color.White else Color(0xFF222222) + val menuIconColor = if (isDarkTheme) Color.White.copy(alpha = 0.47f) else Color(0xFF676B70) + val menuDividerColor = if (isDarkTheme) Color(0xFF1C1D1F) else Color(0xFFF5F5F5) + val menuDangerColor = Color(0xFFFF3B30) + + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy( + surface = menuBgColor, + onSurface = menuTextColor + ) ) { - DropdownMenuItem( - text = { Text("Search members") }, - leadingIcon = { - Icon(Icons.Default.Search, contentDescription = null) - }, - onClick = { - showMenu = false - selectedTab = GroupInfoTab.MEMBERS - showSearch = true - } - ) - DropdownMenuItem( - text = { Text("Copy invite") }, - leadingIcon = { - Icon(Icons.Default.PersonAdd, contentDescription = null) - }, - onClick = { - showMenu = false - copyInvite() - } - ) - DropdownMenuItem( - text = { Text("Encryption key") }, - leadingIcon = { - Icon(Icons.Default.Lock, contentDescription = null) - }, - onClick = { - showMenu = false - openEncryptionKey() - } - ) - Divider() - DropdownMenuItem( - text = { Text("Leave group", color = Color(0xFFFF3B30)) }, - leadingIcon = { - Icon(Icons.Default.ExitToApp, contentDescription = null, tint = Color(0xFFFF3B30)) - }, - onClick = { - showMenu = false - showLeaveConfirm = true - } - ) + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + modifier = Modifier.widthIn(min = 196.dp).background(menuBgColor), + properties = PopupProperties( + focusable = true, + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + GroupInfoMenuItem( + icon = Icons.Default.Search, + text = "Search members", + tintColor = menuIconColor, + textColor = menuTextColor, + onClick = { + showMenu = false + selectedTab = GroupInfoTab.MEMBERS + showSearch = true + } + ) + GroupInfoMenuItem( + icon = Icons.Default.PersonAdd, + text = "Copy invite", + tintColor = menuIconColor, + textColor = menuTextColor, + onClick = { + showMenu = false + copyInvite() + } + ) + GroupInfoMenuItem( + icon = Icons.Default.Lock, + text = "Encryption key", + tintColor = menuIconColor, + textColor = menuTextColor, + onClick = { + showMenu = false + openEncryptionKey() + } + ) + Divider(color = menuDividerColor) + GroupInfoMenuItem( + icon = Icons.Default.ExitToApp, + text = "Leave group", + tintColor = menuDangerColor, + textColor = menuDangerColor, + onClick = { + showMenu = false + showLeaveConfirm = true + } + ) + } } } @@ -1821,6 +1840,39 @@ private fun GroupEncryptionKeyPage( } } +@Composable +private fun GroupInfoMenuItem( + icon: ImageVector, + text: String, + tintColor: Color, + textColor: Color, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clickable(onClick = onClick) + .padding(horizontal = 18.dp), + contentAlignment = Alignment.CenterStart + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = icon, + contentDescription = null, + tint = tintColor, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(19.dp)) + Text( + text = text, + color = textColor, + fontSize = 16.sp + ) + } + } +} + @Composable private fun GroupActionButton( modifier: Modifier = Modifier,