fetch both sides poc

This commit is contained in:
Diogo
2024-10-28 22:23:48 +00:00
parent 796d3916bd
commit 53c77d310a
4 changed files with 218 additions and 63 deletions
@@ -11,6 +11,7 @@ import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ChatSectionArea
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrationToDeviceState
@@ -65,6 +66,7 @@ object ChatModel {
// current chat
val chatId = mutableStateOf<String?>(null)
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
var chatItemsSectionArea = mutableMapOf<Long, ChatSectionArea>()
// rhId, chatId
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
@@ -2,7 +2,27 @@ package chat.simplex.common.views.chat
import androidx.compose.runtime.snapshots.SnapshotStateList
import chat.simplex.common.model.*
import java.util.UUID
import chat.simplex.common.platform.chatModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
enum class ChatSectionArea {
Bottom,
Current,
Destination
}
data class ChatSectionAreaBoundary (
var minIndex: Int,
var maxIndex: Int,
val area: ChatSectionArea
)
data class ChatSection (
val items: MutableList<SectionItems>,
val area: ChatSectionArea,
val boundary: ChatSectionAreaBoundary
)
data class SectionItems (
val mergeCategory: CIMergeCategory?,
@@ -18,9 +38,9 @@ fun SectionItems.getPreviousChatItem(chatItem: ChatItem): ChatItem? {
return items.getOrNull(itemIndex + 1)
}
fun List<ChatItem>.putIntoSections(revealedItems: Set<Long>): List<SectionItems> {
val sections = mutableListOf<SectionItems>()
fun List<ChatItem>.putIntoSections(revealedItems: Set<Long>): List<ChatSection> {
val chatItemsSectionArea = chatModel.chatItemsSectionArea
val sections = mutableListOf<ChatSection>()
var recent: SectionItems = if (isNotEmpty()) {
val first = this[0]
@@ -47,7 +67,15 @@ fun List<ChatItem>.putIntoSections(revealedItems: Set<Long>): List<SectionItems>
return emptyList()
}
sections.add(recent)
val area = chatItemsSectionArea[recent.items[0].id] ?: ChatSectionArea.Bottom
sections.add(
ChatSection(
items = mutableListOf(recent),
area = area,
boundary = ChatSectionAreaBoundary(minIndex = 0, maxIndex = 0, area = area)
)
)
var prev = this[0]
var index = 0
@@ -57,27 +85,47 @@ fun List<ChatItem>.putIntoSections(revealedItems: Set<Long>): List<SectionItems>
continue
}
val item = this[index]
val category = item.mergeCategory
if (recent.mergeCategory == category) {
if (item.chatDir is CIDirection.GroupRcv && prev.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember != (prev.chatDir as CIDirection.GroupRcv).groupMember) {
recent.showAvatar.add(item.id)
}
val itemArea = chatItemsSectionArea[item.id] ?: ChatSectionArea.Bottom
val existingSection = sections.find { it.area == itemArea }
recent.items.add(item)
recent.itemPositions[item.id] = index
} else {
recent = SectionItems(
if (existingSection == null) {
val newSection = SectionItems(
mergeCategory = item.mergeCategory,
items = SnapshotStateList<ChatItem>().also { it.add(item) },
revealed = item.mergeCategory == null || revealedItems.contains(item.id),
showAvatar = mutableSetOf<Long>().also {
if (item.chatDir is CIDirection.GroupRcv && (prev.chatDir !is CIDirection.GroupRcv || (prev.chatDir as CIDirection.GroupRcv).groupMember != item.chatDir.groupMember)) {
it.add(item.id)
}
it.add(item.id)
},
itemPositions = mutableMapOf(item.id to index),
)
sections.add(recent)
sections.add(
ChatSection(items = mutableListOf(newSection), area = itemArea, boundary = ChatSectionAreaBoundary(minIndex = index, maxIndex = index, area = itemArea))
)
} else {
recent = existingSection.items.last()
val category = item.mergeCategory
if (recent.mergeCategory == category) {
if (item.chatDir is CIDirection.GroupRcv && prev.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember != (prev.chatDir as CIDirection.GroupRcv).groupMember) {
recent.showAvatar.add(item.id)
}
recent.items.add(item)
recent.itemPositions[item.id] = index
} else {
val newSectionItems = SectionItems(
mergeCategory = item.mergeCategory,
items = SnapshotStateList<ChatItem>().also { it.add(item) },
revealed = item.mergeCategory == null || revealedItems.contains(item.id),
showAvatar = mutableSetOf<Long>().also {
if (item.chatDir is CIDirection.GroupRcv && (prev.chatDir !is CIDirection.GroupRcv || (prev.chatDir as CIDirection.GroupRcv).groupMember != item.chatDir.groupMember)) {
it.add(item.id)
}
},
itemPositions = mutableMapOf(item.id to index),
)
existingSection.items.add(newSectionItems)
}
existingSection.boundary.maxIndex = index
}
prev = item
index++
@@ -86,9 +134,44 @@ fun List<ChatItem>.putIntoSections(revealedItems: Set<Long>): List<SectionItems>
return sections
}
suspend fun apiLoadMessagesAroundItem(chatInfo: ChatInfo, chatModel: ChatModel, aroundItemId: Long, rhId: Long?) {
val pagination = ChatPagination.Around(aroundItemId, ChatPagination.PRELOAD_COUNT)
data class ChatSectionLoad (
val position: Int,
val sectionArea: ChatSectionArea
) {
fun prepareItems(items: List<ChatItem>): List<ChatItem> {
val chatItemsSectionArea = chatModel.chatItemsSectionArea
val itemsToAdd = mutableListOf<ChatItem>()
for (cItem in items) {
val itemSectionArea = chatItemsSectionArea[cItem.id]
if (itemSectionArea == null) {
itemsToAdd.add(cItem)
} else if (itemSectionArea != this.sectionArea) {
val targetSection = when (itemSectionArea) {
ChatSectionArea.Bottom -> ChatSectionArea.Bottom
ChatSectionArea.Current -> if (this.sectionArea == ChatSectionArea.Bottom) ChatSectionArea.Bottom else ChatSectionArea.Current
ChatSectionArea.Destination -> if (this.sectionArea == ChatSectionArea.Bottom) ChatSectionArea.Bottom else ChatSectionArea.Destination
}
chatItemsSectionArea.filter { it.value == itemSectionArea }
.forEach { chatItemsSectionArea[it.key] = targetSection }
}
}
println("prepareItems ${itemsToAdd.size} ${sectionArea.name}")
chatItemsSectionArea.putAll(itemsToAdd.associate { it.id to sectionArea })
return itemsToAdd
}
}
suspend fun apiLoadMessagesAroundItem(chatInfo: ChatInfo, chatModel: ChatModel, aroundItemId: Long, rhId: Long?, chatSectionLoad: ChatSectionLoad) {
val pagination = ChatPagination.Around(aroundItemId, ChatPagination.PRELOAD_COUNT * 2)
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, pagination) ?: return
if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.addAll(0, chat.chatItems)
withContext(Dispatchers.Main) {
val itemsToAdd = chatSectionLoad.prepareItems(chat.chatItems)
println("prepared items ${itemsToAdd.size}")
if (itemsToAdd.isNotEmpty()) {
chatModel.chatItems.addAll(chatSectionLoad.position, itemsToAdd)
}
}
}
@@ -95,6 +95,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
showSearch.value = false
searchText.value = ""
selectedChatItems.value = null
chatModel.chatItemsSectionArea = mutableMapOf<Long, ChatSectionArea>().also { it.putAll(chatModel.chatItems.value.associate { it.id to ChatSectionArea.Bottom }) }
}
}
}
@@ -290,19 +291,35 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
}
}
},
loadMessages = { chatId, scrollDirection ->
loadMessages = { chatId, scrollDirection, section ->
val c = chatModel.getChat(chatId)
if (chatModel.chatId.value != chatId) return@ChatLayout
when (scrollDirection) {
ScrollDirection.Up -> {
val firstId = chatModel.chatItems.value.firstOrNull()?.id
println("requesting up ${section.area}")
val firstSectionItemIdx = chatModel.chatItems.size - 1 - section.maxIndex
val firstId = chatModel.chatItems.value.getOrNull(firstSectionItemIdx)?.id
if (c != null && firstId != null) {
withBGApi {
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
val chatSectionLoad = ChatSectionLoad(firstSectionItemIdx, section.area)
apiLoadPrevMessages(c, chatModel, firstId, searchText.value, chatSectionLoad)
}
}
}
ScrollDirection.Down -> {
println("requesting down ${section.area}")
val lastSectionItemIdx = chatModel.chatItems.size - 1 - section.minIndex
val lastId = chatModel.chatItems.value.getOrNull(lastSectionItemIdx)?.id
if (c != null && lastId != null) {
withBGApi {
val chatSectionLoad = ChatSectionLoad(lastSectionItemIdx, section.area)
apiLoadAfterMessages(c, chatModel, lastId, searchText.value, chatSectionLoad)
}
}
}
ScrollDirection.Down -> {}
else -> {}
}
},
@@ -610,7 +627,7 @@ fun ChatLayout(
back: () -> Unit,
info: () -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadMessages: (ChatId, ScrollDirection) -> Unit,
loadMessages: (ChatId, ScrollDirection, ChatSectionAreaBoundary) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
receiveFile: (Long) -> Unit,
@@ -638,7 +655,7 @@ fun ChatLayout(
onComposed: suspend (chatId: String) -> Unit,
developerTools: Boolean,
showViaProxy: Boolean,
showSearch: MutableState<Boolean>
showSearch: MutableState<Boolean>,
) {
val scope = rememberCoroutineScope()
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
@@ -719,7 +736,7 @@ fun ChatLayout(
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy
)
}
}
@@ -950,7 +967,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadMessages: (ChatId, ScrollDirection) -> Unit,
loadMessages: (ChatId, ScrollDirection, ChatSectionAreaBoundary) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
receiveFile: (Long) -> Unit,
@@ -972,7 +989,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: suspend (chatId: String) -> Unit,
developerTools: Boolean,
showViaProxy: Boolean
showViaProxy: Boolean,
) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
@@ -989,27 +1006,42 @@ fun BoxWithConstraintsScope.ChatItemsList(
scope.launch { listState.scrollToItem(0) }
}
}
val preloadItems = remember { mutableStateOf(true) }
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, preloadItems.value, loadMessages)
Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
val revealedItems = rememberSaveable { mutableStateOf(setOf<Long>()) }
val sections by remember { derivedStateOf { (reversedChatItems).putIntoSections(revealedItems.value) } }
val preloadItems = remember { mutableStateOf(true) }
val boundaries = remember { derivedStateOf { sections.map { it.boundary } } }
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, preloadItems, boundaries, loadMessages)
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val animatedScrollToIndex: (Int) -> Unit = { idx ->
withBGApi {
scope.launch {
listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, idx + 1), -maxHeightRounded)
}.also {
it.join()
preloadItems.value = true
}
}
}
val scrollToItem: (Long) -> Unit = { itemId: Long ->
val index = reversedChatItems.indexOfFirst { it.id == itemId }
println("here")
preloadItems.value = false
if (index != -1) {
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) }
animatedScrollToIndex(index)
} else {
preloadItems.value = false
withBGApi {
try {
apiLoadMessagesAroundItem(rhId = remoteHostId, chatModel = chatModel, chatInfo = chatInfo, aroundItemId = itemId)
// TODO: If this section exists it should be removed.
val chatSectionLoad = ChatSectionLoad(0, ChatSectionArea.Destination)
apiLoadMessagesAroundItem(rhId = remoteHostId, chatModel = chatModel, chatInfo = chatInfo, aroundItemId = itemId, chatSectionLoad = chatSectionLoad)
val idx = reversedChatItems.indexOfFirst { it.id == itemId }
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, idx + 1), -maxHeightRounded) }
} finally {
animatedScrollToIndex(idx)
} catch (ex: Exception) {
preloadItems.value = true
}
}
@@ -1259,16 +1291,18 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
for (section in sections) {
if (section.revealed) {
itemsIndexed(section.items, key = { _, item -> (item.id to item.meta.createdAt.toEpochMilliseconds()).toString() }) { i, cItem ->
// index here is just temporary, should be removed at all or put in the section items
ChatViewListItem(section.itemPositions[cItem.id] ?: -1, section = section, showAvatar = section.showAvatar.contains(cItem.id), cItem) }
} else {
val item = section.items.last()
item(key = { (item.id to item.meta.createdAt.toEpochMilliseconds()).toString() }) {
// here you make one collapsed item from multiple items (should be already in section items)
ChatViewListItem(section.itemPositions[item.id] ?: -1, section = section, showAvatar = section.showAvatar.contains(item.id), item)
for (area in sections) {
for (section in area.items) {
if (section.revealed) {
itemsIndexed(section.items, key = { _, item -> (item.id to item.meta.createdAt.toEpochMilliseconds()).toString() }) { i, cItem ->
// index here is just temporary, should be removed at all or put in the section items
ChatViewListItem(section.itemPositions[cItem.id] ?: -1, section = section, showAvatar = section.showAvatar.contains(cItem.id), cItem) }
} else {
val item = section.items.last()
item(key = { (item.id to item.meta.createdAt.toEpochMilliseconds()).toString() }) {
// here you make one collapsed item from multiple items (should be already in section items)
ChatViewListItem(section.itemPositions[item.id] ?: -1, section = section, showAvatar = section.showAvatar.contains(item.id), item)
}
}
}
}
@@ -1437,8 +1471,9 @@ fun PreloadItems(
chatId: String,
listState: LazyListState,
remaining: Int = 10,
enabled: Boolean,
onLoadMore: (ChatId, ScrollDirection) -> Unit,
enabled: State<Boolean>,
boundaries: State<List<ChatSectionAreaBoundary>>,
onLoadMore: (ChatId, ScrollDirection, ChatSectionAreaBoundary) -> Unit,
) {
// Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection
val allowLoad = remember { mutableStateOf(false) }
@@ -1479,16 +1514,32 @@ fun PreloadItems(
val lInfo = listState.layoutInfo
val totalItemsNumber = lInfo.totalItemsCount
val lastVisibleItemIndex = (lInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
if (allowLoad.value && lastVisibleItemIndex > (totalItemsNumber - remaining) && totalItemsNumber >= ChatPagination.INITIAL_COUNT)
totalItemsNumber + ChatPagination.PRELOAD_COUNT
else
0
}
.filter { it > 0 }
.collect {
if (enabled) {
onLoadMore.value(chatId.value, scrollDirection)
println("boundaries: $boundaries")
val section = if (scrollDirection == ScrollDirection.Up) {
boundaries.value.find { listState.layoutInfo.visibleItemsInfo.lastIndex in it.minIndex..it.maxIndex }
} else {
boundaries.value.find { listState.firstVisibleItemIndex in it.minIndex..it.maxIndex }
}
if (allowLoad.value && section != null && enabled.value) {
val numberOfItemsInSection = section.maxIndex.minus(section.minIndex) + 1
println("checking: $numberOfItemsInSection $section")
if (scrollDirection == ScrollDirection.Up && lastVisibleItemIndex > (section.maxIndex - remaining) && numberOfItemsInSection >= ChatPagination.INITIAL_COUNT) {
section
} else if (scrollDirection == ScrollDirection.Down && listState.firstVisibleItemIndex < (section.minIndex + remaining) && totalItemsNumber > remaining) {
section
} else {
null
}
} else {
null
}
}
.filterNotNull()
.collect {
onLoadMore.value(chatId.value, scrollDirection, it)
}
}
}
@@ -2141,7 +2192,7 @@ fun PreviewChatLayout() {
back = {},
info = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
deleteMessages = { _ -> },
receiveFile = { _ -> },
@@ -2213,7 +2264,7 @@ fun PreviewGroupChatLayout() {
back = {},
info = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
deleteMessages = {},
receiveFile = { _ -> },
@@ -32,6 +32,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock
@Composable
@@ -230,12 +231,30 @@ fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatId.value = chat.chatInfo.id
}
suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String, chatSectionLoad: ChatSectionLoad) {
val chatInfo = ch.chatInfo
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.addAll(0, chat.chatItems)
withContext(Dispatchers.Main) {
val itemsToAdd = chatSectionLoad.prepareItems(chat.chatItems)
if (itemsToAdd.isNotEmpty()) {
chatModel.chatItems.addAll(chatSectionLoad.position, itemsToAdd)
}
}
}
suspend fun apiLoadAfterMessages(ch: Chat, chatModel: ChatModel, afterChatItemId: Long, search: String, chatSectionLoad: ChatSectionLoad) {
val chatInfo = ch.chatInfo
val pagination = ChatPagination.After(afterChatItemId, ChatPagination.PRELOAD_COUNT)
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
if (chatModel.chatId.value != chat.id) return
withContext(Dispatchers.Main) {
val itemsToAdd = chatSectionLoad.prepareItems(chat.chatItems)
if (itemsToAdd.isNotEmpty()) {
chatModel.chatItems.addAll(chatSectionLoad.position, itemsToAdd)
}
}
}
suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {