feat: enhance versioning and avatar handling with dynamic properties and improved UI interactions
This commit is contained in:
@@ -1389,7 +1389,21 @@ fun ChatDetailScreen(
|
||||
else
|
||||
user.publicKey,
|
||||
originalChatPublicKey =
|
||||
user.publicKey
|
||||
user.publicKey,
|
||||
attachments =
|
||||
msg.attachments
|
||||
.filter {
|
||||
it.type !=
|
||||
AttachmentType
|
||||
.MESSAGES
|
||||
}
|
||||
.map {
|
||||
attachment ->
|
||||
attachment.copy(
|
||||
localUri =
|
||||
""
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
ForwardManager
|
||||
@@ -1797,6 +1811,8 @@ fun ChatDetailScreen(
|
||||
message,
|
||||
isDarkTheme =
|
||||
isDarkTheme,
|
||||
isSelectionMode =
|
||||
isSelectionMode,
|
||||
showTail =
|
||||
showTail,
|
||||
isGroupStart =
|
||||
|
||||
@@ -500,7 +500,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
// 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния!
|
||||
// Это важно для правильного отображения forward сообщений сразу
|
||||
val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey)
|
||||
val forwardMessages = ForwardManager.consumeForwardMessagesForChat(publicKey)
|
||||
val hasForward = forwardMessages.isNotEmpty()
|
||||
if (hasForward) {}
|
||||
|
||||
@@ -543,12 +543,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
text = fm.text,
|
||||
timestamp = fm.timestamp,
|
||||
isOutgoing = fm.isOutgoing,
|
||||
publicKey = fm.senderPublicKey
|
||||
publicKey = fm.senderPublicKey,
|
||||
attachments = fm.attachments
|
||||
)
|
||||
}
|
||||
_isForwardMode.value = true
|
||||
// Очищаем ForwardManager после применения
|
||||
ForwardManager.clear()
|
||||
} else {
|
||||
// Сбрасываем forward state если нет forward сообщений
|
||||
_replyMessages.value = emptyList()
|
||||
@@ -1261,6 +1260,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val attType = AttachmentType.fromInt(attJson.optInt("type", 0))
|
||||
val attPreview = attJson.optString("preview", "")
|
||||
val attBlob = attJson.optString("blob", "")
|
||||
val attWidth = attJson.optInt("width", 0)
|
||||
val attHeight = attJson.optInt("height", 0)
|
||||
|
||||
if (attId.isNotEmpty()) {
|
||||
replyAttachmentsFromJson.add(
|
||||
@@ -1268,7 +1269,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
id = attId,
|
||||
type = attType,
|
||||
preview = attPreview,
|
||||
blob = attBlob
|
||||
blob = attBlob,
|
||||
width = attWidth,
|
||||
height = attHeight
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1412,7 +1415,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
text = msg.text,
|
||||
timestamp = msg.timestamp.time,
|
||||
isOutgoing = msg.isOutgoing,
|
||||
publicKey = if (msg.isOutgoing) sender else opponent
|
||||
publicKey = if (msg.isOutgoing) sender else opponent,
|
||||
attachments =
|
||||
msg.attachments
|
||||
.filter { it.type != AttachmentType.MESSAGES }
|
||||
.map { it.copy(localUri = "") }
|
||||
)
|
||||
}
|
||||
_isForwardMode.value = true
|
||||
@@ -1572,6 +1579,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
put("id", att.id)
|
||||
put("type", att.type.value)
|
||||
put("preview", att.preview)
|
||||
put("width", att.width)
|
||||
put("height", att.height)
|
||||
// Для IMAGE/FILE - blob не включаем (слишком большой)
|
||||
// Для MESSAGES - включаем blob
|
||||
put(
|
||||
|
||||
@@ -164,6 +164,7 @@ fun ChatsListScreen(
|
||||
onSettingsClick: () -> Unit,
|
||||
onInviteFriendsClick: () -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
onRequestsClick: () -> Unit = {},
|
||||
onNewChat: () -> Unit,
|
||||
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
|
||||
backgroundBlurColorId: String = "avatar",
|
||||
@@ -429,7 +430,7 @@ fun ChatsListScreen(
|
||||
if (showRequestsScreen) return@pointerInput
|
||||
|
||||
val velocityTracker = VelocityTracker()
|
||||
val relaxedTouchSlop = viewConfiguration.touchSlop * 0.45f
|
||||
val relaxedTouchSlop = viewConfiguration.touchSlop * 0.8f
|
||||
|
||||
awaitEachGesture {
|
||||
val down =
|
||||
@@ -1062,10 +1063,31 @@ fun ChatsListScreen(
|
||||
AnimatedContent(
|
||||
targetState = showRequestsScreen,
|
||||
transitionSpec = {
|
||||
fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
fadeOut(animationSpec = tween(150))
|
||||
if (targetState) {
|
||||
// Opening requests: slide in from right
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> fullWidth } + fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> -fullWidth / 4 } + fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
} else {
|
||||
// Closing requests: slide out to right
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> -fullWidth / 4 } + fadeIn(
|
||||
animationSpec = tween(200)
|
||||
) togetherWith
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(280, easing = FastOutSlowInEasing)
|
||||
) { fullWidth -> fullWidth } + fadeOut(
|
||||
animationSpec = tween(150)
|
||||
)
|
||||
}
|
||||
},
|
||||
label = "RequestsTransition"
|
||||
) { isRequestsScreen ->
|
||||
@@ -1163,11 +1185,12 @@ fun ChatsListScreen(
|
||||
RequestsSection(
|
||||
count =
|
||||
requestsCount,
|
||||
requests =
|
||||
requests,
|
||||
isDarkTheme =
|
||||
isDarkTheme,
|
||||
onClick = {
|
||||
showRequestsScreen =
|
||||
true
|
||||
onRequestsClick()
|
||||
}
|
||||
)
|
||||
Divider(
|
||||
@@ -2015,8 +2038,14 @@ fun SwipeableDialogItem(
|
||||
}
|
||||
?: break
|
||||
if (change.changedToUpIgnoreConsumed()
|
||||
)
|
||||
) {
|
||||
// Tap detected — finger went up before touchSlop
|
||||
if (!passedSlop) {
|
||||
change.consume()
|
||||
onClick()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
val delta = change.positionChange()
|
||||
totalDragX += delta.x
|
||||
@@ -2155,7 +2184,7 @@ fun SwipeableDialogItem(
|
||||
isPinned = isPinned,
|
||||
isBlocked = isBlocked,
|
||||
avatarRepository = avatarRepository,
|
||||
onClick = onClick
|
||||
onClick = null // Tap handled by parent pointerInput
|
||||
)
|
||||
|
||||
// Сепаратор внутри контента
|
||||
@@ -2178,7 +2207,7 @@ fun DialogItemContent(
|
||||
isPinned: Boolean = false,
|
||||
isBlocked: Boolean = false,
|
||||
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
||||
onClick: () -> Unit
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
|
||||
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||
@@ -2257,7 +2286,10 @@ fun DialogItemContent(
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.then(
|
||||
if (onClick != null) Modifier.clickable(onClick = onClick)
|
||||
else Modifier
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -2656,33 +2688,141 @@ fun TypingIndicatorSmall() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 📬 Секция Requests - кнопка для перехода к списку запросов */
|
||||
/** 📬 Секция Requests — Telegram-style chat item (как Archived Chats) */
|
||||
@Composable
|
||||
fun RequestsSection(count: Int, isDarkTheme: Boolean, onClick: () -> Unit) {
|
||||
val textColor = if (isDarkTheme) Color(0xFF4DABF7) else Color(0xFF228BE6)
|
||||
val arrowColor = if (isDarkTheme) Color(0xFFC9C9C9) else Color(0xFF228BE6)
|
||||
fun RequestsSection(
|
||||
count: Int,
|
||||
requests: List<DialogUiModel> = emptyList(),
|
||||
isDarkTheme: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||
val secondaryTextColor =
|
||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) }
|
||||
val iconBgColor =
|
||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFC7C7CC) }
|
||||
|
||||
// Последний запрос — показываем имя отправителя как subtitle
|
||||
val lastRequest = remember(requests) { requests.firstOrNull() }
|
||||
val subtitle = remember(lastRequest) {
|
||||
when {
|
||||
lastRequest == null -> ""
|
||||
lastRequest.opponentTitle.isNotEmpty() &&
|
||||
lastRequest.opponentTitle != lastRequest.opponentKey &&
|
||||
lastRequest.opponentTitle != lastRequest.opponentKey.take(7) ->
|
||||
lastRequest.opponentTitle
|
||||
lastRequest.opponentUsername.isNotEmpty() ->
|
||||
"@${lastRequest.opponentUsername}"
|
||||
else -> lastRequest.opponentKey.take(7)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Requests +$count",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor
|
||||
)
|
||||
// Иконка — круглый аватар как в Telegram Archived Chats
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(iconBgColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.MailForward,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(26.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = TablerIcons.ChevronRight,
|
||||
contentDescription = "Open requests",
|
||||
tint = arrowColor.copy(alpha = 0.6f),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// Текст: название + последний отправитель
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Requests",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
if (subtitle.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 14.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Badge с количеством
|
||||
if (count > 0) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
||||
.clip(RoundedCornerShape(11.dp))
|
||||
.background(iconBgColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (count > 99) "99+" else count.toString(),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (count > 0) {
|
||||
// Если нет subtitle но есть count
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 22.dp, minHeight = 22.dp)
|
||||
.clip(RoundedCornerShape(11.dp))
|
||||
.background(iconBgColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (count > 99) "99+" else count.toString(),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.rosetta.messenger.ui.chats
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.network.SearchUser
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.ArrowLeft
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RequestsListScreen(
|
||||
isDarkTheme: Boolean,
|
||||
chatsViewModel: ChatsListViewModel,
|
||||
onBack: () -> Unit,
|
||||
onUserSelect: (SearchUser) -> Unit
|
||||
) {
|
||||
val chatsState by chatsViewModel.chatsState.collectAsState()
|
||||
val requests = chatsState.requests
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = TablerIcons.ArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = PrimaryBlue
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = "Requests",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
color = textColor
|
||||
)
|
||||
},
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = backgroundColor,
|
||||
scrolledContainerColor = backgroundColor,
|
||||
navigationIconContentColor = textColor,
|
||||
titleContentColor = textColor
|
||||
)
|
||||
)
|
||||
},
|
||||
containerColor = backgroundColor
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
RequestsScreen(
|
||||
requests = requests,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = onBack,
|
||||
onRequestClick = { request ->
|
||||
onUserSelect(chatsViewModel.dialogToSearchUser(request))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,7 @@ fun TypingIndicator(isDarkTheme: Boolean) {
|
||||
fun MessageBubble(
|
||||
message: ChatMessage,
|
||||
isDarkTheme: Boolean,
|
||||
isSelectionMode: Boolean = false,
|
||||
showTail: Boolean = true,
|
||||
isGroupStart: Boolean = false,
|
||||
isSelected: Boolean = false,
|
||||
@@ -307,6 +308,8 @@ fun MessageBubble(
|
||||
if (message.isOutgoing) Color(0xFFB3E5FC) // Светло-голубой на синем фоне
|
||||
else Color(0xFF2196F3) // Стандартный Material Blue для входящих
|
||||
}
|
||||
val linksEnabled = !isSelectionMode
|
||||
val textClickHandler: (() -> Unit)? = if (isSelectionMode) onClick else null
|
||||
|
||||
val timeColor =
|
||||
remember(message.isOutgoing, isDarkTheme) {
|
||||
@@ -705,6 +708,10 @@ fun MessageBubble(
|
||||
fontSize = 16.sp,
|
||||
linkColor =
|
||||
linkColor,
|
||||
enableLinks =
|
||||
linksEnabled,
|
||||
onClick =
|
||||
textClickHandler,
|
||||
onLongClick =
|
||||
onLongClick // 🔥 Long press для selection
|
||||
)
|
||||
@@ -786,6 +793,8 @@ fun MessageBubble(
|
||||
color = textColor,
|
||||
fontSize = 17.sp,
|
||||
linkColor = linkColor,
|
||||
enableLinks = linksEnabled,
|
||||
onClick = textClickHandler,
|
||||
onLongClick =
|
||||
onLongClick // 🔥
|
||||
// Long
|
||||
@@ -861,6 +870,8 @@ fun MessageBubble(
|
||||
color = textColor,
|
||||
fontSize = 17.sp,
|
||||
linkColor = linkColor,
|
||||
enableLinks = linksEnabled,
|
||||
onClick = textClickHandler,
|
||||
onLongClick =
|
||||
onLongClick // 🔥
|
||||
// Long
|
||||
|
||||
@@ -399,7 +399,9 @@ fun MessageInputBar(
|
||||
ReplyImagePreview(
|
||||
attachment = imageAttachment,
|
||||
modifier = Modifier.size(36.dp),
|
||||
senderPublicKey = if (msg.isOutgoing) myPublicKey else opponentPublicKey,
|
||||
senderPublicKey = msg.publicKey.ifEmpty {
|
||||
if (msg.isOutgoing) myPublicKey else opponentPublicKey
|
||||
},
|
||||
recipientPrivateKey = myPrivateKey
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Reference in New Issue
Block a user