From 7631485796d17a34db2fdf0a548877c05a39dbbb Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:27:11 -0800 Subject: [PATCH] more changes --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- .../Shared/Views/Chat/ChatItemsMerger.swift | 34 ++-- apps/ios/Shared/Views/Chat/ChatView.swift | 168 +++++++++--------- apps/ios/Shared/Views/Chat/ReverseList.swift | 39 ++-- apps/ios/SimpleXChat/API.swift | 3 +- apps/ios/SimpleXChat/APITypes.swift | 7 +- 6 files changed, 142 insertions(+), 111 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 02cbd81a25..7a418f8b44 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -322,7 +322,7 @@ let loadItemsPerPage = 50 func apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) { let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search)) - if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo) } + if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) } throw r } diff --git a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift index c0b3f94d90..82223cdfce 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -15,7 +15,7 @@ struct MergedItems { // chat item id, index in list let indexInParentItems: Dictionary - static func create(_ items: [ChatItem], _ unreadCount: Binding, _ revealedItems: Set, _ chatState: ActiveChatState) -> MergedItems { + static func create(_ items: [ChatItem], _ unreadCount: Int, _ revealedItems: Set, _ chatState: ActiveChatState) -> MergedItems { if items.isEmpty { return MergedItems(items: [], splits: [], indexInParentItems: [:]) } @@ -30,7 +30,7 @@ struct MergedItems { var unclosedSplitIndex: Int? = nil var unclosedSplitIndexInParent: Int? = nil var visibleItemIndexInParent = -1 - var unreadBefore = unreadCount.wrappedValue - chatState.unreadAfterNewestLoaded + var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil var lastRangeInReversedForMergedItems: BoxedValue>? = nil var recent: MergedItem? = nil @@ -42,14 +42,14 @@ struct MergedItems { let itemIsSplit = itemSplits.contains(item.id) if item.id == unreadAfterItemId { - unreadBefore = unreadCount.wrappedValue - chatState.unreadAfter + unreadBefore = unreadCount - chatState.unreadAfter } if item.isRcvNew { unreadBefore -= 1 } let revealed = item.mergeCategory == nil || revealedItems.contains(item.id) - if recent != nil, case let .grouped(items, _, _, _, mergeCategory, unreadIds, _) = recent, mergeCategory == category, let first = items.first, !revealedItems.contains(first.item.id) && !itemIsSplit { + if recent != nil, case let .grouped(items, _, _, _, mergeCategory, _, _) = recent, mergeCategory == category, let first = items.first, !revealedItems.contains(first.item.id) && !itemIsSplit { let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore) recent!.appendItem(listItem) @@ -67,11 +67,11 @@ struct MergedItems { let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore) if item.mergeCategory != nil { if item.mergeCategory != prev?.mergeCategory || lastRevealedIdsInMergedItems == nil { - lastRevealedIdsInMergedItems?.boxedValue = revealedItems.contains(item.id) ? [item.id] : [] - } else if revealed, var lastRevealedIdsInMergedItems { + lastRevealedIdsInMergedItems = BoxedValue(revealedItems.contains(item.id) ? [item.id] : []) + } else if revealed, let lastRevealedIdsInMergedItems { lastRevealedIdsInMergedItems.boxedValue.append(item.id) } - lastRangeInReversedForMergedItems?.boxedValue = index ... index + lastRangeInReversedForMergedItems = BoxedValue(index ... index) recent = MergedItem.grouped( items: [listItem], revealed: revealed, @@ -114,7 +114,7 @@ struct MergedItems { } -enum MergedItem { +enum MergedItem: Hashable { // the item that is always single, cannot be grouped and always revealed case single( item: ListItem, @@ -158,8 +158,8 @@ enum MergedItem { } } - func reveal(reveal: Bool, revealedItems: Binding>) { - if case .grouped(let items, let revealed, var revealedIdsWithinGroup, let rangeInReversed, let mergeCategory, let unreadIds, let startIndexInReversedItems) = self { + func reveal(_ reveal: Bool, _ revealedItems: Binding>) { + if case .grouped(let items, _, let revealedIdsWithinGroup, _, _, _, _) = self { var newRevealed = revealedItems.wrappedValue var i = 0 if reveal { @@ -226,7 +226,7 @@ struct SplitRange { let indexRangeInParentItems: ClosedRange } -struct ListItem { +struct ListItem: Hashable { let item: ChatItem let prevItem: ChatItem? let nextItem: ChatItem? @@ -278,7 +278,15 @@ class ActiveChatState { } } -class BoxedValue { +class BoxedValue: Hashable { + static func == (lhs: BoxedValue, rhs: BoxedValue) -> Bool { + lhs.boxedValue == rhs.boxedValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(self)") + } + var boxedValue : T init(_ value: T) { self.boxedValue = value @@ -286,7 +294,7 @@ class BoxedValue { } extension ReverseList.Controller { - func visibleItemIndexesNonReversed(_ mergedItems: Binding, _ scrollModel: ReverseListScrollModel) -> ClosedRange { + func visibleItemIndexesNonReversed(_ mergedItems: Binding) -> ClosedRange { let zero = 0 ... 0 if itemCount == 0 { return zero diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ff3d00a979..350bc58d03 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -16,6 +16,7 @@ private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var im = ItemsModel.shared + @State var revealedItems: Set = Set() @State var theme: AppTheme = buildTheme() @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @@ -32,7 +33,6 @@ struct ChatView: View { @State private var connectionCode: String? @State private var loadingItems = false @State private var firstPage = false - @State private var revealedChatItem: ChatItem? @State private var searchMode = false @State private var searchText: String = "" @FocusState private var searchFocussed @@ -168,11 +168,13 @@ struct ChatView: View { } .onAppear { selectedChatItems = nil + revealedItems = Set() initChatView() } .onChange(of: chatModel.chatId) { cId in showChatInfoSheet = false selectedChatItems = nil + revealedItems = Set() scrollModel.scrollToBottom() stopAudioPlayer() if let cId { @@ -185,14 +187,20 @@ struct ChatView: View { dismiss() } } - .onChange(of: revealedChatItem) { _ in + .onChange(of: revealedItems) { _ in NotificationCenter.postReverseListNeedsLayout() } .onChange(of: im.isLoading) { isLoading in if !isLoading, im.reversedChatItems.count <= loadItemsPerPage, filtered(im.reversedChatItems).count < 10 { - loadChatItems(chat.chatInfo) + let pagination: ChatPagination = + if let lastItem = im.reversedChatItems.last { + .before(chatItemId: lastItem.id, count: loadItemsPerPage) + } else { + .last(count: loadItemsPerPage) + } + loadChatItems(chat.chatInfo, pagination) } } .environmentObject(scrollModel) @@ -425,7 +433,11 @@ 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, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state) { index, mergedItem in + let ci = switch mergedItem { + case let .single(item, _): item.item + case let .grouped(items, _, _, _, _, _, _): items.last!.item + } let voiceNoFrame = voiceWithoutFrame(ci) let maxWidth = cInfo.chatType == .group ? voiceNoFrame @@ -435,19 +447,22 @@ struct ChatView: View { ? (g.size.width - 32) : (g.size.width - 32) * 0.84 return ChatItemWithMenu( + index: index, + isLastItem: index == mergedItems.items.count - 1, chat: $chat, chatItem: ci, + item: mergedItem, maxWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, showChatInfoSheet: $showChatInfoSheet, - revealedChatItem: $revealedChatItem, + revealedItems: $revealedItems, selectedChatItems: $selectedChatItems, forwardedChatItems: $forwardedChatItems ) .id(ci.id) // Required to trigger `onAppear` on iOS15 - } loadPage: { - loadChatItems(cInfo) + } loadItems: { pagination, visibleItemIndexesNonReversed in + loadChatItems(cInfo, pagination, visibleItemIndexesNonReversed) } .opacity(ItemsModel.shared.isLoading ? 0 : 1) .padding(.vertical, -InvertedTableView.inset) @@ -847,41 +862,31 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo) { + private func loadChatItems(_ cInfo: ChatInfo, _ pagination: ChatPagination, _ visibleItemIndexesNonReversed: @escaping () -> ClosedRange = { 0 ... 0 }) { Task { if loadingItems || firstPage { return } loadingItems = true - do { - var reversedPage = Array() - 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 chatItems = try await apiGetChatItems( - type: cInfo.chatType, - id: cInfo.apiId, - pagination: pagination, - search: searchText - ) - chatItemsAvailable = !chatItems.isEmpty - reversedPage.append(contentsOf: chatItems.reversed()) + var chatItemsAvailable = true + var itemsCountChanged = false + // Load additional items until the page is +50 large after merging + while chatItemsAvailable && filtered(im.reversedChatItems).count < loadItemsPerPage { + let oldCount = im.reversedChatItems.count + await apiLoadMessages( + cInfo.chatType, + cInfo.apiId, + pagination, + im.chatState, + searchText, + visibleItemIndexesNonReversed + ) + itemsCountChanged = im.reversedChatItems.count != oldCount + chatItemsAvailable = itemsCountChanged + } + await MainActor.run { + if !itemsCountChanged { + firstPage = true } - await MainActor.run { - if reversedPage.count == 0 { - firstPage = true - } else { - im.reversedChatItems.append(contentsOf: reversedPage) - } - loadingItems = false - } - } catch let error { - logger.error("apiGetChat error: \(responseError(error))") - await MainActor.run { loadingItems = false } + loadingItems = false } } } @@ -897,12 +902,15 @@ struct ChatView: View { @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner @Binding @ObservedObject var chat: Chat @ObservedObject var dummyModel: ChatItemDummyModel = .shared + let index: Int + let isLastItem: Bool let chatItem: ChatItem + let merged: MergedItem let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? @Binding var showChatInfoSheet: Bool - @Binding var revealedChatItem: ChatItem? + @Binding var revealedItems: Set @State private var deletingItem: ChatItem? = nil @State private var showDeleteMessage = false @@ -918,7 +926,7 @@ struct ChatView: View { @State private var allowMenu: Bool = true @State private var markedRead = false - var revealed: Bool { chatItem == revealedChatItem } + var revealed: Bool { revealedItems.contains(chatItem.id) } typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?) @@ -940,46 +948,44 @@ struct ChatView: View { var body: some View { let currIndex = m.getChatItemIndex(chatItem) let ciCategory = chatItem.mergeCategory - let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) - let range = itemsRange(currIndex, prevHidden) - let timeSeparation = getItemSeparation(chatItem, at: currIndex) let im = ItemsModel.shared + + let last = isLastItem ? im.reversedChatItems.last : nil + let listItem = merged.newest() + let item = listItem.item + let range: ClosedRange? = if case let .grouped(_, _, _, rangeInReversed, _, _, _) = merged { + rangeInReversed.boxedValue + } else { + nil + } + let itemSeparation: ItemSeparation + let prevItemSeparationLargeGap: Boolean + let single = switch merged { case .single: true; default: false } + // if single || revealed { + // let prev = listItem.prevItem + // itemSeparation = getItemSeparation(item, prev) + // let nextForGap = if (item.mergeCategory != nil && item.mergeCategory == prev?.mergeCategory) || isLastItem { nil } else { listItem.nextItem } + // prevItemSeparationLargeGap = if nextForGap == nil { false } else { getItemSeparationLargeGap(nextForGap, item) } + // } else { + // itemSeparation = getItemSeparation(item, nil) + // prevItemSeparationLargeGap = false + // } Group { - if revealed, let range = range { - let items = Array(zip(Array(range), im.reversedChatItems[range])) - VStack(spacing: 0) { - ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in - let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1] - chatItemView(ci, nil, prev, getItemSeparation(ci, at: i)) - .overlay { - if let selected = selectedChatItems, ci.canBeDeletedForSelf { - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - let checked = selected.contains(ci.id) - selectUnselectChatItem(select: !checked, ci) - } + VStack(spacing: 0) { + chatItemView(item, range, listItem.prevItem, itemSeparation) + // if let date = timeSeparation.date { + // DateSeparator(date: date).padding(8) + // } + .overlay { + if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + let checked = selected.contains(chatItem.id) + selectUnselectChatItem(select: !checked, chatItem) } - } + } } - } - } else { - VStack(spacing: 0) { - chatItemView(chatItem, range, prevItem, timeSeparation) - if let date = timeSeparation.date { - DateSeparator(date: date).padding(8) - } - } - .overlay { - if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - let checked = selected.contains(chatItem.id) - selectUnselectChatItem(select: !checked, chatItem) - } - } - } } } .onAppear { @@ -1072,7 +1078,7 @@ struct ChatView: View { @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View { let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2 if case let .groupRcv(member) = ci.chatDir, - case let .group(groupInfo) = chat.chatInfo { + case .group = chat.chatInfo { let (prevMember, memCount): (GroupMember?, Int) = if let range = range { m.getPrevHiddenMember(member, range) @@ -1597,7 +1603,7 @@ struct ChatView: View { private func hideButton() -> Button { Button { withConditionalAnimation { - revealedChatItem = nil + merged.reveal(false, $revealedItems) } } label: { Label( @@ -1672,7 +1678,7 @@ struct ChatView: View { private func revealButton(_ ci: ChatItem) -> Button { Button { withConditionalAnimation { - revealedChatItem = ci + merged.reveal(true, $revealedItems) } } label: { Label( @@ -1685,7 +1691,7 @@ struct ChatView: View { private func expandButton() -> Button { Button { withConditionalAnimation { - revealedChatItem = chatItem + merged.reveal(true, $revealedItems) } } label: { Label( @@ -1698,7 +1704,7 @@ struct ChatView: View { private func shrinkButton() -> Button { Button { withConditionalAnimation { - revealedChatItem = nil + merged.reveal(false, $revealedItems) } } label: { Label ( diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index 187cbb2a38..9a186b912e 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -13,13 +13,17 @@ import SimpleXChat /// A List, which displays it's items in reverse order - from bottom to top struct ReverseList: UIViewControllerRepresentable { let items: Array + @Binding var revealedItems: Set + @Binding var unreadCount: Int @Binding var scrollState: ReverseListScrollModel.State /// Closure, that returns user interface for a given item - let content: (ChatItem) -> Content + /// Index, merged item + let content: (Int, MergedItem) -> Content - let loadPage: () -> Void + // pagination, visibleItemIndexesNonReversed + let loadItems: (ChatPagination, @escaping () -> ClosedRange) -> Void func makeUIViewController(context: Context) -> Controller { Controller(representer: self) @@ -46,13 +50,19 @@ struct ReverseList: UIViewControllerRepresentable { public class Controller: UITableViewController { private enum Section { case main } var representer: ReverseList - private var dataSource: UITableViewDiffableDataSource! - var itemCount: Int = 0 + private var dataSource: UITableViewDiffableDataSource! + var itemCount: Int { + get { + mergedItems.items.count + } + } + private var mergedItems: MergedItems private let updateFloatingButtons = PassthroughSubject() private var bag = Set() init(representer: ReverseList) { self.representer = representer + self.mergedItems = MergedItems.create(representer.items, representer.unreadCount, Set(), ItemsModel.shared.chatState) super.init(style: .plain) // 1. Style @@ -75,20 +85,22 @@ struct ReverseList: UIViewControllerRepresentable { } // 3. Configure data source - self.dataSource = UITableViewDiffableDataSource( + self.dataSource = UITableViewDiffableDataSource( tableView: tableView ) { (tableView, indexPath, item) -> UITableViewCell? in if indexPath.item > self.itemCount - 8 { - self.representer.loadPage() + logger.debug("LALAL ITEM \(indexPath.item)") + let pagination = ChatPagination.last(count: 0) + self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.mergedItems)) }) } let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) if #available(iOS 16.0, *) { - cell.contentConfiguration = UIHostingConfiguration { self.representer.content(item) } + cell.contentConfiguration = UIHostingConfiguration { self.representer.content(indexPath.item, item) } .margins(.all, 0) .minSize(height: 1) // Passing zero will result in system default of 44 points being used } else { if let cell = cell as? HostingCell { - cell.set(content: self.representer.content(item), parent: self) + cell.set(content: self.representer.content(indexPath.item, item), parent: self) } else { fatalError("Unexpected Cell Type for: \(item)") } @@ -185,22 +197,23 @@ struct ReverseList: UIViewControllerRepresentable { } func update(items: [ChatItem]) { - var snapshot = NSDiffableDataSourceSnapshot() + let wasCount = itemCount + self.mergedItems = MergedItems.create(items, representer.unreadCount, representer.revealedItems, ItemsModel.shared.chatState) + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(items) + snapshot.appendItems(mergedItems.items) dataSource.defaultRowAnimation = .none dataSource.apply( snapshot, - animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1 + animatingDifferences: wasCount != 0 && abs(mergedItems.items.count - wasCount) == 1 ) // Sets content offset on initial load - if itemCount == 0 { + if wasCount == 0 { tableView.setContentOffset( CGPoint(x: 0, y: -InvertedTableView.inset), animated: false ) } - itemCount = items.count updateFloatingButtons.send() } diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index abf6390196..5e5f047611 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -213,8 +213,7 @@ public func chatResponse(_ s: String) -> ChatResponse { if let jApiChat = jResp["apiChat"] as? NSDictionary, let user: UserRef = try? decodeObject(jApiChat["user"] as Any), let jChat = jApiChat["chat"] as? NSDictionary, - let jNavInfo = jApiChat["navInfo"] as? NSDictionary, - let (chat, navInfo) = try? parseChatData(jChat, jNavInfo) { + let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) { return .apiChat(user: user, chat: chat, navInfo: navInfo) } } else if type == "chatCmdError" { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 65f164354c..1e3b8c1625 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -563,7 +563,7 @@ public enum ChatResponse: Decodable, Error { case chatStopped case chatSuspended case apiChats(user: UserRef, chats: [ChatData]) - case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo) + case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?) case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) case serverOperatorConditions(conditions: ServerOperatorConditions) @@ -1977,6 +1977,11 @@ public struct ChatSettings: Codable, Hashable { public struct NavigationInfo: Decodable { public var afterUnread: Int = 0 public var afterTotal: Int = 0 + + public init(afterUnread: Int = 0, afterTotal: Int = 0) { + self.afterUnread = afterUnread + self.afterTotal = afterTotal + } } public enum MsgFilter: String, Codable, Hashable {