Исправлено поведение аватарок серий в группах и выделение сообщений
This commit is contained in:
@@ -62,6 +62,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -70,6 +71,7 @@ import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.viewinterop.AndroidView
|
||||
@@ -115,6 +117,26 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private data class IncomingRunAvatarAccumulator(
|
||||
val senderPublicKey: String,
|
||||
val senderDisplayName: String,
|
||||
var minTopPx: Float,
|
||||
var maxBottomPx: Float
|
||||
)
|
||||
|
||||
private data class IncomingRunAvatarOverlay(
|
||||
val runHeadIndex: Int,
|
||||
val senderPublicKey: String,
|
||||
val senderDisplayName: String,
|
||||
val topPx: Float
|
||||
)
|
||||
|
||||
private data class IncomingRunAvatarUiState(
|
||||
val showOnRunHeads: Set<Int>,
|
||||
val showOnRunTails: Set<Int>,
|
||||
val overlays: List<IncomingRunAvatarOverlay>
|
||||
)
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
||||
@@ -647,7 +669,250 @@ fun ChatDetailScreen(
|
||||
// <20>🔥 messagesWithDates — pre-computed in ViewModel on Dispatchers.Default
|
||||
// (dedup + sort + date headers off the main thread)
|
||||
val messagesWithDates by viewModel.messagesWithDates.collectAsState()
|
||||
val resolveSenderPublicKey: (ChatMessage?) -> String =
|
||||
remember(isGroupChat, currentUserPublicKey, user.publicKey) {
|
||||
{ msg ->
|
||||
when {
|
||||
msg == null -> ""
|
||||
msg.senderPublicKey.isNotBlank() -> msg.senderPublicKey.trim()
|
||||
msg.isOutgoing -> currentUserPublicKey.trim()
|
||||
isGroupChat -> ""
|
||||
else -> user.publicKey.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
val resolveSenderIdentity: (ChatMessage?) -> String =
|
||||
remember(isGroupChat, currentUserPublicKey, user.publicKey) {
|
||||
{ msg ->
|
||||
when {
|
||||
msg == null -> ""
|
||||
msg.isOutgoing ->
|
||||
"out:${currentUserPublicKey.trim().lowercase(Locale.ROOT)}"
|
||||
msg.senderPublicKey.isNotBlank() ->
|
||||
"in:${msg.senderPublicKey.trim().lowercase(Locale.ROOT)}"
|
||||
isGroupChat && msg.senderName.isNotBlank() ->
|
||||
"name:${msg.senderName.trim().lowercase(Locale.ROOT)}"
|
||||
!isGroupChat ->
|
||||
"in:${user.publicKey.trim().lowercase(Locale.ROOT)}"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
val isMessageBoundary: (ChatMessage, ChatMessage?) -> Boolean =
|
||||
remember(isGroupChat, currentUserPublicKey, user.publicKey) {
|
||||
{ currentMessage, adjacentMessage ->
|
||||
if (adjacentMessage == null) {
|
||||
true
|
||||
} else if (adjacentMessage.isOutgoing != currentMessage.isOutgoing) {
|
||||
true
|
||||
} else if (
|
||||
kotlin.math.abs(
|
||||
currentMessage.timestamp.time -
|
||||
adjacentMessage.timestamp.time
|
||||
) > 60_000L
|
||||
) {
|
||||
true
|
||||
} else if (
|
||||
isGroupChat &&
|
||||
!currentMessage.isOutgoing &&
|
||||
!adjacentMessage.isOutgoing
|
||||
) {
|
||||
val currentSenderIdentity =
|
||||
resolveSenderIdentity(currentMessage)
|
||||
val adjacentSenderIdentity =
|
||||
resolveSenderIdentity(adjacentMessage)
|
||||
if (
|
||||
currentSenderIdentity.isBlank() ||
|
||||
adjacentSenderIdentity.isBlank()
|
||||
) {
|
||||
true
|
||||
} else {
|
||||
!currentSenderIdentity.equals(
|
||||
adjacentSenderIdentity,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
val messageRunNewestIndex =
|
||||
remember(messagesWithDates, isGroupChat, currentUserPublicKey, user.publicKey) {
|
||||
IntArray(messagesWithDates.size).also { runHeadByIndex ->
|
||||
messagesWithDates.indices.forEach { messageIndex ->
|
||||
if (messageIndex == 0) {
|
||||
runHeadByIndex[messageIndex] = messageIndex
|
||||
} else {
|
||||
val currentMessage = messagesWithDates[messageIndex].first
|
||||
val newerMessage = messagesWithDates[messageIndex - 1].first
|
||||
runHeadByIndex[messageIndex] =
|
||||
if (isMessageBoundary(currentMessage, newerMessage)) {
|
||||
messageIndex
|
||||
} else {
|
||||
runHeadByIndex[messageIndex - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val messageRunOldestIndexByHead =
|
||||
remember(messageRunNewestIndex) {
|
||||
IntArray(messageRunNewestIndex.size) { it }.also { runTailByHead ->
|
||||
messageRunNewestIndex.indices.forEach { messageIndex ->
|
||||
val runHeadIndex = messageRunNewestIndex[messageIndex]
|
||||
runTailByHead[runHeadIndex] = messageIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val incomingRunAvatarSize = 42.dp
|
||||
val incomingRunAvatarInsetStart = 6.dp
|
||||
val incomingRunAvatarTopGuard = 4.dp
|
||||
val incomingRunAvatarUiState by remember(
|
||||
isGroupChat,
|
||||
messageRunNewestIndex,
|
||||
messageRunOldestIndexByHead,
|
||||
messagesWithDates,
|
||||
listState,
|
||||
density
|
||||
) {
|
||||
derivedStateOf {
|
||||
if (!isGroupChat || messagesWithDates.isEmpty()) {
|
||||
return@derivedStateOf IncomingRunAvatarUiState(
|
||||
showOnRunHeads = emptySet(),
|
||||
showOnRunTails = emptySet(),
|
||||
overlays = emptyList()
|
||||
)
|
||||
}
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val visibleItems = layoutInfo.visibleItemsInfo
|
||||
if (visibleItems.isEmpty()) {
|
||||
return@derivedStateOf IncomingRunAvatarUiState(
|
||||
showOnRunHeads = emptySet(),
|
||||
showOnRunTails = emptySet(),
|
||||
overlays = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val avatarSizePx = with(density) { incomingRunAvatarSize.toPx() }
|
||||
val topGuardPx = with(density) { incomingRunAvatarTopGuard.toPx() }
|
||||
val viewportStart = layoutInfo.viewportStartOffset.toFloat()
|
||||
val viewportEnd = layoutInfo.viewportEndOffset.toFloat()
|
||||
val maxAvatarTop = viewportEnd - avatarSizePx
|
||||
val visibleIndexSet = visibleItems.map { it.index }.toHashSet()
|
||||
val visibleRuns = linkedMapOf<Int, IncomingRunAvatarAccumulator>()
|
||||
|
||||
visibleItems.forEach { itemInfo ->
|
||||
val visibleIndex = itemInfo.index
|
||||
if (visibleIndex !in messagesWithDates.indices) {
|
||||
return@forEach
|
||||
}
|
||||
val message = messagesWithDates[visibleIndex].first
|
||||
if (message.isOutgoing) {
|
||||
return@forEach
|
||||
}
|
||||
val senderPublicKey = resolveSenderPublicKey(message).trim()
|
||||
if (senderPublicKey.isBlank()) {
|
||||
return@forEach
|
||||
}
|
||||
val runHeadIndex =
|
||||
messageRunNewestIndex.getOrNull(visibleIndex)
|
||||
?: visibleIndex
|
||||
val itemTopPx = itemInfo.offset.toFloat()
|
||||
val itemBottomPx = (itemInfo.offset + itemInfo.size).toFloat()
|
||||
val currentRun = visibleRuns[runHeadIndex]
|
||||
if (currentRun == null) {
|
||||
visibleRuns[runHeadIndex] =
|
||||
IncomingRunAvatarAccumulator(
|
||||
senderPublicKey = senderPublicKey,
|
||||
senderDisplayName =
|
||||
message.senderName.ifBlank {
|
||||
senderPublicKey
|
||||
},
|
||||
minTopPx = itemTopPx,
|
||||
maxBottomPx = itemBottomPx
|
||||
)
|
||||
} else {
|
||||
if (itemTopPx < currentRun.minTopPx) {
|
||||
currentRun.minTopPx = itemTopPx
|
||||
}
|
||||
if (itemBottomPx > currentRun.maxBottomPx) {
|
||||
currentRun.maxBottomPx = itemBottomPx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showOnRunHeads = hashSetOf<Int>()
|
||||
val showOnRunTails = hashSetOf<Int>()
|
||||
val overlays = arrayListOf<IncomingRunAvatarOverlay>()
|
||||
|
||||
visibleRuns.forEach { (runHeadIndex, runData) ->
|
||||
val runTailIndex =
|
||||
messageRunOldestIndexByHead.getOrNull(runHeadIndex)
|
||||
?: runHeadIndex
|
||||
val isRunHeadVisible = visibleIndexSet.contains(runHeadIndex)
|
||||
val isRunTailVisible = visibleIndexSet.contains(runTailIndex)
|
||||
|
||||
when {
|
||||
isRunHeadVisible -> {
|
||||
// Start/default phase: keep avatar on the lower (newest) bubble.
|
||||
showOnRunHeads.add(runHeadIndex)
|
||||
}
|
||||
isRunTailVisible -> {
|
||||
// End phase: keep avatar attached to the last bubble in run.
|
||||
showOnRunTails.add(runHeadIndex)
|
||||
}
|
||||
else -> {
|
||||
// Middle phase: floating avatar while scrolling through run.
|
||||
var avatarTopPx =
|
||||
kotlin.math.min(
|
||||
runData.maxBottomPx - avatarSizePx,
|
||||
maxAvatarTop
|
||||
)
|
||||
val topClampPx = runData.minTopPx + topGuardPx
|
||||
if (avatarTopPx < topClampPx) {
|
||||
avatarTopPx = topClampPx
|
||||
}
|
||||
if (
|
||||
avatarTopPx + avatarSizePx > viewportStart &&
|
||||
avatarTopPx < viewportEnd
|
||||
) {
|
||||
overlays +=
|
||||
IncomingRunAvatarOverlay(
|
||||
runHeadIndex = runHeadIndex,
|
||||
senderPublicKey =
|
||||
runData.senderPublicKey,
|
||||
senderDisplayName =
|
||||
runData.senderDisplayName,
|
||||
topPx = avatarTopPx
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IncomingRunAvatarUiState(
|
||||
showOnRunHeads = showOnRunHeads,
|
||||
showOnRunTails = showOnRunTails,
|
||||
overlays = overlays.sortedBy { it.topPx }
|
||||
)
|
||||
}
|
||||
}
|
||||
val openProfileByPublicKey: (String) -> Unit = { rawPublicKey ->
|
||||
val normalizedPublicKey = rawPublicKey.trim()
|
||||
if (normalizedPublicKey.isNotBlank()) {
|
||||
scope.launch {
|
||||
val resolvedUser =
|
||||
viewModel.resolveUserForProfile(normalizedPublicKey)
|
||||
if (resolvedUser != null) {
|
||||
showContextMenu = false
|
||||
contextMenuMessage = null
|
||||
onUserProfileClick(resolvedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 🔥 Функция для скролла к сообщению с подсветкой
|
||||
val scrollToMessage: (String) -> Unit = { messageId ->
|
||||
scope.launch {
|
||||
@@ -2065,7 +2330,7 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
// Есть сообщения
|
||||
else ->
|
||||
else -> {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier =
|
||||
@@ -2117,36 +2382,6 @@ fun ChatDetailScreen(
|
||||
index,
|
||||
(message, showDate)
|
||||
->
|
||||
// Определяем,
|
||||
// показывать ли
|
||||
// хвостик
|
||||
// (последнее
|
||||
// сообщение в
|
||||
// группе)
|
||||
val nextMessage =
|
||||
messagesWithDates
|
||||
.getOrNull(
|
||||
index +
|
||||
1
|
||||
)
|
||||
?.first
|
||||
val showTail =
|
||||
nextMessage ==
|
||||
null ||
|
||||
nextMessage
|
||||
.isOutgoing !=
|
||||
message.isOutgoing ||
|
||||
(message.timestamp
|
||||
.time -
|
||||
nextMessage
|
||||
.timestamp
|
||||
.time) >
|
||||
60_000
|
||||
|
||||
// Определяем начало
|
||||
// новой
|
||||
// группы (для
|
||||
// отступов)
|
||||
val prevMessage =
|
||||
messagesWithDates
|
||||
.getOrNull(
|
||||
@@ -2154,18 +2389,59 @@ fun ChatDetailScreen(
|
||||
1
|
||||
)
|
||||
?.first
|
||||
val nextMessage =
|
||||
messagesWithDates
|
||||
.getOrNull(
|
||||
index +
|
||||
1
|
||||
)
|
||||
?.first
|
||||
val senderPublicKeyForMessage =
|
||||
resolveSenderPublicKey(
|
||||
message
|
||||
)
|
||||
// Для reverseLayout + DESC списка:
|
||||
// prev = более новое сообщение,
|
||||
// next = более старое.
|
||||
val showTail =
|
||||
isMessageBoundary(message, prevMessage)
|
||||
val isGroupStart =
|
||||
prevMessage !=
|
||||
null &&
|
||||
(prevMessage
|
||||
.isOutgoing !=
|
||||
message.isOutgoing ||
|
||||
(prevMessage
|
||||
.timestamp
|
||||
.time -
|
||||
message.timestamp
|
||||
.time) >
|
||||
60_000)
|
||||
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() &&
|
||||
((index ==
|
||||
runHeadIndex &&
|
||||
isHeadPhase &&
|
||||
showTail) ||
|
||||
(index ==
|
||||
runTailIndex &&
|
||||
isTailPhase &&
|
||||
isGroupStart))
|
||||
|
||||
Column {
|
||||
if (showDate
|
||||
@@ -2182,14 +2458,6 @@ fun ChatDetailScreen(
|
||||
}
|
||||
val selectionKey =
|
||||
message.id
|
||||
val senderPublicKeyForMessage =
|
||||
if (message.senderPublicKey.isNotBlank()) {
|
||||
message.senderPublicKey
|
||||
} else if (message.isOutgoing) {
|
||||
currentUserPublicKey
|
||||
} else {
|
||||
user.publicKey
|
||||
}
|
||||
MessageBubble(
|
||||
message =
|
||||
message,
|
||||
@@ -2201,6 +2469,8 @@ fun ChatDetailScreen(
|
||||
isSelectionMode,
|
||||
showTail =
|
||||
showTail,
|
||||
showIncomingGroupAvatar =
|
||||
showIncomingGroupAvatar,
|
||||
isGroupStart =
|
||||
isGroupStart,
|
||||
isSelected =
|
||||
@@ -2225,7 +2495,8 @@ fun ChatDetailScreen(
|
||||
user.publicKey,
|
||||
showGroupSenderLabel =
|
||||
isGroupChat &&
|
||||
!message.isOutgoing,
|
||||
!message.isOutgoing &&
|
||||
isGroupStart,
|
||||
isGroupSenderAdmin =
|
||||
isGroupChat &&
|
||||
senderPublicKeyForMessage
|
||||
@@ -2384,6 +2655,12 @@ fun ChatDetailScreen(
|
||||
true
|
||||
)
|
||||
},
|
||||
onAvatarClick = {
|
||||
avatarOwnerPublicKey ->
|
||||
openProfileByPublicKey(
|
||||
avatarOwnerPublicKey
|
||||
)
|
||||
},
|
||||
onForwardedSenderClick = { senderPublicKey ->
|
||||
// Open profile of the forwarded message sender
|
||||
scope.launch {
|
||||
@@ -2539,6 +2816,61 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (incomingRunAvatarUiState.overlays.isNotEmpty()) {
|
||||
val avatarInsetPx =
|
||||
with(density) {
|
||||
incomingRunAvatarInsetStart
|
||||
.roundToPx()
|
||||
}
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.matchParentSize()
|
||||
.graphicsLayer {
|
||||
clip = true
|
||||
}
|
||||
) {
|
||||
incomingRunAvatarUiState.overlays.forEach { overlay ->
|
||||
key(
|
||||
overlay.runHeadIndex,
|
||||
overlay.senderPublicKey
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.offset {
|
||||
IntOffset(
|
||||
avatarInsetPx,
|
||||
overlay.topPx
|
||||
.toInt()
|
||||
)
|
||||
}
|
||||
) {
|
||||
AvatarImage(
|
||||
publicKey =
|
||||
overlay.senderPublicKey,
|
||||
avatarRepository =
|
||||
avatarRepository,
|
||||
size =
|
||||
incomingRunAvatarSize,
|
||||
isDarkTheme =
|
||||
isDarkTheme,
|
||||
onClick =
|
||||
if (isSelectionMode) null
|
||||
else {
|
||||
{
|
||||
openProfileByPublicKey(
|
||||
overlay.senderPublicKey
|
||||
)
|
||||
}
|
||||
},
|
||||
displayName =
|
||||
overlay.senderDisplayName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Конец Column внутри Scaffold content
|
||||
|
||||
@@ -290,6 +290,7 @@ fun MessageBubble(
|
||||
isSystemSafeChat: Boolean = false,
|
||||
isSelectionMode: Boolean = false,
|
||||
showTail: Boolean = true,
|
||||
showIncomingGroupAvatar: Boolean? = null,
|
||||
isGroupStart: Boolean = false,
|
||||
isSelected: Boolean = false,
|
||||
isHighlighted: Boolean = false,
|
||||
@@ -311,6 +312,7 @@ fun MessageBubble(
|
||||
onRetry: () -> Unit = {},
|
||||
onDelete: () -> Unit = {},
|
||||
onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> },
|
||||
onAvatarClick: (senderPublicKey: String) -> Unit = {},
|
||||
onForwardedSenderClick: (senderPublicKey: String) -> Unit = {},
|
||||
onMentionClick: (username: String) -> Unit = {},
|
||||
onGroupInviteOpen: (SearchUser) -> Unit = {},
|
||||
@@ -536,11 +538,12 @@ fun MessageBubble(
|
||||
val telegramIncomingAvatarSize = 42.dp
|
||||
val telegramIncomingAvatarLane = 48.dp
|
||||
val telegramIncomingAvatarInset = 6.dp
|
||||
val showIncomingGroupAvatar =
|
||||
isGroupChat &&
|
||||
!message.isOutgoing &&
|
||||
showTail &&
|
||||
senderPublicKey.isNotBlank()
|
||||
val shouldShowIncomingGroupAvatar =
|
||||
showIncomingGroupAvatar
|
||||
?: (isGroupChat &&
|
||||
!message.isOutgoing &&
|
||||
showTail &&
|
||||
senderPublicKey.isNotBlank())
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
@@ -553,7 +556,7 @@ fun MessageBubble(
|
||||
) {
|
||||
// Selection checkmark
|
||||
AnimatedVisibility(
|
||||
visible = isSelected,
|
||||
visible = isSelected && message.isOutgoing,
|
||||
enter =
|
||||
fadeIn(tween(150)) +
|
||||
scaleIn(
|
||||
@@ -579,11 +582,15 @@ fun MessageBubble(
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !isSelected,
|
||||
AnimatedVisibility(
|
||||
visible = !isSelected || !message.isOutgoing,
|
||||
enter = fadeIn(tween(100)),
|
||||
exit = fadeOut(tween(100))
|
||||
) { Spacer(modifier = Modifier.width(12.dp)) }
|
||||
) {
|
||||
val leadingSpacerWidth =
|
||||
if (!message.isOutgoing && isGroupChat) 0.dp else 12.dp
|
||||
Spacer(modifier = Modifier.width(leadingSpacerWidth))
|
||||
}
|
||||
|
||||
if (message.isOutgoing) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -597,7 +604,7 @@ fun MessageBubble(
|
||||
.align(Alignment.Bottom),
|
||||
contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
if (showIncomingGroupAvatar) {
|
||||
if (shouldShowIncomingGroupAvatar) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
@@ -607,11 +614,21 @@ fun MessageBubble(
|
||||
),
|
||||
contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
val avatarClickHandler: (() -> Unit)? =
|
||||
if (!isSelectionMode &&
|
||||
senderPublicKey
|
||||
.isNotBlank()
|
||||
) {
|
||||
{ onAvatarClick(senderPublicKey) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
AvatarImage(
|
||||
publicKey = senderPublicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
size = telegramIncomingAvatarSize,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = avatarClickHandler,
|
||||
displayName =
|
||||
senderName.ifBlank {
|
||||
senderPublicKey
|
||||
@@ -1217,6 +1234,37 @@ fun MessageBubble(
|
||||
// 💬 Context menu anchor (DropdownMenu positions relative to this Box)
|
||||
contextMenuContent()
|
||||
}
|
||||
|
||||
if (!message.isOutgoing) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
AnimatedVisibility(
|
||||
visible = isSelected,
|
||||
enter =
|
||||
fadeIn(tween(150)) +
|
||||
scaleIn(
|
||||
initialScale = 0.3f,
|
||||
animationSpec =
|
||||
spring(dampingRatio = 0.6f)
|
||||
),
|
||||
exit = fadeOut(tween(100)) + scaleOut(targetScale = 0.3f)
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(start = 4.dp, end = 12.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = TelegramIcons.Done,
|
||||
contentDescription = "Selected",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1259,27 +1307,8 @@ private fun isGroupInviteCode(text: String): Boolean {
|
||||
}
|
||||
|
||||
private fun groupSenderLabelColor(publicKey: String, isDarkTheme: Boolean): Color {
|
||||
val paletteDark =
|
||||
listOf(
|
||||
Color(0xFF7ED957),
|
||||
Color(0xFF6EC1FF),
|
||||
Color(0xFFFF9F68),
|
||||
Color(0xFFC38AFF),
|
||||
Color(0xFFFF7AA2),
|
||||
Color(0xFF4DD7C8)
|
||||
)
|
||||
val paletteLight =
|
||||
listOf(
|
||||
Color(0xFF2E7D32),
|
||||
Color(0xFF1565C0),
|
||||
Color(0xFFEF6C00),
|
||||
Color(0xFF6A1B9A),
|
||||
Color(0xFFC2185B),
|
||||
Color(0xFF00695C)
|
||||
)
|
||||
val palette = if (isDarkTheme) paletteDark else paletteLight
|
||||
val index = kotlin.math.abs(publicKey.hashCode()) % palette.size
|
||||
return palette[index]
|
||||
// Match nickname color with avatar initials color.
|
||||
return com.rosetta.messenger.ui.chats.getAvatarColor(publicKey, isDarkTheme).textColor
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user