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
7 changed files with 109 additions and 67 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
}
+2 -2
View File
@@ -1420,9 +1420,9 @@ func apiGetVersion() throws -> CoreVersionInfo {
throw r
}
func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) {
func getAgentSubsTotal() throws -> (SMPServerSubs, Bool) {
let userId = try currentUserId("getAgentSubsTotal")
let r = await chatSendCmd(.getAgentSubsTotal(userId: userId))
let r = chatSendCmdSync(.getAgentSubsTotal(userId: userId), log: false)
if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) }
logger.error("getAgentSubsTotal error: \(String(describing: r))")
throw r
+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)
@@ -324,7 +324,7 @@ struct ChatListView: View {
struct SubsStatusIndicator: View {
@State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs
@State private var hasSess: Bool = false
@State private var task: Task<Void, Never>?
@State private var timer: Timer? = nil
@State private var showServersSummary = false
@AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false
@@ -343,10 +343,10 @@ struct SubsStatusIndicator: View {
}
.disabled(ChatModel.shared.chatRunning != true)
.onAppear {
startTask()
startTimer()
}
.onDisappear {
stopTask()
stopTimer()
}
.appSheet(isPresented: $showServersSummary) {
ServersSummaryView()
@@ -354,28 +354,25 @@ struct SubsStatusIndicator: View {
}
}
private func startTask() {
task = Task {
while !Task.isCancelled {
if AppChatState.shared.value == .active {
do {
let (subs, hasSess) = try await getAgentSubsTotal()
await MainActor.run {
self.subs = subs
self.hasSess = hasSess
}
} catch let error {
logger.error("getSubsTotal error: \(responseError(error))")
}
}
try? await Task.sleep(nanoseconds: 1_000_000_000) // Sleep for 1 second
private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if AppChatState.shared.value == .active {
getSubsTotal()
}
}
}
func stopTask() {
task?.cancel()
task = nil
func stopTimer() {
timer?.invalidate()
timer = nil
}
private func getSubsTotal() {
do {
(subs, hasSess) = try getAgentSubsTotal()
} catch let error {
logger.error("getSubsTotal error: \(responseError(error))")
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: f229e135e33ec5ee1f6d0978bd903d84b0b60efa
tag: 0dd52dc69f197a86f2f03e47b339bf3a94a596a3
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."f229e135e33ec5ee1f6d0978bd903d84b0b60efa" = "0p4n4bghg24a98py9mij6s24fvsix4a73lwkg6skqf4rx8zp392v";
"https://github.com/simplex-chat/simplexmq.git"."0dd52dc69f197a86f2f03e47b339bf3a94a596a3" = "1dzfpk9bq8y885i2z419k79f2nyagrqkqh8rw3gzmf1hy5w3fbjs";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";