mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-06-04 10:21:50 +00:00
changes
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, *) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user