diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 51c9f102b7..03518f3384 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -85,7 +85,6 @@ final class ChatModel: ObservableObject { // current chat @Published var chatId: String? var chatItemStatuses: Dictionary = [:] - @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] @Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list @Published var membersLoaded = false @@ -290,6 +289,7 @@ final class ChatModel: ObservableObject { } func updateChats(with newChats: [ChatData]) { + var chatsToPop: Set = [] for i in 0.. 0 { - if chatId == nil { - withAnimation { popChat_(i) } - } else if chatId == cInfo.id { - chatToTop = cInfo.id - } else { - popChat_(i) - } + popChatCollector.addChat(cInfo.id) } } else { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) @@ -579,7 +570,7 @@ final class ChatModel: ObservableObject { // update current chat markChatItemRead_(itemIndex) // update preview - unreadCollector.decreaseUnreadCounter(chatIndex) + unreadCollector.decreaseUnreadCounter(cInfo.id) } } } @@ -588,29 +579,80 @@ final class ChatModel: ObservableObject { private let unreadCollector = UnreadCollector() class UnreadCollector { - private let subject = PassthroughSubject() + private let subject = PassthroughSubject() private var bag = Set() - private var dictionary = Dictionary() + private var unreadCounts: [ChatId: Int] = [:] init() { subject .debounce(for: 1, scheduler: DispatchQueue.main) - .sink { _ in - self.dictionary.forEach { key, value in - ChatModel.shared.decreaseUnreadCounter(key, by: value) + .sink { + let m = ChatModel.shared + for (chatId, count) in self.unreadCounts { + if let i = m.getChatIndex(chatId) { + m.decreaseUnreadCounter(i, by: count) + } } - self.dictionary = Dictionary() + self.unreadCounts = [:] } .store(in: &bag) } // Only call from main thread - func decreaseUnreadCounter(_ chatIndex: Int) { - dictionary[chatIndex] = (dictionary[chatIndex] ?? 0) + 1 - subject.send(chatIndex) + func decreaseUnreadCounter(_ chatId: ChatId) { + unreadCounts[chatId] = (unreadCounts[chatId] ?? 0) + 1 + subject.send() } } + let popChatCollector = PopChatCollector() + + class PopChatCollector { + private let subject = PassthroughSubject() + private var bag = Set() + private var chatsToPop: Set = [] + + init() { + subject + .throttle(for: 10, scheduler: DispatchQueue.main, latest: true) + .sink { self.popRecentChats() } + .store(in: &bag) + } + + // Only call from main thread + func addChat(_ chatId: ChatId) { + chatsToPop.insert(chatId) + subject.send() + } + + func addChats(_ chatIds: Set) { + chatsToPop = chatsToPop.union(chatIds) + subject.send() + } + + func popRecentChats() { + let m = ChatModel.shared + if m.chatId != nil { return } + var ixs: IndexSet = [] + var chs: [Chat] = [] + var skipped: ChatId? + for chatId in chatsToPop { + if m.chatId == chatId { + skipped = chatId + } else if let i = m.getChatIndex(chatId) { + ixs.insert(i) + chs.append(m.chats[i]) + } + } + chs.sort { ch1, ch2 in ch1.chatInfo.chatTs > ch2.chatInfo.chatTs } + withAnimation { + m.chats.remove(atOffsets: ixs) + m.chats.insert(contentsOf: chs, at: 0) + } + chatsToPop = if let skipped { [skipped] } else { [] } + } + } + private func markChatItemRead_(_ i: Int) { let meta = im.reversedChatItems[i].meta if case .rcvNew = meta.itemStatus { @@ -704,17 +746,6 @@ final class ChatModel: ObservableObject { return (prevMember, memberIds.count) } - func popChat(_ id: String) { - if let i = getChatIndex(id) { - popChat_(i) - } - } - - private func popChat_(_ i: Int, to position: Int = 0) { - let chat = chats.remove(at: i) - chats.insert(chat, at: position) - } - func dismissConnReqView(_ id: String) { if id == showingInvitation?.connId { markShowingInvitationUsed() diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a9bdfab236..6d2897338f 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1711,7 +1711,7 @@ func processReceivedMsg(_ res: ChatResponse) async { let cItem = aChatItem.chatItem await MainActor.run { if active(user) { - NewItemThrottler.shared.receive(cItem, for: cInfo) + m.addChatItem(cInfo, cItem) } else if cItem.isRcvNew && cInfo.ntfsEnabled { m.increaseUnreadCounter(user: user) } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 82d322d6fd..e75538fb3a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -158,10 +158,9 @@ struct ChatListView: View { .offset(x: -8) } } - .onChange(of: chatModel.chatId) { _ in - if chatModel.chatId == nil, let chatId = chatModel.chatToTop { - chatModel.chatToTop = nil - chatModel.popChat(chatId) + .onChange(of: chatModel.chatId) { chatId in + if chatId == nil { + chatModel.popChatCollector.popRecentChats() } stopAudioPlayer() } diff --git a/apps/ios/Shared/Views/Helpers/NewItemThrottler.swift b/apps/ios/Shared/Views/Helpers/NewItemThrottler.swift index f2e86d8c0d..893e28f29d 100644 --- a/apps/ios/Shared/Views/Helpers/NewItemThrottler.swift +++ b/apps/ios/Shared/Views/Helpers/NewItemThrottler.swift @@ -10,27 +10,4 @@ import Foundation import Combine import SimpleXChat -class NewItemThrottler { - static let shared = NewItemThrottler() - private let subject = PassthroughSubject() - private var bag = Set() - private var accumulated = [(ChatInfo, ChatItem)]() - - private init() { - subject - .throttle(for: 1, scheduler: DispatchQueue.main, latest: true) - .sink { - self.accumulated.forEach { cInfo, cItem in - ChatModel.shared.addChatItem(cInfo, cItem) - } - self.accumulated = [] - } - .store(in: &bag) - } - - func receive(_ cItem: ChatItem, for cInfo: ChatInfo) { - DispatchQueue.main.async { self.accumulated.append((cInfo, cItem)) } - subject.send() - } -}