looking good

This commit is contained in:
2026-02-24 13:34:56 +00:00
parent b159bf6a56
commit 6cdcbbfd4c
4 changed files with 393 additions and 112 deletions

View File

@@ -1,8 +1,10 @@
package com.example.fluffytrix.ui.screens.main
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -13,6 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.example.fluffytrix.ui.screens.main.components.ChannelList
@@ -37,9 +40,34 @@ fun MainScreen(
val channelName by viewModel.channelName.collectAsState()
val isReorderMode by viewModel.isReorderMode.collectAsState()
val homeUnreadStatus by viewModel.homeUnreadStatus.collectAsState()
val channelSections by viewModel.channelSections.collectAsState()
val unreadMarkerIndex by viewModel.unreadMarkerIndex.collectAsState()
// Back button opens channel list when in a chat, or does nothing if already open
BackHandler(enabled = selectedChannel != null && !showChannelList) {
viewModel.toggleChannelList()
}
Scaffold { padding ->
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(showChannelList) {
if (!showChannelList) {
var totalDrag = 0f
detectHorizontalDragGestures(
onDragStart = { totalDrag = 0f },
onHorizontalDrag = { _, dragAmount ->
totalDrag += dragAmount
if (totalDrag > 60f) {
viewModel.toggleChannelList()
totalDrag = 0f
}
},
)
}
},
) {
// Main content: SpaceList + chat + members
Row(modifier = Modifier.fillMaxSize()) {
SpaceList(
@@ -60,6 +88,7 @@ fun MainScreen(
onSendMessage = { viewModel.sendMessage(it) },
onSendFile = { viewModel.sendFile(it) },
onLoadMore = { viewModel.loadMoreMessages() },
unreadMarkerIndex = unreadMarkerIndex,
modifier = Modifier.weight(1f),
contentPadding = padding,
)
@@ -83,11 +112,13 @@ fun MainScreen(
Spacer(modifier = Modifier.width(64.dp))
ChannelList(
channels = channels,
sections = channelSections,
selectedChannel = selectedChannel,
onChannelClick = {
viewModel.selectChannel(it)
viewModel.toggleChannelList()
},
onDismiss = { viewModel.toggleChannelList() },
onLogout = {
viewModel.logout()
onLogout()

View File

@@ -69,6 +69,12 @@ data class MessageItem(
val timestamp: Long,
)
data class ChannelSection(
val spaceId: String? = null,
val spaceName: String? = null,
val channels: List<ChannelItem> = emptyList(),
)
data class MemberItem(
val userId: String,
val displayName: String,
@@ -120,11 +126,21 @@ class MainViewModel(
private var cachedOrphanIds: Set<String>? = null
private val _roomUnreadStatus = MutableStateFlow<Map<String, UnreadStatus>>(emptyMap())
private val _roomUnreadCount = MutableStateFlow<Map<String, ULong>>(emptyMap())
private val _unreadMarkerIndex = MutableStateFlow(-1)
val unreadMarkerIndex: StateFlow<Int> = _unreadMarkerIndex
private val _homeUnreadStatus = MutableStateFlow(UnreadStatus.NONE)
val homeUnreadStatus: StateFlow<UnreadStatus> = _homeUnreadStatus
private val _spaceChildrenMap = MutableStateFlow<Map<String, Set<String>>>(emptyMap())
private val _channelSections = MutableStateFlow<List<ChannelSection>>(emptyList())
val channelSections: StateFlow<List<ChannelSection>> = _channelSections
// Maps spaceId -> list of (childSpaceId, childSpaceName, Set<roomIds>)
private val _directChildSpaces = MutableStateFlow<Map<String, List<Triple<String, String, Set<String>>>>>(emptyMap())
// Per-room caches
private val messageCache = mutableMapOf<String, MutableList<MessageItem>>()
private val messageIds = mutableMapOf<String, MutableSet<String>>()
@@ -188,6 +204,7 @@ class MainViewModel(
}
val syncUnread = mutableMapOf<String, UnreadStatus>()
val syncUnreadCount = mutableMapOf<String, ULong>()
for (room in joinedRooms) {
val roomId = room.id()
if (roomId == _selectedChannel.value) continue
@@ -203,9 +220,12 @@ class MainViewModel(
}
if (highlight > 0uL || unreadMentions > 0uL) syncUnread[roomId] = UnreadStatus.MENTIONED
else if (notification > 0uL || unreadNotif > 0uL || unread > 0uL) syncUnread[roomId] = UnreadStatus.UNREAD
if (unread > 0uL) syncUnreadCount[roomId] = unread
else if (notification > 0uL) syncUnreadCount[roomId] = notification
} catch (_: Exception) { }
}
_roomUnreadStatus.value = syncUnread
_roomUnreadCount.value = syncUnreadCount
// Rebuild spaces list only when the set of space IDs changes
try {
@@ -266,13 +286,10 @@ class MainViewModel(
private fun observeSpaceFiltering() {
viewModelScope.launch {
combine(_allChannelRooms, _spaceChildren, _selectedSpace, _channelOrderMap, _roomUnreadStatus) { args ->
@Suppress("UNCHECKED_CAST")
val allChannels = args[0] as List<ChannelItem>
val children = args[1] as Set<String>?
val spaceId = args[2] as String?
val orderMap = args[3] as Map<String, List<String>>
val unreadMap = args[4] as Map<String, UnreadStatus>
combine(
combine(_allChannelRooms, _spaceChildren, _selectedSpace) { a, b, c -> Triple(a, b, c) },
combine(_channelOrderMap, _roomUnreadStatus, _directChildSpaces) { a, b, c -> Triple(a, b, c) },
) { (allChannels, children, spaceId), (orderMap, unreadMap, directChildren) ->
val filtered = if (children == null) allChannels
else allChannels.filter { it.id in children }
val withUnread = filtered.map { ch ->
@@ -280,11 +297,47 @@ class MainViewModel(
if (status != ch.unreadStatus) ch.copy(unreadStatus = status) else ch
}
val savedOrder = spaceId?.let { orderMap[it] }
if (savedOrder != null) {
val sorted = if (savedOrder != null) {
val indexMap = savedOrder.withIndex().associate { (i, id) -> id to i }
withUnread.sortedBy { indexMap[it.id] ?: Int.MAX_VALUE }
} else withUnread
}.collect { _channels.value = it }
// Build sections
val childSpaces = spaceId?.let { directChildren[it] }
val sections = if (childSpaces.isNullOrEmpty()) {
// Home view or space with no child spaces — single flat section
listOf(ChannelSection(channels = sorted))
} else {
val channelMap = sorted.associateBy { it.id }
val usedIds = mutableSetOf<String>()
val sectionList = mutableListOf<ChannelSection>()
// Direct rooms (not in any child space)
val childSpaceRoomIds = childSpaces.flatMap { it.third }.toSet()
val directRooms = sorted.filter { it.id !in childSpaceRoomIds }
if (directRooms.isNotEmpty()) {
sectionList.add(ChannelSection(channels = directRooms))
usedIds.addAll(directRooms.map { it.id })
}
// Child space sections
for ((csId, csName, csRoomIds) in childSpaces) {
val sectionChannels = csRoomIds.mapNotNull { channelMap[it] }
.filter { it.id !in usedIds }
if (sectionChannels.isNotEmpty()) {
sectionList.add(ChannelSection(spaceId = csId, spaceName = csName, channels = sectionChannels))
usedIds.addAll(sectionChannels.map { it.id })
}
}
sectionList
}
sorted to sections
}.collect { (sorted, sections) ->
_channels.value = sorted
_channelSections.value = sections
}
}
}
@@ -729,31 +782,62 @@ class MainViewModel(
try {
val spaceService = client.spaceService()
val map = mutableMapOf<String, Set<String>>()
val directMap = mutableMapOf<String, List<Triple<String, String, Set<String>>>>()
for (space in _spaces.value) {
if (space.id in _spaceChildrenMap.value) continue
val allRoomIds = mutableSetOf<String>()
val childSpaceIds = mutableSetOf<String>()
val directChildSpaceList = mutableListOf<Pair<String, String>>() // id, name
val visited = mutableSetOf(space.id)
var currentLevel = listOf(space.id)
var isFirstLevel = true
while (currentLevel.isNotEmpty()) {
val results = currentLevel.map { sid ->
async { paginateSpaceFully(spaceService, sid) }
async { sid to paginateSpaceFully(spaceService, sid) }
}.awaitAll()
val nextLevel = mutableListOf<String>()
for (children in results) {
for ((parentSid, children) in results) {
for (child in children) {
allRoomIds.add(child.roomId)
if (child.roomType is org.matrix.rustcomponents.sdk.RoomType.Space) {
childSpaceIds.add(child.roomId)
if (visited.add(child.roomId)) nextLevel.add(child.roomId)
if (isFirstLevel && parentSid == space.id) {
directChildSpaceList.add(child.roomId to (child.displayName ?: child.roomId))
}
}
}
}
currentLevel = nextLevel
isFirstLevel = false
}
map[space.id] = allRoomIds - childSpaceIds
// Resolve rooms for each direct child space
val resolved = directChildSpaceList.map { (csId, csName) ->
val csRoomIds = mutableSetOf<String>()
val csVisited = mutableSetOf(csId)
var csLevel = listOf(csId)
while (csLevel.isNotEmpty()) {
val csResults = csLevel.map { sid ->
async { paginateSpaceFully(spaceService, sid) }
}.awaitAll()
val csNext = mutableListOf<String>()
for (ch in csResults.flatten()) {
if (ch.roomType is org.matrix.rustcomponents.sdk.RoomType.Space) {
if (csVisited.add(ch.roomId)) csNext.add(ch.roomId)
} else {
csRoomIds.add(ch.roomId)
}
}
csLevel = csNext
}
Triple(csId, csName, csRoomIds as Set<String>)
}
directMap[space.id] = resolved
}
_spaceChildrenMap.value = _spaceChildrenMap.value + map
_directChildSpaces.value = _directChildSpaces.value + directMap
updateSpaceUnreadStatus()
// Recompute orphan rooms now that we have all space children
computeOrphanRoomsFromCache()
@@ -826,6 +910,7 @@ class MainViewModel(
// Use cache if available
_spaceChildrenMap.value[spaceId]?.let { cached ->
_spaceChildren.value = cached
// directChildSpaces is already cached too via the combine in observeSpaceFiltering
return
}
val client = authRepository.getClient() ?: return
@@ -834,25 +919,60 @@ class MainViewModel(
val spaceService = client.spaceService()
val allRoomIds = mutableSetOf<String>()
val childSpaceIds = mutableSetOf<String>()
// Track direct child spaces of the selected space
val directChildSpaceList = mutableListOf<Triple<String, String, Set<String>>>()
// BFS with parallel loading of sibling spaces
val visited = mutableSetOf(spaceId)
var currentLevel = listOf(spaceId)
var isFirstLevel = true
while (currentLevel.isNotEmpty()) {
val results = currentLevel.map { sid ->
async { paginateSpaceFully(spaceService, sid) }
async { sid to paginateSpaceFully(spaceService, sid) }
}.awaitAll()
val nextLevel = mutableListOf<String>()
for (children in results) {
for ((parentSid, children) in results) {
for (child in children) {
allRoomIds.add(child.roomId)
if (child.roomType is org.matrix.rustcomponents.sdk.RoomType.Space) {
childSpaceIds.add(child.roomId)
if (visited.add(child.roomId)) nextLevel.add(child.roomId)
// Track direct child spaces of the selected space
if (isFirstLevel && parentSid == spaceId) {
// Will fill rooms later
directChildSpaceList.add(Triple(child.roomId, child.displayName ?: child.roomId, emptySet()))
}
}
}
}
currentLevel = nextLevel
isFirstLevel = false
}
// Now resolve rooms for each direct child space (recursively)
val resolvedDirectChildren = directChildSpaceList.map { (csId, csName, _) ->
val csRoomIds = mutableSetOf<String>()
val csVisited = mutableSetOf(csId)
var csLevel = listOf(csId)
while (csLevel.isNotEmpty()) {
val csResults = csLevel.map { sid ->
async { paginateSpaceFully(spaceService, sid) }
}.awaitAll()
val csNext = mutableListOf<String>()
for (ch in csResults.flatten()) {
if (ch.roomType is org.matrix.rustcomponents.sdk.RoomType.Space) {
if (csVisited.add(ch.roomId)) csNext.add(ch.roomId)
} else {
csRoomIds.add(ch.roomId)
}
}
csLevel = csNext
}
Triple(csId, csName, csRoomIds as Set<String>)
}
_directChildSpaces.value = _directChildSpaces.value + (spaceId to resolvedDirectChildren)
// Only keep actual rooms, not child spaces
val roomIds = allRoomIds - childSpaceIds
_spaceChildren.value = roomIds
@@ -866,11 +986,31 @@ class MainViewModel(
}
fun selectChannel(channelId: String) {
// Capture unread count before clearing
val unreadCount = _roomUnreadCount.value[channelId]?.toInt() ?: 0
_unreadMarkerIndex.value = if (unreadCount > 0) unreadCount - 1 else -1
_selectedChannel.value = channelId
if (_roomUnreadStatus.value.containsKey(channelId)) {
_roomUnreadStatus.value = _roomUnreadStatus.value - channelId
_roomUnreadCount.value = _roomUnreadCount.value - channelId
updateSpaceUnreadStatus()
}
// Send read receipt to the server
sendReadReceipt(channelId)
}
private fun sendReadReceipt(roomId: String) {
val client = authRepository.getClient() ?: return
viewModelScope.launch(Dispatchers.IO) {
try {
val room = client.getRoom(roomId) ?: return@launch
room.markAsRead(org.matrix.rustcomponents.sdk.ReceiptType.READ)
} catch (e: Exception) {
android.util.Log.e("MainVM", "Failed to send read receipt for $roomId", e)
}
}
}
private fun updateSpaceUnreadStatus() {

View File

@@ -24,10 +24,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.SwapVert
import androidx.compose.material.icons.filled.Tag
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -37,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@@ -46,26 +50,32 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.pointer.pointerInput
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.zIndex
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import com.example.fluffytrix.ui.screens.main.ChannelItem
import com.example.fluffytrix.ui.screens.main.ChannelSection
import com.example.fluffytrix.ui.screens.main.UnreadStatus
import kotlin.math.roundToInt
@Composable
fun ChannelList(
channels: List<ChannelItem>,
sections: List<ChannelSection>,
selectedChannel: String?,
onChannelClick: (String) -> Unit,
onLogout: () -> Unit,
onDismiss: () -> Unit = {},
contentPadding: PaddingValues,
isReorderMode: Boolean = false,
onToggleReorderMode: () -> Unit = {},
onMoveChannel: (from: Int, to: Int) -> Unit = { _, _ -> },
) {
var showLogoutDialog by remember { mutableStateOf(false) }
val collapsedSections = remember { mutableStateMapOf<String, Boolean>() }
// Drag state
var draggingIndex by remember { mutableIntStateOf(-1) }
@@ -92,9 +102,22 @@ fun ChannelList(
Column(
modifier = Modifier
.width(240.dp)
.fillMaxWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.surface)
.pointerInput(Unit) {
var totalDrag = 0f
detectHorizontalDragGestures(
onDragStart = { totalDrag = 0f },
onHorizontalDrag = { _, dragAmount ->
totalDrag += dragAmount
if (totalDrag < -60f) {
onDismiss()
totalDrag = 0f
}
},
)
}
.padding(top = contentPadding.calculateTopPadding()),
) {
// Header
@@ -133,11 +156,52 @@ fun ChannelList(
state = listState,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
itemsIndexed(channels, key = { _, ch -> ch.id }) { index, channel ->
for ((sectionIdx, section) in sections.withIndex()) {
// Section header for child spaces
if (section.spaceName != null && section.spaceId != null) {
item(key = "section_${section.spaceId}") {
val isCollapsed = collapsedSections[section.spaceId] ?: false
if (sectionIdx > 0) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 4.dp),
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { collapsedSections[section.spaceId] = !isCollapsed }
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = if (isCollapsed) Icons.Default.ExpandMore else Icons.Default.ExpandLess,
contentDescription = if (isCollapsed) "Expand" else "Collapse",
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = section.spaceName.uppercase(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
val isCollapsed = section.spaceId?.let { collapsedSections[it] ?: false } ?: false
if (!isCollapsed) {
itemsIndexed(section.channels, key = { _, ch -> ch.id }) { index, channel ->
val isSelected = channel.id == selectedChannel
val isDragging = draggingIndex == index
val hasUnread = channel.unreadStatus != UnreadStatus.NONE
val globalIndex = channels.indexOfFirst { it.id == channel.id }
val isDragging = draggingIndex == globalIndex
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp, label = "elevation")
Row(
@@ -147,10 +211,10 @@ fun ChannelList(
if (isDragging) Modifier
.zIndex(1f)
.offset { IntOffset(0, dragOffsetY.roundToInt()) }
.shadow(elevation, RoundedCornerShape(4.dp))
.shadow(elevation, RoundedCornerShape(8.dp))
else Modifier
)
.clip(RoundedCornerShape(4.dp))
.clip(RoundedCornerShape(8.dp))
.background(
when {
isDragging -> MaterialTheme.colorScheme.surfaceContainerHigh
@@ -159,11 +223,11 @@ fun ChannelList(
}
)
.then(if (!isReorderMode) Modifier.clickable { onChannelClick(channel.id) } else Modifier)
.padding(horizontal = 8.dp, vertical = 6.dp),
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isReorderMode) {
val currentIndex by rememberUpdatedState(index)
val currentGlobalIndex by rememberUpdatedState(globalIndex)
Icon(
imageVector = Icons.Default.DragHandle,
contentDescription = "Drag to reorder",
@@ -172,13 +236,13 @@ fun ChannelList(
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
draggingIndex = currentIndex
draggingIndex = currentGlobalIndex
dragOffsetY = 0f
},
onDrag = { change, dragAmount ->
change.consume()
dragOffsetY += dragAmount.y
val itemHeight = 34.dp.toPx()
val itemHeight = 40.dp.toPx()
val draggedPositions = (dragOffsetY / itemHeight).roundToInt()
val targetIndex = (draggingIndex + draggedPositions).coerceIn(0, channels.lastIndex)
if (targetIndex != draggingIndex) {
@@ -199,35 +263,42 @@ fun ChannelList(
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(6.dp))
}
if (channel.unreadStatus != UnreadStatus.NONE) {
if (hasUnread) {
Box(
modifier = Modifier
.size(8.dp)
.background(
if (channel.unreadStatus == UnreadStatus.MENTIONED) androidx.compose.ui.graphics.Color.Red
else androidx.compose.ui.graphics.Color.Gray,
if (channel.unreadStatus == UnreadStatus.MENTIONED) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.primary,
androidx.compose.foundation.shape.CircleShape,
),
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(6.dp))
} else {
Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(14.dp))
}
Icon(
imageVector = if (channel.isEncrypted) Icons.Default.Lock else Icons.Default.Tag,
contentDescription = null,
modifier = Modifier.height(18.dp),
tint = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp),
tint = when {
isSelected -> MaterialTheme.colorScheme.onPrimaryContainer
hasUnread -> MaterialTheme.colorScheme.onSurface
else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
},
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(10.dp))
Text(
text = channel.name,
style = MaterialTheme.typography.bodyMedium,
color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (hasUnread) FontWeight.SemiBold else FontWeight.Normal,
color = when {
isSelected -> MaterialTheme.colorScheme.onPrimaryContainer
hasUnread -> MaterialTheme.colorScheme.onSurface
else -> MaterialTheme.colorScheme.onSurfaceVariant
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@@ -235,4 +306,6 @@ fun ChannelList(
}
}
}
}
}
}

View File

@@ -119,6 +119,7 @@ fun MessageTimeline(
onSendMessage: (String) -> Unit,
onSendFile: (Uri) -> Unit,
onLoadMore: () -> Unit = {},
unreadMarkerIndex: Int = -1,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
@@ -213,12 +214,48 @@ fun MessageTimeline(
val next = if (index + 1 < count) messages[index + 1] else null
val isFirstInGroup = next == null || next.senderName != message.senderName
// Show "NEW" divider after the last unread message
// In reverse layout, unreadMarkerIndex 0 = newest message
// The divider goes after index unreadMarkerIndex (visually above unread block)
if (index == unreadMarkerIndex) {
Column {
if (isFirstInGroup) {
Spacer(modifier = Modifier.height(12.dp))
FullMessage(message)
} else {
CompactMessage(message.content)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.error.copy(alpha = 0.6f),
)
Text(
text = "NEW",
modifier = Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.error,
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.error.copy(alpha = 0.6f),
)
}
Spacer(modifier = Modifier.height(8.dp))
}
} else {
if (isFirstInGroup) {
Spacer(modifier = Modifier.height(12.dp))
FullMessage(message)
} else {
CompactMessage(message.content)
}
}
}
}