This commit is contained in:
Avently
2024-12-19 10:04:04 -08:00
parent 7631485796
commit c01959bf1f
3 changed files with 141 additions and 102 deletions
@@ -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<ClosedRange<Int>>,
mergeCategory: CIMergeCategory?,
unreadIds: Set<Int64>,
unreadIds: BoxedValue<Set<Int64>>,
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<Set<Int64>>) {
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
}
}
}
+117 -58
View File
@@ -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<Int64> = 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<Int>?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View {
@ViewBuilder func chatItemListView(
_ itemAtZeroIndexInWholeList: Bool,
_ range: ClosedRange<Int>?,
_ 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, *) {
+11 -11
View File
@@ -13,6 +13,7 @@ import SimpleXChat
/// 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 mergedItems: MergedItems
@Binding var revealedItems: Set<Int64>
@Binding var unreadCount: Int
@@ -53,16 +54,14 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
private var dataSource: UITableViewDiffableDataSource<Section, MergedItem>!
var itemCount: Int {
get {
mergedItems.items.count
representer.mergedItems.items.count
}
}
private var mergedItems: MergedItems
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>()
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<Content: View>: 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<Content: View>: 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<Section, MergedItem>()
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<Content: View>: 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<Content: View>: 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
}