Релиз 1.2.4: фиксы чатов, медиа и release notes
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user