Релиз 1.2.4: фиксы чатов, медиа и release notes

This commit is contained in:
2026-03-20 00:44:18 +05:00
parent 5ecb2a8db4
commit 004b54ec7c
7 changed files with 197 additions and 68 deletions

View File

@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
// ═══════════════════════════════════════════════════════════
// Rosetta versioning — bump here on each release
// ═══════════════════════════════════════════════════════════
val rosettaVersionName = "1.2.3"
val rosettaVersionCode = 25 // Increment on each release
val rosettaVersionName = "1.2.4"
val rosettaVersionCode = 26 // Increment on each release
android {
namespace = "com.rosetta.messenger"

View File

@@ -19,14 +19,31 @@ object ReleaseNotes {
Что обновлено после версии 1.2.3
Группы и медиа
- Исправлено отображение групповых баблов и стеков сообщений
- Исправлено позиционирование аватарки: имя и аватар в группе теперь не разъезжаются
- Исправлена обрезка имени отправителя в медиа-баблах
- Исправлено растяжение фото в forwarded/media-пузырях
Чат-лист и Requests
- Полностью переработано поведение блока Requests: pull-жест, раскрытие и скрытие как у архива в Telegram
- Доработана вытягивающаяся анимация: requests сразу появляются первым элементом при pull вниз
- Убраны рывки и прыжки списка чатов при анимациях и при пустом списке запросов
Интерфейс
- Убрана лишняя рамка вокруг аватарки в боковом меню
Чаты и группы
- Исправлены групповые баблы и аватарки в стеках сообщений, устранены кривые состояния в медиа-блоках
- Исправлена обрезка имени отправителя в групповых медиа-сообщениях
- Плашки даты в диалоге приведены к Telegram-стилю, добавлена плавающая верхняя дата при скролле
- Сообщение «you joined the group» теперь белого цвета в тёмной теме и на обоях
Медиа и локальные данные
- Исправлена отправка нескольких фото: добавлен корректный optimistic UI и стабильное отображение до/после перезахода
- Экран редактирования фото после камеры унифицирован с редактором фото из галереи
- Удалённые сообщения теперь корректно удаляются локально и не возвращаются после открытия диалога
Обои и темы
- Разделены наборы обоев для светлой и тёмной темы
- Исправлено поведение обоев на разных разрешениях: убраны повторения/растяжения, фон отображается стабильнее
Навигация и UI
- Back-свайп теперь везде скрывает клавиатуру (как на экране поиска)
- На экране группы выровнены размеры иконок Encryption Key и Add Members
- Улучшен back-свайп на экране Encryption Key: возврат во внутреннюю страницу группы
- Приведён к нормальному размер индикатор ошибки в чат-листе
""".trimIndent()
fun getNotice(version: String): String =

View File

@@ -94,7 +94,6 @@ import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.attach.ChatAttachAlert
import com.rosetta.messenger.ui.chats.components.*
import com.rosetta.messenger.ui.chats.components.ImageEditorScreen
import com.rosetta.messenger.ui.chats.components.InAppCameraScreen
import com.rosetta.messenger.ui.chats.components.MultiImageEditorScreen
import com.rosetta.messenger.ui.chats.input.*
@@ -234,6 +233,8 @@ fun ChatDetailScreen(
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
val dateHeaderTextColor = if (isDarkTheme || hasChatWallpaper) Color.White else secondaryTextColor
val dateHeaderBackgroundColor =
if (isDarkTheme || hasChatWallpaper) Color(0x80212121) else Color(0xCCE8E8ED)
val headerIconColor = Color.White
// 🔥 Keyboard & Emoji Coordinator
@@ -391,6 +392,7 @@ fun ChatDetailScreen(
var pendingCameraPhotoUri by remember {
mutableStateOf<Uri?>(null)
} // Фото для редактирования
var pendingCameraPhotoCaption by remember { mutableStateOf("") }
// 📷 Показать встроенную камеру (без системного превью)
var showInAppCamera by remember { mutableStateOf(false) }
@@ -1156,6 +1158,37 @@ fun ChatDetailScreen(
val showScrollToBottomButton by remember(messagesWithDates, isAtBottom, isSendingMessage) {
derivedStateOf { messagesWithDates.isNotEmpty() && !isAtBottom && !isSendingMessage }
}
val floatingDateText by remember(messagesWithDates, listState) {
derivedStateOf {
if (messagesWithDates.isEmpty()) {
return@derivedStateOf null
}
val layoutInfo = listState.layoutInfo
val visibleItems = layoutInfo.visibleItemsInfo
if (visibleItems.isEmpty()) {
return@derivedStateOf null
}
val topVisibleItem =
visibleItems.minByOrNull { itemInfo ->
kotlin.math.abs(itemInfo.offset - layoutInfo.viewportStartOffset)
} ?: return@derivedStateOf null
val messageIndex = topVisibleItem.index
if (messageIndex !in messagesWithDates.indices) {
return@derivedStateOf null
}
getDateText(messagesWithDates[messageIndex].first.timestamp.time)
}
}
val showFloatingDateHeader by
remember(messagesWithDates, floatingDateText, isAtBottom, listState) {
derivedStateOf {
messagesWithDates.isNotEmpty() &&
floatingDateText != null &&
!isAtBottom &&
(listState.isScrollInProgress ||
listState.firstVisibleItemIndex > 0)
}
}
// Telegram-style: Прокрутка ТОЛЬКО при новых сообщениях (не при пагинации)
// 🔥 Скроллим только если изменился ID самого нового сообщения
@@ -2555,12 +2588,41 @@ fun ChatDetailScreen(
isMessageBoundary(message, prevMessage)
val isGroupStart =
isMessageBoundary(message, nextMessage)
val runHeadIndex =
messageRunNewestIndex.getOrNull(
index
) ?: index
val runTailIndex =
messageRunOldestIndexByHead
.getOrNull(
runHeadIndex
)
?: runHeadIndex
val isHeadPhase =
incomingRunAvatarUiState
.showOnRunHeads
.contains(
runHeadIndex
)
val isTailPhase =
incomingRunAvatarUiState
.showOnRunTails
.contains(
runHeadIndex
)
val showIncomingGroupAvatar =
isGroupChat &&
!message.isOutgoing &&
senderPublicKeyForMessage
.isNotBlank() &&
isGroupStart
((index ==
runHeadIndex &&
isHeadPhase &&
showTail) ||
(index ==
runTailIndex &&
isTailPhase &&
isGroupStart))
Column {
if (showDate
@@ -2571,8 +2633,10 @@ fun ChatDetailScreen(
message.timestamp
.time
),
secondaryTextColor =
dateHeaderTextColor
textColor =
dateHeaderTextColor,
backgroundColor =
dateHeaderBackgroundColor
)
}
val selectionKey =
@@ -2943,6 +3007,42 @@ fun ChatDetailScreen(
}
}
}
androidx.compose.animation.AnimatedVisibility(
visible =
showFloatingDateHeader &&
!isLoading &&
!isSelectionMode,
enter =
fadeIn(animationSpec = tween(120)) +
slideInVertically(
animationSpec =
tween(120)
) { height ->
-height / 2
},
exit =
fadeOut(animationSpec = tween(100)) +
slideOutVertically(
animationSpec =
tween(100)
) { height ->
-height / 2
},
modifier =
Modifier.align(Alignment.TopCenter)
.padding(top = 8.dp)
.zIndex(3f)
) {
floatingDateText?.let { dateText ->
DateHeader(
dateText = dateText,
textColor = dateHeaderTextColor,
backgroundColor =
dateHeaderBackgroundColor,
verticalPadding = 0.dp
)
}
}
if (incomingRunAvatarUiState.overlays.isNotEmpty()) {
val avatarInsetPx =
with(density) {
@@ -3560,7 +3660,8 @@ fun ChatDetailScreen(
InAppCameraScreen(
onDismiss = { showInAppCamera = false },
onPhotoTaken = { photoUri ->
// Сначала редактор (skipEnterAnimation=1f), потом убираем камеру
// После камеры открываем тот же fullscreen-редактор,
// что и для фото из галереи.
pendingCameraPhotoUri = photoUri
showInAppCamera = false
}
@@ -3569,26 +3670,25 @@ fun ChatDetailScreen(
// 📷 Image Editor для фото с камеры (с caption как в Telegram)
pendingCameraPhotoUri?.let { uri ->
ImageEditorScreen(
SimpleFullscreenPhotoOverlay(
imageUri = uri,
onDismiss = {
pendingCameraPhotoUri = null
inputFocusTrigger++
},
onSave = { editedUri ->
// 🚀 Мгновенный optimistic UI - фото появляется СРАЗУ
viewModel.sendImageFromUri(editedUri, "")
showMediaPicker = false
},
onSaveWithCaption = { editedUri, caption ->
// 🚀 Мгновенный optimistic UI - фото появляется СРАЗУ
modifier = Modifier.fillMaxSize().zIndex(100f),
showCaptionInput = true,
caption = pendingCameraPhotoCaption,
onCaptionChange = { pendingCameraPhotoCaption = it },
isDarkTheme = isDarkTheme,
onSend = { editedUri, caption ->
viewModel.sendImageFromUri(editedUri, caption)
showMediaPicker = false
pendingCameraPhotoUri = null
pendingCameraPhotoCaption = ""
inputFocusTrigger++
},
isDarkTheme = isDarkTheme,
showCaptionInput = true,
recipientName = user.title,
skipEnterAnimation = true // Из камеры — мгновенно, без fade
onDismiss = {
pendingCameraPhotoUri = null
pendingCameraPhotoCaption = ""
inputFocusTrigger++
}
)
}

View File

@@ -2169,15 +2169,27 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
/** 🔥 Удалить сообщение (для ошибки отправки) */
fun deleteMessage(messageId: String) {
val account = myPublicKey ?: return
val opponent = opponentKey ?: return
val dialogKey = getDialogKey(account, opponent)
// Удаляем из UI сразу на main
_messages.value = _messages.value.filter { it.id != messageId }
val updatedMessages = _messages.value.filter { it.id != messageId }
_messages.value = updatedMessages
// Синхронизируем глобальный кэш диалога, иначе удалённые сообщения могут вернуться
// при повторном открытии чата из stale cache.
updateCacheWithLimit(account, dialogKey, updatedMessages)
messageRepository.clearDialogCache(opponent)
// Удаляем из БД в IO + удаляем pin если был
viewModelScope.launch(Dispatchers.IO) {
val account = myPublicKey ?: return@launch
val dialogKey = opponentKey ?: return@launch
pinnedMessageDao.removePin(account, dialogKey, messageId)
messageDao.deleteMessage(account, messageId)
if (account == opponent) {
dialogDao.updateSavedMessagesDialogFromMessages(account)
} else {
dialogDao.updateDialogFromMessages(account, opponent)
}
}
}

View File

@@ -4462,17 +4462,17 @@ fun DialogItemContent(
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier =
Modifier.size(22.dp)
Modifier.size(16.dp)
.clip(CircleShape)
.background(Color(0xFFE53935)),
contentAlignment = Alignment.Center
) {
Text(
text = "!",
fontSize = 13.sp,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
lineHeight = 13.sp,
lineHeight = 10.sp,
maxLines = 1
)
}

View File

@@ -137,6 +137,7 @@ 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.SwipeBackContainer
import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.icons.TelegramIcons
import com.rosetta.messenger.ui.settings.FullScreenAvatarViewer
@@ -339,6 +340,7 @@ fun GroupInfoScreen(
val accentColor = if (isDarkTheme) Color(0xFF5AA5FF) else Color(0xFF228BE6)
val actionContentColor = if (isDarkTheme) Color.White else Color(0xFF1C1C1E)
val groupActionButtonBlue = if (isDarkTheme) Color(0xFF4A4A4D) else Color(0xFF2478C2)
val groupMenuTrailingIconSize = 22.dp
LaunchedEffect(Unit) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
@@ -805,8 +807,8 @@ fun GroupInfoScreen(
swipedMemberKey = null
}
}
LaunchedEffect(swipedMemberKey) {
onSwipeBackEnabledChanged(swipedMemberKey == null)
LaunchedEffect(swipedMemberKey, showEncryptionPage) {
onSwipeBackEnabledChanged(swipedMemberKey == null && !showEncryptionPage)
}
DisposableEffect(Unit) {
onDispose {
@@ -1207,7 +1209,7 @@ fun GroupInfoScreen(
imageVector = Icons.Default.PersonAdd,
contentDescription = null,
tint = accentColor,
modifier = Modifier.size(22.dp)
modifier = Modifier.size(groupMenuTrailingIconSize)
)
}
@@ -1233,7 +1235,7 @@ fun GroupInfoScreen(
)
if (encryptionKeyLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
modifier = Modifier.size(groupMenuTrailingIconSize),
strokeWidth = 2.dp,
color = accentColor
)
@@ -1241,12 +1243,12 @@ fun GroupInfoScreen(
val identiconKey = encryptionKey.ifBlank { dialogPublicKey }
Box(
modifier = Modifier
.size(34.dp)
.clip(RoundedCornerShape(6.dp))
.size(groupMenuTrailingIconSize)
.clip(RoundedCornerShape(3.dp))
) {
TelegramStyleIdenticon(
keyRender = identiconKey,
size = 34.dp,
size = groupMenuTrailingIconSize,
isDarkTheme = isDarkTheme
)
}
@@ -1565,11 +1567,12 @@ fun GroupInfoScreen(
)
}
AnimatedVisibility(
visible = showEncryptionPage,
enter = fadeIn(animationSpec = tween(durationMillis = 260)),
exit = fadeOut(animationSpec = tween(durationMillis = 200)),
modifier = Modifier.fillMaxSize()
SwipeBackContainer(
isVisible = showEncryptionPage,
onBack = { showEncryptionPage = false },
isDarkTheme = isDarkTheme,
layer = 3,
propagateBackgroundProgress = false
) {
val displayLines = remember(encryptionKey) { encodeGroupKeyForDisplay(encryptionKey) }
GroupEncryptionKeyPage(

View File

@@ -59,6 +59,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.PopupProperties
@@ -191,37 +192,33 @@ fun TelegramStyleMessageContent(
private data class LayoutResult(val width: Int, val height: Int, val timeX: Int, val timeY: Int)
/** Date header with fade-in animation */
/** Telegram-like date header chip (inline separator and floating top badge). */
@Composable
fun DateHeader(dateText: String, secondaryTextColor: Color) {
var isVisible by remember { mutableStateOf(false) }
val alpha by
animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
label = "dateAlpha"
)
LaunchedEffect(dateText) { isVisible = true }
fun DateHeader(
dateText: String,
textColor: Color,
backgroundColor: Color,
modifier: Modifier = Modifier,
verticalPadding: Dp = 12.dp
) {
Row(
modifier =
Modifier.fillMaxWidth().padding(vertical = 12.dp).graphicsLayer {
this.alpha = alpha
},
modifier
.fillMaxWidth()
.padding(vertical = verticalPadding),
horizontalArrangement = Arrangement.Center
) {
Text(
text = dateText,
fontSize = 13.sp,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor,
color = textColor,
modifier =
Modifier.background(
color = secondaryTextColor.copy(alpha = 0.1f),
shape = RoundedCornerShape(12.dp)
color = backgroundColor,
shape = RoundedCornerShape(10.dp)
)
.padding(horizontal = 12.dp, vertical = 4.dp)
.padding(horizontal = 10.dp, vertical = 3.dp)
)
}
}