Compare commits

...

17 Commits

Author SHA1 Message Date
Levitating Pineapple d5090c3df2 animate row removal 2024-08-17 22:45:36 +03:00
Levitating Pineapple 71a3c511eb no animation near bottom 2024-08-17 10:27:55 +03:00
Evgeny Poberezkin 78e27f3296 reset retained 2024-08-17 07:50:15 +01:00
Levitating Pineapple d064fe00b1 cleanup 2024-08-16 22:31:38 +03:00
Levitating Pineapple 28fc268e7f point based nearBottom 2024-08-16 22:20:20 +03:00
Levitating Pineapple d76b321919 fix scroll to bottom, on insert 2024-08-16 21:48:41 +03:00
Levitating Pineapple 299766a758 handle retained items 2024-08-16 21:21:34 +03:00
Levitating Pineapple 86fd8cf5a1 find added message count 2024-08-16 20:44:10 +03:00
Levitating Pineapple 46d7195615 animate all added images 2024-08-16 19:22:59 +03:00
Levitating Pineapple 5b18da4fe6 dual snapshot animation 2024-08-16 19:11:47 +03:00
Levitating Pineapple 4005da2524 increase animation time 2024-08-16 13:34:31 +03:00
Levitating Pineapple 7e12dad716 cleanup 2024-08-16 12:42:42 +03:00
Levitating Pineapple bd2d751a81 Merge branch 'master' into lp/chat-message-animation 2024-08-16 12:33:59 +03:00
Levitating Pineapple 1321a198f3 fade in chat item 2024-08-16 12:32:12 +03:00
Evgeny Poberezkin 6777944cec Merge branch 'master' into lp/chat-message-animation 2024-08-15 14:53:01 +01:00
Levitating Pineapple 1f2c4ecb17 update comment 2024-08-15 16:20:59 +03:00
Levitating Pineapple 1f18fca722 chat new message animation 2024-08-15 16:06:39 +03:00
3 changed files with 87 additions and 42 deletions
+1 -16
View File
@@ -50,9 +50,6 @@ class ItemsModel: ObservableObject {
var reversedChatItems: [ChatItem] = [] {
willSet { publisher.send() }
}
var itemAdded = false {
willSet { publisher.send() }
}
// Publishes directly to `objectWillChange` publisher,
// this will cause reversedChatItems to be rendered without throttling
@@ -456,7 +453,6 @@ final class ChatModel: ObservableObject {
ci.meta.itemStatus = status
}
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
im.itemAdded = true
}
return true
}
@@ -551,7 +547,6 @@ final class ChatModel: ObservableObject {
let cItem = ChatItem.liveDummy(chatInfo.chatType)
withAnimation {
im.reversedChatItems.insert(cItem, at: 0)
im.itemAdded = true
}
return cItem
}
@@ -902,22 +897,14 @@ final class ChatModel: ObservableObject {
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
var i = 0
var totalBelow = 0
var unreadBelow = 0
while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) {
totalBelow += 1
if im.reversedChatItems[i].isRcvNew {
unreadBelow += 1
}
i += 1
}
return UnreadChatItemCounts(
// TODO these thresholds account for the fact that items are still "visible" while
// covered by compose area, they should be replaced with the actual height in pixels below the screen.
isNearBottom: totalBelow < 15,
isReallyNearBottom: totalBelow < 2,
unreadBelow: unreadBelow
)
return UnreadChatItemCounts(unreadBelow: unreadBelow)
}
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
@@ -941,8 +928,6 @@ struct NTFContactRequest {
}
struct UnreadChatItemCounts: Equatable {
var isNearBottom: Bool
var isReallyNearBottom: Bool
var unreadBelow: Int
}
+7 -15
View File
@@ -395,7 +395,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,
scrollState: $scrollModel.state,
isFarFromBottom: $scrollModel.isFarFromBottom
) { ci in
let voiceNoFrame = voiceWithoutFrame(ci)
let maxWidth = cInfo.chatType == .group
? voiceNoFrame
@@ -432,14 +436,6 @@ struct ChatView: View {
.onChange(of: im.reversedChatItems) { _ in
floatingButtonModel.chatItemsChanged()
}
.onChange(of: im.itemAdded) { added in
if added {
im.itemAdded = false
if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom {
scrollModel.scrollToBottom()
}
}
}
}
}
@@ -470,11 +466,7 @@ struct ChatView: View {
private var bag = Set<AnyCancellable>()
init() {
unreadChatItemCounts = UnreadChatItemCounts(
isNearBottom: true,
isReallyNearBottom: true,
unreadBelow: 0
)
unreadChatItemCounts = UnreadChatItemCounts(unreadBelow: 0)
events
.receive(on: DispatchQueue.global(qos: .background))
.scan(Set<String>()) { itemsInView, event in
@@ -538,7 +530,7 @@ struct ChatView: View {
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !counts.isNearBottom {
} else if scrollModel.isFarFromBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
+79 -11
View File
@@ -14,6 +14,7 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
let items: Array<Item>
@Binding var scrollState: ReverseListScrollModel<Item>.State
@Binding var isFarFromBottom: Bool
/// Closure, that returns user interface for a given item
let content: (Item) -> Content
@@ -163,24 +164,90 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
animated: animated
)
} else {
tableView.setContentOffset(
CGPoint(x: .zero, y: -InvertedTableView.inset),
animated: animated
)
scrollToBottom(animated: animated)
}
Task { representer.scrollState = .atDestination }
}
func update(items: Array<Item>) {
private func scrollToBottom(animated: Bool) {
tableView.setContentOffset(
CGPoint(x: .zero, y: -InvertedTableView.inset),
animated: animated
)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let adjustedOffset = tableView.contentOffset.y + InvertedTableView.inset
if representer.isFarFromBottom {
if adjustedOffset < 800 {
DispatchQueue.main.async { self.representer.isFarFromBottom = false }
}
} else if adjustedOffset > 1000 {
DispatchQueue.main.async { self.representer.isFarFromBottom = true }
}
}
private var isNearBottom: Bool { scrollOffset > 0 && scrollOffset < 500 }
private var isAtBottom: Bool { scrollOffset == 0 }
private var scrollOffset: CGFloat { tableView.contentOffset.y + InvertedTableView.inset }
private var itemId: Item.ID?
private var retainedItems: [Item]?
private var updateInProgress = false
func update(items: [Item]) {
if updateInProgress {
retainedItems = items
return
}
if let itemId,
let i = items.firstIndex(where: { $0.id == itemId }),
i > 0 {
updateInProgress = true
// Update existing items without animation
_update(items: Array(items[i...])) {
DispatchQueue.main.async {
// Added items animated by sliding from bottom (.top)
self._update(
items: items,
animated: self.isAtBottom,
rowAnimation: self.isAtBottom ? .top : .none
) {
self.updateInProgress = false
if self.isNearBottom { self.scrollToBottom(animated: false) }
// Process update, which might have arrived before completion
if let items = self.retainedItems {
self.retainedItems = nil
self.update(items: items)
}
}
}
}
} else {
let wasItemsRemoved = !items.isEmpty && items.count < itemCount
_update(
items: items,
animated: wasItemsRemoved,
rowAnimation: wasItemsRemoved ? .fade : .none
)
}
itemId = items.first?.id
itemCount = items.count
}
private func _update(
items: [Item],
animated: Bool = false,
rowAnimation: UITableView.RowAnimation = .none,
completion: (() -> Void)? = nil
) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.defaultRowAnimation = .none
dataSource.apply(
snapshot,
animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1
)
itemCount = items.count
dataSource.defaultRowAnimation = rowAnimation
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
}
}
@@ -239,6 +306,7 @@ class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
}
@Published var state: State = .atDestination
@Published var isFarFromBottom: Bool = false
func scrollToNextPage() {
state = .scrollingTo(.nextPage)