mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
* ios: open chat on first unread, "scroll" to quoted items that were not loaded * more changes * changes * unused * fix reveal logic * debug * changes * test * Revert "test" This reverts commit553be124d5
. * change * change * changes * changes * changes * commented deceleration logic * changes * fixes * optimized item identifiers to use merged item directly * fixed counters * encreased initial and preload counters * fix initial loading and trimming items * optimize * allow marking read * 10 instead of 5 * performance * one more parameter in hash * disable trimming * performance * performance - in background * optimization * next/prev * changes * markread * finally * less logs * read * change after merge * trimming, edge cases * wait until items loaded * Revert "wait until items loaded" This reverts commit895218b978
. * progress indicator * optimization * disable scroll helper * experiment * Revert "experiment" This reverts commitc952c9e623
. * jump * no read * layoutIfNeeded * changes * EndlessScrollView * read * changes * changes * changes * reduce time to open a chat (by ~300ms) * open from the first unread when clicking member chat * refactored and removed unused code * handling search emptiness to scroll to correct position * changes * read state maintain * remove protocol * avoid parsing chatId * pass chat * changes * remove reveal * refactor spaghetti * remove ItemsScrollModel * rename * remove setUpdateListener * unused * optimization * scrollToTop * fix * scrollbar working again * scrollToBottom * fix * scrollBar hiding when not many items on screen * small safer change --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
163 lines
6.1 KiB
Swift
163 lines
6.1 KiB
Swift
//
|
|
// ChatScrollHelpers.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Stanislav Dmitrenko on 20.12.2024.
|
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
func loadLastItems(_ loadingMoreItems: Binding<Bool>, _ chat: Chat) {
|
|
if ItemsModel.shared.chatState.totalAfter == 0 {
|
|
return
|
|
}
|
|
loadingMoreItems.wrappedValue = true
|
|
Task {
|
|
try? await Task.sleep(nanoseconds: 1500_000000)
|
|
if ChatModel.shared.chatId != chat.chatInfo.id {
|
|
await MainActor.run {
|
|
loadingMoreItems.wrappedValue = false
|
|
}
|
|
return
|
|
}
|
|
await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState)
|
|
await MainActor.run {
|
|
loadingMoreItems.wrappedValue = false
|
|
}
|
|
}
|
|
}
|
|
|
|
class PreloadState {
|
|
static let shared = PreloadState()
|
|
var prevFirstVisible: Int64 = Int64.min
|
|
var prevItemsCount: Int = 0
|
|
var preloading: Bool = false
|
|
}
|
|
|
|
func preloadIfNeeded(
|
|
_ allowLoadMoreItems: Binding<Bool>,
|
|
_ ignoreLoadingRequests: Binding<Int64?>,
|
|
_ listState: EndlessScrollView<MergedItem>.ListState,
|
|
_ mergedItems: BoxedValue<MergedItems>,
|
|
loadItems: @escaping (Bool, ChatPagination) async -> Bool
|
|
) {
|
|
let state = PreloadState.shared
|
|
guard !listState.isScrolling,
|
|
state.prevFirstVisible != listState.firstVisibleItemIndex || state.prevItemsCount != mergedItems.boxedValue.indexInParentItems.count,
|
|
!state.preloading,
|
|
listState.totalItemsCount > 0
|
|
else {
|
|
return
|
|
}
|
|
state.prevFirstVisible = listState.firstVisibleItemId as! Int64
|
|
state.prevItemsCount = mergedItems.boxedValue.indexInParentItems.count
|
|
state.preloading = true
|
|
let allowLoadMore = allowLoadMoreItems.wrappedValue
|
|
Task {
|
|
defer {
|
|
state.preloading = false
|
|
}
|
|
await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
|
|
await loadItems(false, pagination)
|
|
}
|
|
}
|
|
}
|
|
|
|
func preloadItems(
|
|
_ mergedItems: MergedItems,
|
|
_ allowLoadMoreItems: Bool,
|
|
_ listState: EndlessScrollView<MergedItem>.ListState,
|
|
_ ignoreLoadingRequests: Binding<Int64?>,
|
|
_ loadItems: @escaping (ChatPagination) async -> Bool)
|
|
async {
|
|
let allowLoad = allowLoadMoreItems || mergedItems.items.count == listState.lastVisibleItemIndex + 1
|
|
let remaining = ChatPagination.UNTIL_PRELOAD_COUNT
|
|
let firstVisibleIndex = listState.firstVisibleItemIndex
|
|
|
|
if !(await preloadItemsBefore()) {
|
|
await preloadItemsAfter()
|
|
}
|
|
|
|
func preloadItemsBefore() async -> Bool {
|
|
let splits = mergedItems.splits
|
|
let lastVisibleIndex = listState.lastVisibleItemIndex
|
|
var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits)
|
|
let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed()
|
|
if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining {
|
|
lastIndexToLoadFrom = items.count - 1
|
|
}
|
|
let loadFromItemId: Int64?
|
|
if allowLoad, let lastIndexToLoadFrom {
|
|
let index = items.count - 1 - lastIndexToLoadFrom
|
|
loadFromItemId = index >= 0 ? items[index].id : nil
|
|
} else {
|
|
loadFromItemId = nil
|
|
}
|
|
guard let loadFromItemId, ignoreLoadingRequests.wrappedValue != loadFromItemId else {
|
|
return false
|
|
}
|
|
let sizeWas = items.count
|
|
let firstItemIdWas = items.first?.id
|
|
let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT))
|
|
if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id {
|
|
ignoreLoadingRequests.wrappedValue = loadFromItemId
|
|
}
|
|
return triedToLoad
|
|
}
|
|
|
|
func preloadItemsAfter() async {
|
|
let splits = mergedItems.splits
|
|
let split = splits.last(where: { $0.indexRangeInParentItems.contains(firstVisibleIndex) })
|
|
// we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom)
|
|
let reversedItems: [ChatItem] = ItemsModel.shared.reversedChatItems
|
|
if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex {
|
|
let index = split.indexRangeInReversed.lowerBound
|
|
if index >= 0 {
|
|
let loadFromItemId = reversedItems[index].id
|
|
_ = await loadItems(ChatPagination.after(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func oldestPartiallyVisibleListItemInListStateOrNull(_ listState: EndlessScrollView<MergedItem>.ListState) -> ListItem? {
|
|
if listState.lastVisibleItemIndex < listState.items.count {
|
|
return listState.items[listState.lastVisibleItemIndex].oldest()
|
|
} else {
|
|
return listState.items.last?.oldest()
|
|
}
|
|
}
|
|
|
|
private func findLastIndexToLoadFromInSplits(_ firstVisibleIndex: Int, _ lastVisibleIndex: Int, _ remaining: Int, _ splits: [SplitRange]) -> Int? {
|
|
for split in splits {
|
|
// before any split
|
|
if split.indexRangeInParentItems.lowerBound > firstVisibleIndex {
|
|
if lastVisibleIndex > (split.indexRangeInParentItems.lowerBound - remaining) {
|
|
return split.indexRangeInReversed.lowerBound - 1
|
|
}
|
|
break
|
|
}
|
|
let containsInRange = split.indexRangeInParentItems.contains(firstVisibleIndex)
|
|
if containsInRange {
|
|
if lastVisibleIndex > (split.indexRangeInParentItems.upperBound - remaining) {
|
|
return split.indexRangeInReversed.upperBound
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Disable animation on iOS 15
|
|
func withConditionalAnimation<Result>(
|
|
_ animation: Animation? = .default,
|
|
_ body: () throws -> Result
|
|
) rethrows -> Result {
|
|
if #available(iOS 16.0, *) {
|
|
try withAnimation(animation, body)
|
|
} else {
|
|
try body()
|
|
}
|
|
}
|