diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift index 2d013ec3a8..06d7f584d9 100644 --- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -11,17 +11,45 @@ import SimpleXChat func loadLastItems(_ loadingMoreItems: Binding, _ chatInfo: ChatInfo) { //return () + if ItemsModel.shared.chatState.totalAfter == 0 { + return + } + loadingMoreItems.wrappedValue = true Task { - try? await Task.sleep(nanoseconds: 2500_000000) - if ItemsModel.shared.chatState.totalAfter == 0 || ChatModel.shared.chatId != chatInfo.id { + try? await Task.sleep(nanoseconds: 1500_000000) + if ChatModel.shared.chatId != chatInfo.id { + await MainActor.run { + loadingMoreItems.wrappedValue = false + } return } - await MainActor.run { - loadingMoreItems.wrappedValue = true - } await apiLoadMessages(chatInfo.chatType, chatInfo.apiId, ChatPagination.last(count: 50), ItemsModel.shared.chatState) await MainActor.run { loadingMoreItems.wrappedValue = false } } } + +func preloadItems(_ mergedItems: MergedItems, _ state: ListState, _ ignoreLoadingRequests: Binding<[Int64]>, _ loadItems: @escaping (ChatPagination) -> Void) { + let remaining = ChatPagination.UNTIL_PRELOAD_COUNT + let firstVisibleIndex = state.firstVisibleItemIndex + + preloadItemsAfter() + + func preloadItemsAfter() { + let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed() + 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) + logger.debug("LALAL PRELOAD AFTER \(String(describing: split)) \(firstVisibleIndex) \(items.count - 1 - (split?.indexRangeInReversed.lowerBound ?? -1))") + if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex { + let index = items.count - 1 - split.indexRangeInReversed.lowerBound + if index >= 0 { + let loadFromItemId = items[index].id + loadItems(.after(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) + } + + } + } + +} diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 2e65ac7a55..74d91967c6 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -47,6 +47,7 @@ struct ChatView: View { @State private var selectedChatItems: Set? = nil @State private var showDeleteSelectedMessages: Bool = false @State private var allowToDeleteSelectedMessagesForAll: Bool = false + @State private var ignoreLoadingRequests: [Int64] = [] @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @@ -429,12 +430,19 @@ struct ChatView: View { .map { $0.element } } + var searchValueIsEmpty: Bool { + get { + searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + } + private func chatItemsList() -> some View { let cInfo = chat.chatInfo return GeometryReader { g in let _ = logger.debug("LALAL RELOAD \(im.reversedChatItems.count)") - ReverseList(items: im.reversedChatItems, mergedItems: $mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state) { index, mergedItem in + // LALAL CAN I CHANGE BINDING LIKE THIS IN ignoreLoadingRequests? + ReverseList(items: im.reversedChatItems, mergedItems: $mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state, ignoreLoadingRequests: searchValueIsEmpty ? $ignoreLoadingRequests : Binding.constant([])) { index, mergedItem in let ci = switch mergedItem { case let .single(item, _): item.item case let .grouped(items, _, _, _, _, _, _): items.boxedValue.last!.item @@ -892,7 +900,7 @@ struct ChatView: View { var chatItemsAvailable = true var itemsCountChanged = false // Load additional items until the page is +50 large after merging - while chatItemsAvailable && filtered(im.reversedChatItems).count < loadItemsPerPage { + //while chatItemsAvailable && filtered(im.reversedChatItems).count < loadItemsPerPage { let oldCount = im.reversedChatItems.count await apiLoadMessages( cInfo.chatType, @@ -904,7 +912,8 @@ struct ChatView: View { ) itemsCountChanged = im.reversedChatItems.count != oldCount chatItemsAvailable = itemsCountChanged - } + //} + logger.debug("LALAL ITEMCOUNTCHANGED \(itemsCountChanged) \(oldCount) \(im.reversedChatItems.count)") await MainActor.run { if !itemsCountChanged { firstPage = true diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 19e2b528f1..1989557798 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -547,6 +547,11 @@ struct ComposeView: View { if case let .voicePreview(_, duration) = composeState.preview { voiceMessageRecordingTime = TimeInterval(duration) } +// Task { +// for i in 201 ... 400 { +// await sendMessageAsync("\(i)", live: false, ttl: nil) +// } +// } } } diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index 1cea99759b..9669a68428 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -18,8 +18,7 @@ struct ReverseList: UIViewControllerRepresentable { @Binding var unreadCount: Int @Binding var scrollState: ReverseListScrollModel.State - - @State private var itemsUpdaterTask: Task? = nil + @Binding var ignoreLoadingRequests: [Int64] /// Closure, that returns user interface for a given item /// Index, merged item @@ -45,26 +44,14 @@ struct ReverseList: UIViewControllerRepresentable { controller.scroll(to: 0, position: .top) } } else { - itemsUpdaterTask?.cancel() // when tableView is dragging and new items are added, scroll position cannot be set correctly // so it's better to just wait until dragging ends - if controller.tableView.isDragging && !controller.tableView.isDecelerating { - DispatchQueue.main.async { - itemsUpdaterTask = Task { - while controller.tableView.isDragging && !controller.tableView.isDecelerating { - do { - try await Task.sleep(nanoseconds: 100_000000) - } catch { - return - } - } - await MainActor.run { - controller.update(items: items) - } - } - + if controller.tableView.isDragging/* && !controller.tableView.isDecelerating*/ { + controller.runBlockOnEndDecelerating = { + controller.update(items: items) } } else { + controller.runBlockOnEndDecelerating = nil controller.update(items: items) } } @@ -88,6 +75,8 @@ struct ReverseList: UIViewControllerRepresentable { private let updateFloatingButtons = PassthroughSubject() private var bag = Set() + var runBlockOnEndDecelerating: (() -> Void)? = nil + private var scrollToRowOnAppear = 0 init(representer: ReverseList) { @@ -113,15 +102,26 @@ struct ReverseList: UIViewControllerRepresentable { ) } + var prevFirstVisible = -1 + // 3. Configure data source self.dataSource = UITableViewDiffableDataSource( tableView: tableView ) { (tableView, indexPath, item) -> UITableViewCell? in - if indexPath.item > self.itemCount - 8 { + //if indexPath.item > self.itemCount - 8 { logger.debug("LALAL ITEM \(indexPath.item)") - let pagination = ChatPagination.last(count: 0) - self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) }) - } + Task { + // do not put it outside Task because it will be stackoverflow error + if let state = self.getListState(), prevFirstVisible != state.firstVisibleItemIndex { + prevFirstVisible = state.firstVisibleItemIndex + logger.debug("LALAL LOADING before") + preloadItems(self.representer.mergedItems, state, self.representer.$ignoreLoadingRequests) { pagination in + logger.debug("LALAL LOADING INSIDE") + self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) }) + } + } + } + //} let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) if #available(iOS 16.0, *) { cell.contentConfiguration = UIHostingConfiguration { self.representer.content(indexPath.item, self.itemsInPrevSnapshot[item]!) } @@ -186,6 +186,11 @@ struct ReverseList: UIViewControllerRepresentable { NotificationCenter.default.post(name: .chatViewWillBeginScrolling, object: nil) } + override public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + runBlockOnEndDecelerating?() + runBlockOnEndDecelerating = nil + } + /// depending on tableView layout phase conditions, it can already have known size or not. Not possible to correctly scroll to required /// item if the size is unknown func scrollToRowWhenKnowSize(_ row: Int) { @@ -253,13 +258,17 @@ struct ReverseList: UIViewControllerRepresentable { func update(items: [ChatItem]) { var prevSnapshot = dataSource.snapshot() let wasCount = prevSnapshot.numberOfItems - let insertedOneNewestItem = wasCount != 0 && representer.mergedItems.items.count - wasCount == 1 && prevSnapshot.itemIdentifiers.first!.hashValue == self.representer.mergedItems.items[1].hashValue + let willBeCount = representer.mergedItems.items.count + let c = 13 + let insertedOneNewestItem = wasCount != 0 && willBeCount - wasCount == c && prevSnapshot.itemIdentifiers.first!.hashValue == self.representer.mergedItems.items[c].hashValue logger.debug("LALAL WAS \(wasCount) will be \(self.representer.mergedItems.items.count)") //self.representer.mergedItems = MergedItems.create(items, representer.unreadCount, representer.revealedItems, ItemsModel.shared.chatState) let snapshot: NSDiffableDataSourceSnapshot let itemsInCurrentSnapshot: Dictionary if insertedOneNewestItem { - prevSnapshot.insertItems([representer.mergedItems.items.first!.hashValue], beforeItem: prevSnapshot.itemIdentifiers.first!) + for i in 0 ..< c { + prevSnapshot.insertItems([representer.mergedItems.items[c - i].hashValue], beforeItem: prevSnapshot.itemIdentifiers.first!) + } var new = itemsInPrevSnapshot new[representer.mergedItems.items.first!.hashValue] = representer.mergedItems.items.first! itemsInCurrentSnapshot = new @@ -305,8 +314,10 @@ struct ReverseList: UIViewControllerRepresentable { } } else if wasCount != snapshot.numberOfItems { logger.debug("LALAL drag \(self.tableView.isDragging), decel \(self.tableView.isDecelerating)") +// tableView.panGestureRecognizer.isEnabled = false // logger.debug("LALAL drag2 \(self.tableView.isDragging), decel \(self.tableView.isDecelerating)") if tableView.isDecelerating { +// CATransaction.begin() itemsInPrevSnapshot = itemsInCurrentSnapshot tableView.beginUpdates() dataSource.apply( @@ -314,16 +325,18 @@ struct ReverseList: UIViewControllerRepresentable { animatingDifferences: false ) tableView.endUpdates() +// CATransaction.commit() + tableView.panGestureRecognizer.isEnabled = true } else { itemsInPrevSnapshot = itemsInCurrentSnapshot dataSource.apply( snapshot, animatingDifferences: false ) - if snapshot.itemIdentifiers[0] == prevSnapshot.itemIdentifiers[0] { + //if snapshot.itemIdentifiers[0] == prevSnapshot.itemIdentifiers[0] { // added new items to top - } else { + //} else { // added new items to bottom // logger.debug("LALAL WAS HEIGHT \(wasContentHeight) now \(self.tableView.contentSize.height), offset was \(wasOffset), now \(self.tableView.contentOffset.y), will be \(self.tableView.contentOffset.y + (self.tableView.contentSize.height - wasContentHeight)), countDiff \(countDiff), wasVisibleRow \(wasFirstVisibleRow), wasFirstVisibleOffset \(wasFirstVisibleOffset)") self.tableView.scrollToRow( @@ -337,10 +350,11 @@ struct ReverseList: UIViewControllerRepresentable { ) let state = getListState()! logger.debug("LALAL NOW FIRST VISIBLE \(state.firstVisibleItemIndex) \(state.firstVisibleItemOffset)") - } + //} } } updateFloatingButtons.send() + return } override func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -348,8 +362,9 @@ struct ReverseList: UIViewControllerRepresentable { } func getListState() -> ListState? { + logger.debug("LALAL VISIBLE ROWS \((self.tableView.indexPathsForVisibleRows ?? []).map({ $0.row }))") if let visibleRows = tableView.indexPathsForVisibleRows, - visibleRows.last?.item ?? 0 < representer.mergedItems.items.count { + visibleRows.last?.row ?? 0 < representer.mergedItems.items.count { let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset let topItemDate: Date? = if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) { @@ -364,11 +379,11 @@ struct ReverseList: UIViewControllerRepresentable { let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) let bottomItemId: ChatItem.ID? = if let firstVisible { - representer.mergedItems.items[firstVisible.item].newest().item.id + representer.mergedItems.items[firstVisible.row].newest().item.id } else { nil } - return ListState(scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId, firstVisibleItemIndex: firstVisible?.item ?? 0, lastVisibleItemIndex: lastVisible?.item ?? 0, firstVisibleItemOffset: firstVisibleOffset ?? 0) + return ListState(scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId, firstVisibleItemIndex: firstVisible?.row ?? 0, lastVisibleItemIndex: lastVisible?.row ?? 0, firstVisibleItemOffset: firstVisibleOffset ?? 0) } return nil } @@ -537,4 +552,16 @@ class InvertedTableView: UITableView { override var adjustedContentInset: UIEdgeInsets { Self.insets } + +// override var contentOffset: CGPoint { +// get { super.contentOffset } +// set { +// logger.debug("LALAL SET OFFSET \(newValue.y)") +// if newValue.y <= 0 { +// return +// } +// super.contentOffset = newValue +// } +// } + } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 927e94d1e5..d9eee1097d 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1183,6 +1183,10 @@ struct NewUser: Encodable { } public enum ChatPagination { + public static let INITIAL_COUNT = 50 + public static let PRELOAD_COUNT = 50 + public static let UNTIL_PRELOAD_COUNT = 50 + case last(count: Int) case after(chatItemId: Int64, count: Int) case before(chatItemId: Int64, count: Int)