diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index d7b05d8723..c8a4a42f21 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -52,8 +52,13 @@ class ItemsModel: ObservableObject { static let shared = ItemsModel() private let publisher = ObservableObjectPublisher() private var bag = Set() + var chatItemIds = Set() var reversedChatItems: [ChatItem] = [] { - willSet { publisher.send() } + willSet { + chatItemIds.removeAll() + chatItemIds.formUnion(newValue.map { $0.id }) + publisher.send() + } } var itemAdded = false { willSet { publisher.send() } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 0af81a9be0..074d8b122b 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -346,6 +346,7 @@ struct FramedItemView: View { private func loadItemsAround(_ cInfo: ChatInfo, _ chatItemId: Int64) async -> [ChatItem]? { do { + let im = ItemsModel.shared var reversedPage = Array() let pagination: ChatPagination = .around(chatItemId: chatItemId, count: loadItemsPerPage * 2) let (chatItems, gap) = try await apiGetChatItems( @@ -355,13 +356,15 @@ struct FramedItemView: View { search: "" ) - reversedPage.append(contentsOf: chatItems.reversed()) + let dedupedChatItems = chatItems.filter { !im.chatItemIds.contains($0.id) } + reversedPage.append(contentsOf: dedupedChatItems.reversed()) await MainActor.run { - ItemsModel.shared.reversedChatItems.append(contentsOf: reversedPage) - if let size = gap { - ItemsModel.shared.gap = ChatGap(index: reversedPage.count, size: size) + let itemCount = im.reversedChatItems.count + if let size = gap, size - itemCount > 0 { + im.gap = ChatGap(index: itemCount, size: size - itemCount) } + im.reversedChatItems.append(contentsOf: reversedPage) } return reversedPage diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d233353ad9..fc6aa08384 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -443,8 +443,8 @@ struct ChatView: View { forwardedChatItems: $forwardedChatItems ) .id(ci.id) // Required to trigger `onAppear` on iOS15 - } loadPage: { - loadChatItems(cInfo) + } loadPage: { pagination in + loadChatItems(cInfo, pagination) } .opacity(ItemsModel.shared.isLoading ? 0 : 1) .padding(.vertical, -InvertedTableView.inset) @@ -857,7 +857,7 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo) { + private func loadChatItems(_ cInfo: ChatInfo, _ pagination: ChatPagination = .initial(count: loadItemsPerPage)) { Task { if loadingItems || firstPage { return } loadingItems = true @@ -866,26 +866,50 @@ struct ChatView: View { var chatItemsAvailable = true // Load additional items until the page is +50 large after merging while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage { - let pagination: ChatPagination = - if let lastItem = reversedPage.last ?? im.reversedChatItems.last { - .before(chatItemId: lastItem.id, count: loadItemsPerPage) - } else { - .last(count: loadItemsPerPage) - } + let chatPagination: ChatPagination = switch pagination { + case let .before(chatItemId, count): .before(chatItemId: reversedPage.last?.id ?? chatItemId, count: count) + case let .last(count): .last(count: count) + case let .initial(count): .initial(count: count) + case let .after(chatItemId, count): + .after(chatItemId: reversedPage.first?.id ?? chatItemId, count: count) + case .around(_, _): throw RuntimeError("Unsupported pagination type for loading chat items: \(pagination)") + } + let (chatItems, _) = try await apiGetChatItems( type: cInfo.chatType, id: cInfo.apiId, - pagination: pagination, + pagination: chatPagination, search: searchText ) chatItemsAvailable = !chatItems.isEmpty reversedPage.append(contentsOf: chatItems.reversed()) } + let dedupedreversePage = reversedPage.filter { !im.chatItemIds.contains($0.id) } await MainActor.run { if reversedPage.count == 0 { firstPage = true } else { - im.reversedChatItems.append(contentsOf: reversedPage) + + switch pagination { + case .before, .last, .initial: + im.reversedChatItems.append(contentsOf: dedupedreversePage) + case let .after(chatItemId, _): + let index = im.reversedChatItems.firstIndex { $0.id == chatItemId } + logger.error("[scrolling] setting: \(dedupedreversePage.count) \(index ?? -1)") + + if let index { + if let gap = im.gap { + let size = gap.size - dedupedreversePage.count + if size > 0 { + im.gap = ChatGap(index: gap.index, size: size) + } else { + im.gap = nil + } + } + im.reversedChatItems.insert(contentsOf: dedupedreversePage, at: index) + } + case .around(_, _): break + } } loadingItems = false } diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index cea8b4b47a..2d27f80815 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -20,7 +20,7 @@ struct ReverseList: UIViewControllerRepresentable { /// Closure, that returns user interface for a given item let content: (ChatItem) -> Content - let loadPage: () -> Void + let loadPage: (_ pagination: ChatPagination) -> Void func makeUIViewController(context: Context) -> Controller { Controller(representer: self) @@ -41,7 +41,7 @@ struct ReverseList: UIViewControllerRepresentable { controller.scrollToUnread(to: items.first(where: { $0.id == id })) } } else { - logger.error("[scrolling] not scrolling gap: (\(gap?.index ?? -1), \(gap?.size ?? 0)") + logger.error("[scrolling] not scrolling gap: (\(gap?.index ?? -1), \(gap?.size ?? 0))") controller.update(items: items, gap: gap) } } @@ -85,11 +85,12 @@ struct ReverseList: UIViewControllerRepresentable { if self.representer.scrollState == .atDestination { if indexPath.item > self.itemCount - preloadItem, let item = self.getItemAtPath(indexPath: IndexPath(row: self.itemCount - 1, section: 0)) { logger.error("[scrolling] requesting page \(item.text)") - self.representer.loadPage() + self.representer.loadPage(.before(chatItemId: item.id, count: loadItemsPerPage)) } else if let item = self.getFirstItemBeforePlacholder(indexPath) { - logger.error("[scrolling] needs items in top \(item.text)") + // TODO: Cleanup, this should never be possible } else if let item = self.getFirstItemAfterPlacholder(indexPath) { logger.error("[scrolling] needs items in bottom \(item.text)") + self.representer.loadPage(.after(chatItemId: item.id, count: loadItemsPerPage)) } } let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)