Done integration with media api

Implemented items showing, clicks and most of the group media functionality. Needs some UX improvements.
This commit is contained in:
hayk-space
2026-01-20 05:08:55 +04:00
parent f58c23eab8
commit 5fb783c0f5
3 changed files with 696 additions and 31 deletions
@@ -8,23 +8,34 @@ import SectionView
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.platform.base64ToBitmap
import kotlinx.datetime.*
import java.text.SimpleDateFormat
import java.util.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.chatModel
import chat.simplex.common.ui.theme.*
@@ -38,14 +49,22 @@ import chat.simplex.common.views.chatlist.openChat
import chat.simplex.common.views.chatlist.UnreadBadge
import chat.simplex.common.views.chatlist.unreadCountStr
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.ProviderMedia
import androidx.compose.ui.platform.LocalUriHandler
import java.io.File
import java.net.URI
import kotlinx.coroutines.runBlocking
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch
import kotlin.math.sign
enum class GroupInfoTab {
Members,
@@ -70,7 +89,9 @@ fun LazyListScope.GroupChatInfoTabs(
chat: Chat,
groupLink: GroupLink?,
manageGroupLink: () -> Unit,
openMemberSupport: () -> Unit
openMemberSupport: () -> Unit,
isLoadingTab: Boolean = false,
onLoadMoreTabItems: (() -> Unit)? = null
) {
item {
@@ -134,7 +155,10 @@ fun LazyListScope.GroupChatInfoTabs(
GroupInfoTab.Voice -> {
ContentItemsTab(
filteredChatItems = filteredChatItems,
scrollToItemId = scrollToItemId
selectedTab = selectedTab.value,
chat = chat,
isLoading = isLoadingTab,
onLoadMore = onLoadMoreTabItems
)
}
}
@@ -252,9 +276,23 @@ private fun LazyListScope.MembersTabContent(
private fun LazyListScope.ContentItemsTab(
filteredChatItems: State<List<ChatItem>>,
scrollToItemId: MutableState<Long?>
selectedTab: GroupInfoTab,
chat: Chat,
isLoading: Boolean = false,
onLoadMore: (() -> Unit)? = null
) {
if (filteredChatItems.value.isEmpty()) {
if (isLoading && filteredChatItems.value.isEmpty()) {
item {
Box(
Modifier
.fillMaxWidth()
.padding(vertical = DEFAULT_PADDING * 2),
contentAlignment = Alignment.Center
) {
ProgressIndicator()
}
}
} else if (filteredChatItems.value.isEmpty()) {
item {
Box(
Modifier
@@ -264,20 +302,66 @@ private fun LazyListScope.ContentItemsTab(
) {
//TODO: this is just a temporary UI, if no item, tab will not be shown
Text(
"No items found",
stringResource(MR.strings.no_items_found),
color = MaterialTheme.colors.secondary
)
}
}
} else {
items(filteredChatItems.value, key = { it.id }) { chatItem ->
Divider()
SectionItemView(
click = { scrollToItemId.value = chatItem.id },
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
GroupChatItemRow(chatItem)
when (selectedTab) {
GroupInfoTab.Images, GroupInfoTab.Videos -> {
MediaGridTabContent(
chatItems = filteredChatItems.value,
isVideo = selectedTab == GroupInfoTab.Videos
)
}
GroupInfoTab.Files -> {
MediaListTabContent(
chatItems = filteredChatItems.value,
type = MediaListType.Files,
chat = chat
)
}
GroupInfoTab.Links -> {
MediaListTabContent(
chatItems = filteredChatItems.value,
type = MediaListType.Links,
chat = chat
)
}
GroupInfoTab.Voice -> {
MediaListTabContent(
chatItems = filteredChatItems.value,
type = MediaListType.Voice,
chat = chat
)
}
else -> {}
}
// Load more button at the end
if (onLoadMore != null && filteredChatItems.value.isNotEmpty()) {
item {
Box(
Modifier
.fillMaxWidth()
.padding(vertical = DEFAULT_PADDING),
contentAlignment = Alignment.Center
) {
if (isLoading) {
ProgressIndicator()
} else {
TextButton(
onClick = { onLoadMore() },
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
) {
Text(
stringResource(MR.strings.load_more),
color = MaterialTheme.colors.primary
)
}
}
}
}
}
}
@@ -297,6 +381,474 @@ private fun tabLabel(tab: GroupInfoTab): Pair<ImageResource, StringResource> {
}
// Helper enum for media list types
private enum class MediaListType {
Files, Links, Voice
}
// Data class for date grouping
private data class DateGroup(
val monthYear: String, // e.g., "December 2025"
val items: List<ChatItem>
)
// Group items by month and year
private fun groupItemsByDate(items: List<ChatItem>): List<DateGroup> {
val grouped = items.groupBy { item ->
val tz = TimeZone.currentSystemDefault()
val date = item.meta.itemTs.toLocalDateTime(tz).date
val month = date.monthNumber
val year = date.year
// Format: "December 2025"
val monthNames = arrayOf(
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
)
"${monthNames[month - 1]} $year"
}
// Sort by date (newest first)
return grouped.map { (monthYear, items) ->
DateGroup(monthYear, items.sortedByDescending { it.meta.itemTs })
}.sortedByDescending { group ->
val tz = TimeZone.currentSystemDefault()
group.items.firstOrNull()?.meta?.itemTs?.toLocalDateTime(tz)?.date
}
}
// Provider for gallery in group info tabs
private fun providerForGroupGallery(
chatItems: List<ChatItem>,
cItemId: Long
): ImageGalleryProvider {
fun canShowMedia(item: ChatItem): Boolean =
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) &&
(item.file?.loaded == true && (getLoadedFilePath(item.file) != null || chatModel.connectedToRemote()))
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
var processedInternalIndex = -skipInternalIndex.sign
val indexOfFirst = chatItems.indexOfFirst { it.id == initialChatId }
// The first was deleted or moderated
if (indexOfFirst == -1) return null
for (chatItemsIndex in if (skipInternalIndex >= 0) indexOfFirst downTo 0 else indexOfFirst..chatItems.lastIndex) {
val item = chatItems[chatItemsIndex]
if (canShowMedia(item)) {
processedInternalIndex += skipInternalIndex.sign
}
if (processedInternalIndex == skipInternalIndex) {
return chatItemsIndex to item
}
}
return null
}
// Pager has a bug with overflowing when total pages is around Int.MAX_VALUE. Using smaller value
var initialIndex = 10000 / 2
var initialChatId = cItemId
return object: ImageGalleryProvider {
override val initialIndex: Int = initialIndex
override val totalMediaSize = mutableStateOf(10000)
override fun getMedia(index: Int): ProviderMedia? {
val internalIndex = initialIndex - index
val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) {
is MsgContent.MCImage -> {
val res = runBlocking { getLoadedImage(item.file) }
val filePath = getLoadedFilePath(item.file)
if (res != null && filePath != null) {
val (imageBitmap: ImageBitmap, data: ByteArray) = res
ProviderMedia.Image(data, imageBitmap)
} else null
}
is MsgContent.MCVideo -> {
val filePath = if (chatModel.connectedToRemote() && item.file?.loaded == true) getAppFilePath(item.file.fileName) else getLoadedFilePath(item.file)
if (filePath != null) {
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
ProviderMedia.Video(uri, item.file?.fileSource, (item.content.msgContent as MsgContent.MCVideo).image)
} else null
}
else -> null
}
}
override fun currentPageChanged(index: Int) {
val internalIndex = initialIndex - index
val item = item(internalIndex, initialChatId) ?: return
initialIndex = index
initialChatId = item.second.id
}
override fun scrollToStart() {
initialIndex = 0
initialChatId = chatItems.firstOrNull { canShowMedia(it) }?.id ?: return
}
override fun onDismiss(index: Int) {
// No-op for group info tabs
}
}
}
// Grid layout for images and videos
private fun LazyListScope.MediaGridTabContent(
chatItems: List<ChatItem>,
isVideo: Boolean
) {
val groupedItems = groupItemsByDate(chatItems)
groupedItems.forEach { group ->
// Date header
item {
SectionView(title = group.monthYear) {}
}
// Grid items (4 columns)
val columns = 4
val rows = (group.items.size + columns - 1) / columns
repeat(rows) { rowIndex ->
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF)
) {
repeat(columns) { colIndex ->
val itemIndex = rowIndex * columns + colIndex
val chatItem = if (itemIndex < group.items.size) group.items[itemIndex] else null
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(DEFAULT_PADDING_HALF)
.clip(RoundedCornerShape(4.dp))
.clickable {
chatItem?.let { item ->
// Check if file is loaded before opening gallery
if (item.file?.loaded == true && (getLoadedFilePath(item.file) != null || chatModel.connectedToRemote())) {
val provider = providerForGroupGallery(chatItems, item.id)
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView({ provider }, close)
}
}
// If not loaded, do nothing
}
}
) {
if (chatItem != null) {
MediaThumbnail(chatItem, isVideo)
}
}
}
}
}
}
}
}
// Thumbnail for images/videos
@Composable
private fun MediaThumbnail(chatItem: ChatItem, isVideo: Boolean) {
val content = chatItem.content.msgContent
val image = when (content) {
is MsgContent.MCImage -> content.image
is MsgContent.MCVideo -> content.image
else -> null
}
Box(modifier = Modifier.fillMaxSize()) {
if (image != null) {
Image(
bitmap = base64ToBitmap(image),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.surface),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(if (isVideo) MR.images.ic_videocam else MR.images.ic_image),
null,
tint = MaterialTheme.colors.secondary
)
}
}
// Video play icon overlay
if (isVideo || content is MsgContent.MCVideo) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(MR.images.ic_play_arrow_filled),
null,
modifier = Modifier.size(32.dp),
tint = Color.White
)
}
}
}
}
// List layout for files, links, and voice
private fun LazyListScope.MediaListTabContent(
chatItems: List<ChatItem>,
type: MediaListType,
chat: Chat
) {
val groupedItems = groupItemsByDate(chatItems)
groupedItems.forEach { group ->
// Date header
item {
SectionView(title = group.monthYear) {}
}
// List items
items(group.items, key = { it.id }) { chatItem ->
Divider()
when (type) {
MediaListType.Files -> FileItemRow(chatItem, chat)
MediaListType.Links -> LinkItemRow(chatItem)
MediaListType.Voice -> VoiceItemRow(chatItem, chat)
}
}
}
}
// File item row - reuses CIFileView component
@Composable
private fun FileItemRow(chatItem: ChatItem, chat: Chat) {
val file = chatItem.file
val senderName = getSenderName(chatItem)
val dateTime = formatDateTimeForList(chatItem.meta.itemTs)
val scope = rememberCoroutineScope()
val user = chatModel.currentUser.value
Column(
Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
) {
// Sender name and date row
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
senderName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Medium
)
Text(
dateTime,
fontSize = 11.sp,
color = MaterialTheme.colors.secondary
)
}
Spacer(Modifier.height(8.dp))
// File component - using CIFileView which handles all file actions
CIFileView(
file = file,
edited = chatItem.meta.itemEdited,
showMenu = remember { mutableStateOf(false) },
smallView = false,
receiveFile = { fileId ->
if (user != null) {
scope.launch {
withBGApi {
chatModel.controller.receiveFile(chat.remoteHostId, user, fileId)
}
}
}
}
)
}
}
// Link item row
@Composable
private fun LinkItemRow(chatItem: ChatItem) {
val content = chatItem.content.msgContent as? MsgContent.MCLink
?: return
val preview = content.preview
val senderName = getSenderName(chatItem)
val dateTime = formatDateTimeForList(chatItem.meta.itemTs)
val uriHandler = LocalUriHandler.current
SectionItemView(
click = { openBrowserAlert(preview.uri, uriHandler) },
minHeight = 80.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
) {
Column(Modifier.fillMaxWidth()) {
// Sender name and date row
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
senderName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Medium
)
Text(
dateTime,
fontSize = 11.sp,
color = MaterialTheme.colors.secondary
)
}
Spacer(Modifier.height(8.dp))
// Link preview - compact list style
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
if (preview.image.isNotEmpty()) {
Image(
bitmap = base64ToBitmap(preview.image),
contentDescription = stringResource(MR.strings.image_descr_link_preview),
modifier = Modifier
.size(64.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
Spacer(Modifier.width(DEFAULT_PADDING))
} else {
Icon(
painterResource(MR.images.ic_link),
null,
modifier = Modifier.size(40.dp),
tint = MaterialTheme.colors.primary
)
Spacer(Modifier.width(DEFAULT_PADDING))
}
Column(Modifier.weight(1f)) {
if (preview.title.isNotEmpty()) {
Text(
preview.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2
)
Spacer(Modifier.height(4.dp))
}
Text(
preview.uri,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
}
}
}
// Voice item row
@Composable
private fun VoiceItemRow(chatItem: ChatItem, chat: Chat) {
val content = chatItem.content.msgContent as? MsgContent.MCVoice
val duration = content?.duration ?: 0
val senderName = getSenderName(chatItem)
val dateTime = formatDateTimeForList(chatItem.meta.itemTs)
val scope = rememberCoroutineScope()
val user = chatModel.currentUser.value
val receiveFile: (Long) -> Unit = { fileId ->
if (user != null) {
scope.launch {
withBGApi {
chatModel.controller.receiveFile(chat.remoteHostId, user, fileId)
}
}
}
}
SectionItemView(
click = { /* No action - voice playback is handled by CIVoiceView */ },
minHeight = 64.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
) {
Column(Modifier.fillMaxWidth()) {
// Sender name and date row
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
senderName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Medium
)
Text(
dateTime,
fontSize = 11.sp,
color = MaterialTheme.colors.secondary
)
}
Spacer(Modifier.height(8.dp))
CIVoiceView(
providedDurationSec = duration,
file = chatItem.file,
edited = chatItem.meta.itemEdited,
sent = chatItem.chatDir.sent,
hasText = false,
ci = chatItem,
timedMessagesTTL = null,
showViaProxy = false,
showTimestamp = true,
smallView = false,
longClick = { },
receiveFile = receiveFile
)
}
}
}
// Get sender name from chat item
private fun getSenderName(chatItem: ChatItem): String {
return when (val dir = chatItem.chatDir) {
is CIDirection.GroupRcv -> dir.groupMember.chatViewName
is CIDirection.GroupSnd -> "ME" // TODO: Replace with actual sender name
else -> "Sender"
}
}
// Format date/time for list items (e.g., "Dec 20 2022 12:01 PM")
private fun formatDateTimeForList(timestamp: Instant): String {
val tz = TimeZone.currentSystemDefault()
val dateTime = timestamp.toLocalDateTime(tz)
val javaDate = dateTime.toJavaLocalDateTime()
// Convert to milliseconds for SimpleDateFormat
val millis = timestamp.epochSeconds * 1000L + timestamp.nanosecondsOfSecond / 1_000_000
val dateFormat = SimpleDateFormat("MMM dd yyyy hh:mm a", Locale.getDefault())
dateFormat.timeZone = java.util.TimeZone.getDefault()
return dateFormat.format(Date(millis))
}
@Composable
private fun GroupChatItemRow(chatItem: ChatItem) {
Row(
@@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -45,6 +46,8 @@ import chat.simplex.common.views.database.TtlOptions
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
val MEMBER_ROW_AVATAR_SIZE = 42.dp
@@ -463,29 +466,136 @@ fun ModalData.GroupChatInfoLayout(
if (s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) }
}
}
val selectedTab = rememberSaveable { mutableStateOf(GroupInfoTab.Members) }
val filteredChatItems = remember(chat.chatItems, selectedTab.value) {
val selectedTab = remember { stateGetOrPut("selectedTab") { GroupInfoTab.Members } }
// Helper function to map tab to content tag
fun tabToContentTag(tab: GroupInfoTab): MsgContentTag? {
return when (tab) {
GroupInfoTab.Members -> null
GroupInfoTab.Images -> MsgContentTag.Image
GroupInfoTab.Videos -> MsgContentTag.Video
GroupInfoTab.Files -> MsgContentTag.File
GroupInfoTab.Links -> MsgContentTag.Link
GroupInfoTab.Voice -> MsgContentTag.Voice
}
}
// State for storing items per tab and initing with empty list
val tabItemsState = remember { mutableStateMapOf<GroupInfoTab, SnapshotStateList<ChatItem>>() }
val loadingTabItems = remember { mutableStateOf<GroupInfoTab?>(null) }
val tabPaginationState = remember { mutableStateMapOf<GroupInfoTab, ChatPagination?>() }
LaunchedEffect(Unit) {
GroupInfoTab.entries.forEach { tab ->
if (tab != GroupInfoTab.Members && !tabItemsState.containsKey(tab)) {
tabItemsState[tab] = SnapshotStateList()
}
}
}
fun loadTabItems(
tab: GroupInfoTab,
pagination: ChatPagination,
items: SnapshotStateList<ChatItem>,
clearExisting: Boolean
) {
loadingTabItems.value = tab
withBGApi {
val contentTag = tabToContentTag(tab) ?: run {
loadingTabItems.value = null
return@withBGApi
}
val apiResult = chatModel.controller.apiGetChat(
chat.remoteHostId,
chat.chatInfo.chatType,
chat.chatInfo.apiId,
scope = null,
contentTag = contentTag,
pagination = pagination,
search = ""
) ?: run {
loadingTabItems.value = null
return@withBGApi
}
val loadedItems = apiResult.first.chatItems
withContext(Dispatchers.Main) {
// Filter out deleted items and validate content type matches the tab
val filteredItems = loadedItems.filter { item ->
item.meta.itemDeleted == null && when (tab) {
GroupInfoTab.Images -> item.content.msgContent is MsgContent.MCImage
GroupInfoTab.Videos -> item.content.msgContent is MsgContent.MCVideo
GroupInfoTab.Files -> item.content.msgContent is MsgContent.MCFile
GroupInfoTab.Links -> item.content.msgContent is MsgContent.MCLink
GroupInfoTab.Voice -> item.content.msgContent is MsgContent.MCVoice
else -> true
}
}
if (clearExisting) {
items.clear()
items.addAll(filteredItems)
} else {
// Filter out duplicates when appending
val existingIds = items.map { it.id }.toSet()
val newItems = filteredItems.filter { it.id !in existingIds }
items.addAll(newItems)
}
if (clearExisting) {
tabPaginationState[tab] = pagination
}
loadingTabItems.value = null
}
}
}
// Load items when tab is selected
LaunchedEffect(selectedTab.value) {
val tab = selectedTab.value
if (tab == GroupInfoTab.Members) return@LaunchedEffect
val items = tabItemsState[tab] ?: return@LaunchedEffect
// Only load if empty (initial load)
if (items.isNotEmpty()) return@LaunchedEffect
val pagination = ChatPagination.Initial(ChatPagination.PRELOAD_COUNT)
loadTabItems(tab, pagination, items, clearExisting = true)
}
// Get current tab items list and track its size for recomposition
val currentTabItems = remember(selectedTab.value) {
tabItemsState[selectedTab.value]
}
val currentTabItemsSize = remember(currentTabItems) {
currentTabItems?.size ?: 0
}
// Create filteredChatItems from tabItemsState - updates when tab or list size changes
// Using derivedStateOf to observe the SnapshotStateList changes
val filteredChatItems = remember(selectedTab.value, currentTabItemsSize) {
derivedStateOf {
when (selectedTab.value) {
GroupInfoTab.Members -> emptyList()
GroupInfoTab.Images -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCImage && it.meta.itemDeleted == null
}
GroupInfoTab.Videos -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCVideo && it.meta.itemDeleted == null
}
GroupInfoTab.Files -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCFile && it.meta.itemDeleted == null
}
GroupInfoTab.Links -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCLink && it.meta.itemDeleted == null
}
GroupInfoTab.Voice -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCVoice && it.meta.itemDeleted == null
else -> {
// Access the list - SnapshotStateList changes will be observed
val items = tabItemsState[selectedTab.value]
items?.toList() ?: emptyList()
}
}
}
}
val loadMoreTabItems: () -> Unit = loadMoreTabItems@{
val tab = selectedTab.value
if (tab == GroupInfoTab.Members) return@loadMoreTabItems
val items = tabItemsState[tab] ?: return@loadMoreTabItems
val lastItemId = items.lastOrNull()?.id ?: return@loadMoreTabItems
scope.launch(Dispatchers.Default) {
val pagination = ChatPagination.Before(lastItemId, ChatPagination.INITIAL_COUNT)
loadTabItems(tab, pagination, items, clearExisting = false)
}
}
Box {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val selectedItemsBarHeight = if (selectedItems.value != null) AppBarHeight * fontSizeSqrtMultiplier else 0.dp
@@ -567,7 +677,9 @@ fun ModalData.GroupChatInfoLayout(
chat = chat,
groupLink = groupLink,
manageGroupLink = manageGroupLink,
openMemberSupport = openMemberSupport
openMemberSupport = openMemberSupport,
isLoadingTab = loadingTabItems.value == selectedTab.value,
onLoadMoreTabItems = loadMoreTabItems
)
item {
@@ -987,7 +1099,6 @@ fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
)
}
@Composable
fun MemberListSearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
@@ -2774,6 +2774,8 @@
<string name="subscription_results_ignored">Subscriptions ignored</string>
<string name="subscription_errors">Subscription errors</string>
<string name="uploaded_files">Uploaded files</string>
<string name="load_more">Load more</string>
<string name="no_items_found">No items found</string>
<string name="size">Size</string>
<string name="chunks_uploaded">Chunks uploaded</string>
<string name="upload_errors">Upload errors</string>