This commit is contained in:
Diogo
2024-10-16 15:39:41 +01:00
parent 8467d5ff17
commit 1e419b8790
3 changed files with 87 additions and 63 deletions
@@ -13,6 +13,7 @@ struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var scrollModel: ReverseListScrollModel
@EnvironmentObject var sectionModel: ReverseListSectionModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
+16 -38
View File
@@ -28,8 +28,7 @@ struct ChatView: View {
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@StateObject private var scrollModel = ReverseListScrollModel()
@State private var itemSection: [Int64: ChatSection] = [:]
@State private var activeSection: ChatSection = .bottom
@StateObject private var sectionModel = ReverseListSectionModel()
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -370,14 +369,7 @@ struct ChatView: View {
}
}
ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
resetSections()
}
private func resetSections() {
itemSection.removeAll()
im.reversedChatItems.forEach {
itemSection[$0.id] = .bottom
}
sectionModel.resetSections(items: im.reversedChatItems)
}
private func searchToolbar() -> some View {
@@ -433,7 +425,7 @@ struct ChatView: View {
let cInfo = chat.chatInfo
let mergedItems = filtered(im.reversedChatItems)
return GeometryReader { g in
ReverseList(items: mergedItems, scrollState: $scrollModel.state, itemSection: $itemSection, activeSection: $activeSection ) { ci in
ReverseList(items: mergedItems, scrollState: $scrollModel.state, sectionModel: sectionModel) { ci in
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
? voiceNoFrame
@@ -464,7 +456,7 @@ struct ChatView: View {
.onChange(of: searchText) { _ in
Task {
await loadChat(chat: chat, search: searchText)
resetSections()
sectionModel.resetSections(items: im.reversedChatItems)
}
}
.onChange(of: im.itemAdded) { added in
@@ -897,27 +889,11 @@ struct ChatView: View {
if reversedPage.count == 0 {
setBoundary(direction)
} else {
var reversedPageToAppend = Array<ChatItem>()
var targetSection = section
if let sectionIntersectionItem = reversedPage.first(where: { itemSection[$0.id] != nil && itemSection[$0.id] != section }) {
let sectionToDrop = itemSection[sectionIntersectionItem.id] == .bottom ? section : (itemSection[sectionIntersectionItem.id] ?? .destination)
targetSection = itemSection[sectionIntersectionItem.id] == .bottom ? .bottom : section
im.reversedChatItems.forEach {
if itemSection[$0.id] == sectionToDrop {
itemSection[$0.id] = targetSection
}
}
}
reversedPage.forEach { ci in
if itemSection[ci.id] == nil {
reversedPageToAppend.append(ci)
itemSection[ci.id] = targetSection
}
}
self.activeSection = targetSection
let reversedPageToAppend = self.sectionModel.handleSectionInsertion(
candidateSection: section,
reversedPage: reversedPage,
allItems: im.reversedChatItems
)
if direction == .toLatest {
let at = if let chatItemId = chatItem?.id, let index = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) {
@@ -941,7 +917,7 @@ struct ChatView: View {
private func loadItemsAround(_ cInfo: ChatInfo, _ chatItemId: Int64) {
Task {
if loadingItems || itemSection[chatItemId] != nil { return }
if loadingItems || sectionModel.getItemSection(chatItemId) != nil { return }
loadingItems = true
do {
var reversedPage = Array<ChatItem>()
@@ -970,10 +946,12 @@ struct ChatView: View {
reversedPage.append(contentsOf: chatItems.reversed())
}
await MainActor.run {
reversedPage.forEach { ci in
itemSection[ci.id] = .destination
}
im.reversedChatItems.append(contentsOf: reversedPage)
let reversedPageToAppend = self.sectionModel.handleSectionInsertion(
candidateSection: .destination,
reversedPage: reversedPage,
allItems: im.reversedChatItems
)
im.reversedChatItems.append(contentsOf: reversedPageToAppend)
loadingItems = false
}
} catch let error {
+70 -25
View File
@@ -27,8 +27,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
let items: Array<ChatItem>
@Binding var scrollState: ReverseListScrollModel.State
@Binding var itemSection: Dictionary<Int64, ChatSection>
@Binding var activeSection: ChatSection
@ObservedObject var sectionModel: ReverseListSectionModel
/// Closure, that returns user interface for a given item
let content: (ChatItem) -> Content
@@ -96,7 +95,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
) { (tableView, indexPath, item) -> UITableViewCell? in
if let section = self.dataSource.sectionIdentifier(for: indexPath.section) {
let itemCount = self.getTotalItemsInItemSection(indexPath: indexPath)
if self.representer.activeSection == section {
if self.representer.sectionModel.activeSection == section {
if self.scrollDirection == .toOldest, indexPath.item > itemCount - 8, itemCount > 8 {
let lastItem = self.getLastItemInItemSection(indexPath: indexPath)
self.representer.loadPage(.toOldest, section, lastItem)
@@ -197,8 +196,8 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
}
} else {
Task {
if (self.representer.itemSection[id] == nil) {
self.representer.activeSection = .destination
if (self.representer.sectionModel.getItemSection(id) == nil) {
self.representer.sectionModel.activeSection = .destination
self.representer.loadItemsAround(id)
}
}
@@ -229,28 +228,14 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
func update(items: [ChatItem]) {
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]
let sections = self.representer.sectionModel.getSectionsOrdered()
let itemsBySection = self.itemsBySection(items: items)
snapshot.appendSections(sections)
sections.forEach { sec in
if let sectionItems = itemsBySection[sec] {
snapshot.appendItems(sectionItems, toSection: sec)
}
}
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,
@@ -337,6 +322,16 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
nil
}
}
private func itemsBySection(items: [ChatItem]) -> [ChatSection: [ChatItem]] {
let itemsBySection = items.reduce(into: [ChatSection: [ChatItem]]()) { result, ci in
if let sec = self.representer.sectionModel.getItemSection(ci.id) {
result[sec, default: []].append(ci)
}
}
return itemsBySection
}
}
/// `UIHostingConfiguration` back-port for iOS14 and iOS15
@@ -414,6 +409,56 @@ class ReverseListScrollModel: ObservableObject {
}
}
/// Manages ``ReverseList`` sections
class ReverseListSectionModel: ObservableObject {
@Published var activeSection: ChatSection = .bottom
@Published private var itemSection: [Int64: ChatSection] = [:]
@Published private var sections = Set<ChatSection>()
func getItemSection(_ id: Int64) -> ChatSection? {
return itemSection[id]
}
func getSectionsOrdered() -> [ChatSection] {
var orderedSections: [ChatSection] = [.bottom]
if (sections.contains(.destination)) {
orderedSections.append(.destination)
}
return orderedSections
}
func resetSections(items: [ChatItem]) {
itemSection = Dictionary(uniqueKeysWithValues: items.map { ($0.id, .bottom) })
}
func handleSectionInsertion(candidateSection: ChatSection, reversedPage: [ChatItem], allItems: [ChatItem]) -> [ChatItem] {
var reversedPageToAppend = Array<ChatItem>()
var targetSection = candidateSection
if let sectionIntersectionItem = reversedPage.first(where: { itemSection[$0.id] != nil && itemSection[$0.id] != candidateSection }) {
let sectionToDrop = itemSection[sectionIntersectionItem.id] == .bottom ? candidateSection : (itemSection[sectionIntersectionItem.id] ?? .destination)
targetSection = itemSection[sectionIntersectionItem.id] == .bottom ? .bottom : candidateSection
itemSection = itemSection.mapValues { section in
section == sectionToDrop ? targetSection : section
}
sections.remove(targetSection)
}
reversedPage.forEach { ci in
if itemSection[ci.id] == nil {
reversedPageToAppend.append(ci)
itemSection[ci.id] = targetSection
}
}
sections.insert(targetSection)
self.activeSection = targetSection
return reversedPageToAppend
}
}
fileprivate let cellReuseId = "hostingCell"
fileprivate let notificationName = NSNotification.Name(rawValue: "reverseListNeedsLayout")