mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
jump to quote jumping
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user