From c01959bf1fb1cc59c4711c343529008ab05bc805 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:04:04 -0800 Subject: [PATCH] changes --- .../Shared/Views/Chat/ChatItemsMerger.swift | 46 ++--- apps/ios/Shared/Views/Chat/ChatView.swift | 175 ++++++++++++------ apps/ios/Shared/Views/Chat/ReverseList.swift | 22 +-- 3 files changed, 141 insertions(+), 102 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift index 82223cdfce..473d541211 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -49,12 +49,12 @@ struct MergedItems { } let revealed = item.mergeCategory == nil || revealedItems.contains(item.id) - if recent != nil, case let .grouped(items, _, _, _, mergeCategory, _, _) = recent, mergeCategory == category, let first = items.first, !revealedItems.contains(first.item.id) && !itemIsSplit { + if recent != nil, case let .grouped(items, _, _, _, mergeCategory, unreadIds, _) = recent, mergeCategory == category, let first = items.boxedValue.first, !revealedItems.contains(first.item.id) && !itemIsSplit { let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore) - recent!.appendItem(listItem) + items.boxedValue.append(listItem) if item.isRcvNew { - recent!.insertUnreadId(item.id) + unreadIds.boxedValue.insert(item.id) } if let lastRevealedIdsInMergedItems, let lastRangeInReversedForMergedItems { if revealed { @@ -73,12 +73,12 @@ struct MergedItems { } lastRangeInReversedForMergedItems = BoxedValue(index ... index) recent = MergedItem.grouped( - items: [listItem], + items: BoxedValue([listItem]), revealed: revealed, revealedIdsWithinGroup: lastRevealedIdsInMergedItems!, rangeInReversed: lastRangeInReversedForMergedItems!, mergeCategory: item.mergeCategory, - unreadIds: item.isRcvNew ? Set(arrayLiteral: item.id) : Set(), + unreadIds: BoxedValue(item.isRcvNew ? Set(arrayLiteral: item.id) : Set()), startIndexInReversedItems: index ) } else { @@ -126,7 +126,7 @@ enum MergedItem: Hashable { * of [Grouped] item with all grouped items inside [items]. In other words, number of [MergedItem] will always be equal to number of * visible rows in ChatView LazyColumn */ case grouped ( - items: [ListItem], + items: BoxedValue<[ListItem]>, revealed: Bool, // it stores ids for all consecutive revealed items from the same group in order to hide them all on user's action // it's the same list instance for all Grouped items within revealed group @@ -134,37 +134,17 @@ enum MergedItem: Hashable { revealedIdsWithinGroup: BoxedValue<[Int64]>, rangeInReversed: BoxedValue>, mergeCategory: CIMergeCategory?, - unreadIds: Set, + unreadIds: BoxedValue>, startIndexInReversedItems: Int ) - mutating func appendItem(_ item: ListItem) { - switch self { - case let .grouped(items, revealed, revealedIdsWithinGroup, rangeInReversed, mergeCategory, unreadIds, startIndexInReversedItems): - var newItems = items - newItems.append(item) - self = .grouped(items: newItems, revealed: revealed, revealedIdsWithinGroup: revealedIdsWithinGroup, rangeInReversed: rangeInReversed, mergeCategory: mergeCategory, unreadIds: unreadIds, startIndexInReversedItems: startIndexInReversedItems) - case .single: () - } - } - - mutating func insertUnreadId(_ id: Int64) { - switch self { - case let .grouped(items, revealed, revealedIdsWithinGroup, rangeInReversed, mergeCategory, unreadIds, startIndexInReversedItems): - var newUnreadIds = unreadIds - newUnreadIds.insert(id) - self = .grouped(items: items, revealed: revealed, revealedIdsWithinGroup: revealedIdsWithinGroup, rangeInReversed: rangeInReversed, mergeCategory: mergeCategory, unreadIds: newUnreadIds, startIndexInReversedItems: startIndexInReversedItems) - case .single: () - } - } - func reveal(_ reveal: Bool, _ revealedItems: Binding>) { if case .grouped(let items, _, let revealedIdsWithinGroup, _, _, _, _) = self { var newRevealed = revealedItems.wrappedValue var i = 0 if reveal { - while i < items.count { - newRevealed.insert(items[i].item.id) + while i < items.boxedValue.count { + newRevealed.insert(items.boxedValue[i].item.id) i += 1 } } else { @@ -190,28 +170,28 @@ enum MergedItem: Hashable { func hasUnread() -> Bool { switch self { case let .single(item, _): item.item.isRcvNew - case let .grouped(_, _, _, _, _, unreadIds, _): !unreadIds.isEmpty + case let .grouped(_, _, _, _, _, unreadIds, _): !unreadIds.boxedValue.isEmpty } } func newest() -> ListItem { switch self { case let .single(item, _): item - case let .grouped(items, _, _, _, _, _, _): items[0] + case let .grouped(items, _, _, _, _, _, _): items.boxedValue[0] } } func oldest() -> ListItem { switch self { case let .single(item, _): item - case let .grouped(items, _, _, _, _, _, _): items[items.count - 1] + case let .grouped(items, _, _, _, _, _, _): items.boxedValue[items.boxedValue.count - 1] } } func lastIndexInReversed() -> Int { switch self { case .single: startIndexInReversedItems - case let .grouped(items, _, _, _, _, _, _): startIndexInReversedItems + items.count - 1 + case let .grouped(items, _, _, _, _, _, _): startIndexInReversedItems + items.boxedValue.count - 1 } } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 350bc58d03..de20b792fa 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 mergedItems: MergedItems = MergedItems.create(ItemsModel.shared.reversedChatItems, 0, [], ItemsModel.shared.chatState) @State var revealedItems: Set = Set() @State var theme: AppTheme = buildTheme() @Environment(\.dismiss) var dismiss @@ -431,27 +432,27 @@ struct ChatView: View { private func chatItemsList() -> some View { let cInfo = chat.chatInfo - let mergedItems = filtered(im.reversedChatItems) return GeometryReader { g in - ReverseList(items: mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state) { index, mergedItem in + let _ = logger.debug("LALAL RELOAD \(im.reversedChatItems.count)") + ReverseList(items: im.reversedChatItems, mergedItems: $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 + case let .grouped(items, _, _, _, _, _, _): items.boxedValue.last!.item } let voiceNoFrame = voiceWithoutFrame(ci) let maxWidth = cInfo.chatType == .group - ? voiceNoFrame - ? (g.size.width - 28) - 42 - : (g.size.width - 28) * 0.84 - 42 - : voiceNoFrame - ? (g.size.width - 32) - : (g.size.width - 32) * 0.84 + ? voiceNoFrame + ? (g.size.width - 28) - 42 + : (g.size.width - 28) * 0.84 - 42 + : voiceNoFrame + ? (g.size.width - 32) + : (g.size.width - 32) * 0.84 return ChatItemWithMenu( + chat: $chat, index: index, isLastItem: index == mergedItems.items.count - 1, - chat: $chat, chatItem: ci, - item: mergedItem, + merged: mergedItem, maxWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, @@ -464,6 +465,18 @@ struct ChatView: View { } loadItems: { pagination, visibleItemIndexesNonReversed in loadChatItems(cInfo, pagination, visibleItemIndexesNonReversed) } + .onAppear { + mergedItems = MergedItems.create(im.reversedChatItems, chat.chatStats.unreadCount, revealedItems, ItemsModel.shared.chatState) + } + .onChange(of: im.reversedChatItems) { items in + mergedItems = MergedItems.create(items, chat.chatStats.unreadCount, revealedItems, ItemsModel.shared.chatState) + } + .onChange(of: revealedItems) { revealed in + mergedItems = MergedItems.create(im.reversedChatItems, chat.chatStats.unreadCount, revealed, ItemsModel.shared.chatState) + } + .onChange(of: chat.chatStats.unreadCount) { unreadCount in + mergedItems = MergedItems.create(im.reversedChatItems, unreadCount, revealedItems, ItemsModel.shared.chatState) + } .opacity(ItemsModel.shared.isLoading ? 0 : 1) .padding(.vertical, -InvertedTableView.inset) .onTapGesture { hideKeyboard() } @@ -587,6 +600,7 @@ struct ChatView: View { .background(.thinMaterial) .clipShape(Capsule()) .opacity(model.isDateVisible ? 1 : 0) + .padding(.vertical, 4) } VStack { let unreadAbove = model.totalUnread - model.unreadBelow @@ -930,24 +944,57 @@ struct ChatView: View { typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?) - func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation { - let im = ItemsModel.shared - if let i, i > 0 && im.reversedChatItems.count >= i { - let nextItem = im.reversedChatItems[i - 1] - let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60 - return ( - timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs), - largeGap: largeGap, - date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs - ) + func getItemSeparation(_ chatItem: ChatItem, _ prevItem: ChatItem?) -> ItemSeparation { + guard let prevItem else { + return ItemSeparation(timestamp: true, largeGap: true, date: nil) + } + + let sameMemberAndDirection = if case .groupRcv(let prevGroupMember) = prevItem.chatDir, case .groupRcv(let groupMember) = chatItem.chatDir { + groupMember.groupMemberId == prevGroupMember.groupMemberId } else { - return (timestamp: true, largeGap: true, date: nil) + chatItem.chatDir.sent == prevItem.chatDir.sent + } + let largeGap = !sameMemberAndDirection || prevItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60 + + return ItemSeparation( + timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(prevItem.meta.itemTs), + largeGap: largeGap, + date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: prevItem.meta.itemTs) ? nil : prevItem.meta.itemTs + ) + } + + func getItemSeparationLargeGap(_ chatItem: ChatItem, _ nextItem: ChatItem?) -> Bool { + guard let nextItem else { + return true + } + + let sameMemberAndDirection = if case .groupRcv(let nextGroupMember) = nextItem.chatDir, case .groupRcv(let groupMember) = chatItem.chatDir { + groupMember.groupMemberId == nextGroupMember.groupMemberId + } else { + chatItem.chatDir.sent == nextItem.chatDir.sent + } + return !sameMemberAndDirection || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60 + } + + func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool { + let oldIsGroupRcv = switch older?.chatDir { + case .groupRcv: true + default: false + } + let sameMember = switch (older?.chatDir, current.chatDir) { + case (.groupRcv(let oldMember), .groupRcv(let member)): + oldMember.memberId == member.memberId + default: + false + } + if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { + return true + } else { + return false } } var body: some View { - let currIndex = m.getChatItemIndex(chatItem) - let ciCategory = chatItem.mergeCategory let im = ItemsModel.shared let last = isLastItem ? im.reversedChatItems.last : nil @@ -958,34 +1005,39 @@ struct ChatView: View { } else { nil } + let showAvatar = if case .grouped = merged { shouldShowAvatar(item, listItem.nextItem) } else { true } 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 { - 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) - } - } + let prevItemSeparationLargeGap: Bool + let single = switch merged { + case .single: true + default: false + } + if single || revealed { + let prev = listItem.prevItem + itemSeparation = getItemSeparation(item, prev) + let nextForGap = (item.mergeCategory != nil && item.mergeCategory == prev?.mergeCategory) || isLastItem ? nil : listItem.nextItem + prevItemSeparationLargeGap = if let nextForGap { getItemSeparationLargeGap(nextForGap, item) } else { false } + } else { + itemSeparation = getItemSeparation(item, nil) + prevItemSeparationLargeGap = false + } + return VStack(spacing: 0) { + if let last { + DateSeparator(date: last.meta.itemTs).padding(8) + } + chatItemListView(index == 0, range, showAvatar, item, itemSeparation, prevItemSeparationLargeGap) + .overlay { + if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + let checked = selected.contains(chatItem.id) + selectUnselectChatItem(select: !checked, chatItem) + } } + } + if let date = itemSeparation.date { + DateSeparator(date: date).padding(8) } } .onAppear { @@ -1075,20 +1127,27 @@ struct ChatView: View { } } - @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View { + @ViewBuilder func chatItemListView( + _ itemAtZeroIndexInWholeList: Bool, + _ range: ClosedRange?, + _ showAvatar: Bool, + _ ci: ChatItem, + _ itemSeparation: ItemSeparation, + _ previousItemSeparationLargeGap: Bool + ) -> some View { let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2 if case let .groupRcv(member) = ci.chatDir, case .group = chat.chatInfo { - let (prevMember, memCount): (GroupMember?, Int) = - if let range = range { - m.getPrevHiddenMember(member, range) - } else { - (nil, 1) - } - if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil { + if showAvatar { VStack(alignment: .leading, spacing: 4) { if ci.content.showMemberName { Group { + let (prevMember, memCount): (GroupMember?, Int) = + if let range = range { + m.getPrevHiddenMember(member, range) + } else { + (nil, 1) + } if memCount == 1 && member.memberRole > .member { Group { if #available(iOS 16.0, *) { diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index 9a186b912e..9cb07d8f3b 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -13,6 +13,7 @@ import SimpleXChat /// A List, which displays it's items in reverse order - from bottom to top struct ReverseList: UIViewControllerRepresentable { let items: Array + @Binding var mergedItems: MergedItems @Binding var revealedItems: Set @Binding var unreadCount: Int @@ -53,16 +54,14 @@ struct ReverseList: UIViewControllerRepresentable { private var dataSource: UITableViewDiffableDataSource! var itemCount: Int { get { - mergedItems.items.count + representer.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 @@ -91,7 +90,7 @@ struct ReverseList: UIViewControllerRepresentable { if indexPath.item > self.itemCount - 8 { logger.debug("LALAL ITEM \(indexPath.item)") let pagination = ChatPagination.last(count: 0) - self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.mergedItems)) }) + self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) }) } let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) if #available(iOS 16.0, *) { @@ -197,15 +196,16 @@ struct ReverseList: UIViewControllerRepresentable { } func update(items: [ChatItem]) { - let wasCount = itemCount - self.mergedItems = MergedItems.create(items, representer.unreadCount, representer.revealedItems, ItemsModel.shared.chatState) + let wasCount = dataSource.snapshot().numberOfItems + logger.debug("LALAL WAS \(wasCount) will be \(self.representer.mergedItems.items.count)") + //self.representer.mergedItems = MergedItems.create(items, representer.unreadCount, representer.revealedItems, ItemsModel.shared.chatState) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(mergedItems.items) + snapshot.appendItems(representer.mergedItems.items) dataSource.defaultRowAnimation = .none dataSource.apply( snapshot, - animatingDifferences: wasCount != 0 && abs(mergedItems.items.count - wasCount) == 1 + animatingDifferences: wasCount != 0 && abs(representer.mergedItems.items.count - wasCount) == 1 ) // Sets content offset on initial load if wasCount == 0 { @@ -223,11 +223,11 @@ struct ReverseList: UIViewControllerRepresentable { func getListState() -> ListState? { if let visibleRows = tableView.indexPathsForVisibleRows, - visibleRows.last?.item ?? 0 < representer.items.count { + visibleRows.last?.item ?? 0 < representer.mergedItems.items.count { let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset let topItemDate: Date? = if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) { - representer.items[lastVisible.item].meta.itemTs + representer.mergedItems.items[lastVisible.item].oldest().item.meta.itemTs } else { nil } @@ -235,7 +235,7 @@ struct ReverseList: UIViewControllerRepresentable { let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) let bottomItemId: ChatItem.ID? = if let firstVisible { - representer.items[firstVisible.item].id + representer.mergedItems.items[firstVisible.item].newest().item.id } else { nil }