jump to quote jumping

This commit is contained in:
Diogo
2024-10-14 23:07:24 +01:00
parent 095cc286de
commit 9ee1d8d0b9
3 changed files with 114 additions and 17 deletions
@@ -49,9 +49,9 @@ struct FramedItemView: View {
if let qi = chatItem.quotedItem {
ciQuoteView(qi)
.onTapGesture {
if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) {
if let itemId = qi.itemId {
withAnimation {
scrollModel.scrollToItem(id: ci.id)
scrollModel.scrollToItem(id: itemId)
}
}
}
+55 -1
View File
@@ -23,6 +23,7 @@ struct ChatView: View {
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@StateObject private var scrollModel = ReverseListScrollModel()
@State private var itemSection: Dictionary<Int64, ChatSection> = Dictionary()
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -418,7 +419,7 @@ struct ChatView: View {
let cInfo = chat.chatInfo
let mergedItems = filtered(im.reversedChatItems)
return GeometryReader { g in
ReverseList(items: mergedItems, scrollState: $scrollModel.state) { ci in
ReverseList(items: mergedItems, scrollState: $scrollModel.state, itemSection: $itemSection ) { ci in
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
? voiceNoFrame
@@ -440,6 +441,8 @@ struct ChatView: View {
.id(ci.id) // Required to trigger `onAppear` on iOS15
} loadPage: { direction in
loadChatItems(cInfo, direction)
} loadItemsAround: { chatItemId in
loadItemsAround(cInfo, chatItemId)
}
.opacity(ItemsModel.shared.isLoading ? 0 : 1)
.padding(.vertical, -InvertedTableView.inset)
@@ -870,6 +873,57 @@ struct ChatView: View {
if reversedPage.count == 0 {
firstPage = true
} else {
reversedPage.forEach { ci in
itemSection[ci.id] = .bottom
}
im.reversedChatItems.append(contentsOf: reversedPage)
}
loadingItems = false
}
} catch let error {
logger.error("apiGetChat error: \(responseError(error))")
await MainActor.run { loadingItems = false }
}
}
}
private func loadItemsAround(_ cInfo: ChatInfo, _ chatItemId: Int64) {
Task {
if loadingItems { return }
loadingItems = true
do {
var reversedPage = Array<ChatItem>()
var chatItemsAvailable = true
// Load additional items until the page is +50 large after merging
while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage {
let pagination: ChatPagination = .before(chatItemId: chatItemId, count: loadItemsPerPage / 2)
var chatItems = try await apiGetChatItems(
type: cInfo.chatType,
id: cInfo.apiId,
pagination: pagination,
search: searchText
)
if let lastItem = chatItems.last {
let pagination: ChatPagination = .after(chatItemId: lastItem.id, count: loadItemsPerPage / 2)
let afterItems = try await apiGetChatItems(
type: cInfo.chatType,
id: cInfo.apiId,
pagination: pagination,
search: searchText
)
chatItems.append(contentsOf: afterItems)
}
chatItemsAvailable = !chatItems.isEmpty
reversedPage.append(contentsOf: chatItems.reversed())
}
await MainActor.run {
if reversedPage.count == 0 {
firstPage = true
} else {
reversedPage.forEach { ci in
itemSection[ci.id] = .destination
}
im.reversedChatItems.append(contentsOf: reversedPage)
}
loadingItems = false
+57 -14
View File
@@ -16,17 +16,25 @@ enum ChatScrollDirection {
case none
}
enum ChatSection: CaseIterable {
case main
case destination
case bottom
}
/// A List, which displays it's items in reverse order - from bottom to top
struct ReverseList<Content: View>: UIViewControllerRepresentable {
let items: Array<ChatItem>
@Binding var scrollState: ReverseListScrollModel.State
@Binding var itemSection: Dictionary<Int64, ChatSection>
/// Closure, that returns user interface for a given item
let content: (ChatItem) -> Content
let loadPage: (ChatScrollDirection) -> Void
let loadItemsAround: (Int64) -> Void
func makeUIViewController(context: Context) -> Controller {
Controller(representer: self)
}
@@ -39,9 +47,10 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
case .nextPage:
controller.scrollToNextPage()
case let .item(id):
controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom)
controller.update(items: items)
controller.scrollToItem(id: id)
case .bottom:
controller.scroll(to: 0, position: .top)
controller.scroll(to: 0, position: .top, section: .bottom)
}
} else {
controller.update(items: items)
@@ -50,9 +59,8 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
/// Controller, which hosts SwiftUI cells
class Controller: UITableViewController {
private enum Section { case main }
var representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
private var dataSource: UITableViewDiffableDataSource<ChatSection, ChatItem>!
private var itemCount: Int = 0
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>()
@@ -79,12 +87,12 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
forCellReuseIdentifier: cellReuseId
)
}
// 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
self.dataSource = UITableViewDiffableDataSource<ChatSection, ChatItem>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
if self.representer.scrollState == .atDestination, indexPath.item > self.itemCount - 8, self.itemCount > 8 {
self.representer.loadPage(.toOldest)
}
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
@@ -167,17 +175,32 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
)
Task { representer.scrollState = .atDestination }
}
/// Scrolls up
func scrollToItem(id: Int64) {
if let loadedIndex = representer.items.firstIndex(where: { $0.id == id }) {
let ci = representer.items[loadedIndex]
if let indexPath = dataSource.indexPath(for: ci),
let section = dataSource.sectionIdentifier(for: indexPath.section) {
self.scroll(to: indexPath.row, position: .bottom, section: section)
}
} else {
Task {
self.representer.loadItemsAround(id)
}
}
}
/// Scrolls to Item at index path
/// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil`
func scroll(to index: Int?, position: UITableView.ScrollPosition) {
func scroll(to index: Int?, position: UITableView.ScrollPosition, section: ChatSection) {
var animated = false
if #available(iOS 16.0, *) {
animated = true
}
if let index, tableView.numberOfRows(inSection: 0) != 0 {
if let index, let sectionIndex = dataSource.index(for: section), tableView.numberOfRows(inSection: sectionIndex) != 0 {
tableView.scrollToRow(
at: IndexPath(row: index, section: 0),
at: IndexPath(row: index, section: sectionIndex),
at: position,
animated: animated
)
@@ -191,9 +214,29 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
func update(items: [ChatItem]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
var snapshot = NSDiffableDataSourceSnapshot<ChatSection, ChatItem>()
var itemsBySection: Dictionary<ChatSection, [ChatItem]> = Dictionary()
items.forEach { ci in
var section: ChatSection = .bottom
if let sec = representer.itemSection[ci.id] {
section = sec
}
if var existingItems = itemsBySection[section] {
existingItems.append(ci)
itemsBySection[section] = existingItems
} else {
itemsBySection[section] = [ci]
}
}
snapshot.appendSections([.bottom, .destination])
if let bottomItems = itemsBySection[.bottom] {
snapshot.appendItems(bottomItems, toSection: .bottom)
}
if let destinationItems = itemsBySection[.destination] {
snapshot.appendItems(destinationItems, toSection: .destination)
}
dataSource.defaultRowAnimation = .none
dataSource.apply(
snapshot,