import Foundation import SwiftSignalKit public enum ViewUpdateType { case Initial case InitialUnread(MessageIndex) case Generic case FillHole case UpdateVisible } final class ViewTracker { private let queue: Queue private let renderMessage: (IntermediateMessage) -> Message private let getPeer: (PeerId) -> Peer? private let getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings? private let getCachedPeerData: (PeerId) -> CachedPeerData? private let getPeerPresence: (PeerId) -> PeerPresence? private let getTotalUnreadState: () -> ChatListTotalUnreadState private let getPeerReadState: (PeerId) -> CombinedPeerReadState? private let operationLogGetOperations: (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry] private let operationLogGetTailIndex: (PeerOperationLogTag) -> Int32? private let getPreferencesEntry: (ValueBoxKey) -> PreferencesEntry? private var chatListViews = Bag<(MutableChatListView, ValuePipe<(ChatListView, ViewUpdateType)>)>() private var messageHistoryViews = Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>() private var contactPeerIdsViews = Bag<(MutableContactPeerIdsView, ValuePipe)>() private var contactPeersViews = Bag<(MutableContactPeersView, ValuePipe)>() private let messageHistoryHolesView = MutableMessageHistoryHolesView() private let messageHistoryHolesViewSubscribers = Bag>() private let chatListHolesView = MutableChatListHolesView() private let chatListHolesViewSubscribers = Bag>() private var unsentMessageView: UnsentMessageHistoryView private let unsendMessageIdsViewSubscribers = Bag>() private var synchronizeReadStatesView: MutableSynchronizePeerReadStatesView private let synchronizePeerReadStatesViewSubscribers = Bag>() private var peerViews = Bag<(MutablePeerView, ValuePipe)>() private var unreadMessageCountsViews = Bag<(MutableUnreadMessageCountsView, ValuePipe)>() private var peerMergedOperationLogViews = Bag<(MutablePeerMergedOperationLogView, ValuePipe)>() private let getTimestampBasedMessageAttributesHead: (UInt16) -> TimestampBasedMessageAttributesEntry? private var timestampBasedMessageAttributesViews = Bag<(MutableTimestampBasedMessageAttributesView, ValuePipe)>() private var combinedViews = Bag<(CombinedMutableView, ValuePipe)>() private var postboxStateViews = Bag<(MutablePostboxStateView, ValuePipe)>() private var messageViews = Bag<(MutableMessageView, ValuePipe)>() private var preferencesViews = Bag<(MutablePreferencesView, ValuePipe)>() private var multiplePeersViews = Bag<(MutableMultiplePeersView, ValuePipe)>() private var itemCollectionsViews = Bag<(MutableItemCollectionsView, ValuePipe)>() init(queue: Queue, renderMessage: @escaping (IntermediateMessage) -> Message, getPeer: @escaping (PeerId) -> Peer?, getPeerNotificationSettings: @escaping (PeerId) -> PeerNotificationSettings?, getCachedPeerData: @escaping (PeerId) -> CachedPeerData?, getPeerPresence: @escaping (PeerId) -> PeerPresence?, getTotalUnreadState: @escaping () -> ChatListTotalUnreadState, getPeerReadState: @escaping (PeerId) -> CombinedPeerReadState?, operationLogGetOperations: @escaping (PeerOperationLogTag, Int32, Int) -> [PeerMergedOperationLogEntry], operationLogGetTailIndex: @escaping (PeerOperationLogTag) -> Int32?, getTimestampBasedMessageAttributesHead: @escaping (UInt16) -> TimestampBasedMessageAttributesEntry?, getPreferencesEntry: @escaping (ValueBoxKey) -> PreferencesEntry?, unsentMessageIds: [MessageId], synchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation]) { self.queue = queue self.renderMessage = renderMessage self.getPeer = getPeer self.getPeerNotificationSettings = getPeerNotificationSettings self.getCachedPeerData = getCachedPeerData self.getPeerPresence = getPeerPresence self.getTotalUnreadState = getTotalUnreadState self.getPeerReadState = getPeerReadState self.operationLogGetOperations = operationLogGetOperations self.operationLogGetTailIndex = operationLogGetTailIndex self.getTimestampBasedMessageAttributesHead = getTimestampBasedMessageAttributesHead self.getPreferencesEntry = getPreferencesEntry self.unsentMessageView = UnsentMessageHistoryView(ids: unsentMessageIds) self.synchronizeReadStatesView = MutableSynchronizePeerReadStatesView(operations: synchronizePeerReadStateOperations) } func addPostboxStateView(_ view: MutablePostboxStateView) -> (Bag<(MutablePostboxStateView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.postboxStateViews.add(record) return (index, record.1.signal()) } func removePostboxStateView(_ index: Bag<(MutablePostboxStateView, ValuePipe)>.Index) { self.postboxStateViews.remove(index) } func addMessageHistoryView(_ view: MutableMessageHistoryView) -> (Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>.Index, Signal<(MessageHistoryView, ViewUpdateType), NoError>) { let record = (view, ValuePipe<(MessageHistoryView, ViewUpdateType)>()) let index: Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>.Index index = self.messageHistoryViews.add(record) self.updateTrackedHoles() return (index, record.1.signal()) } func removeMessageHistoryView(index: Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>.Index) { self.messageHistoryViews.remove(index) self.updateTrackedHoles() } func addChatListView(_ view: MutableChatListView) -> (Bag<(MutableChatListView, ValuePipe<(ChatListView, ViewUpdateType)>)>.Index, Signal<(ChatListView, ViewUpdateType), NoError>) { let record = (view, ValuePipe<(ChatListView, ViewUpdateType)>()) let index = self.chatListViews.add(record) self.updateTrackedChatListHoles() return (index, record.1.signal()) } func removeChatListView(_ index: Bag<(MutableChatListView, ValuePipe)>.Index) { self.chatListViews.remove(index) self.updateTrackedChatListHoles() } func addContactPeerIdsView(_ view: MutableContactPeerIdsView) -> (Bag<(MutableContactPeerIdsView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.contactPeerIdsViews.add(record) return (index, record.1.signal()) } func removeContactPeerIdsView(_ index: Bag<(MutableContactPeerIdsView, ValuePipe)>.Index) { self.contactPeerIdsViews.remove(index) } func addContactPeersView(_ view: MutableContactPeersView) -> (Bag<(MutableContactPeersView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.contactPeersViews.add(record) return (index, record.1.signal()) } func removeContactPeersView(_ index: Bag<(MutableContactPeersView, ValuePipe)>.Index) { self.contactPeersViews.remove(index) } func addPeerView(_ view: MutablePeerView) -> (Bag<(MutablePeerView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.peerViews.add(record) return (index, record.1.signal()) } func removePeerView(_ index: Bag<(MutablePeerView, ValuePipe)>.Index) { self.peerViews.remove(index) } func addUnreadMessageCountsView(_ view: MutableUnreadMessageCountsView) -> (Bag<(MutableUnreadMessageCountsView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.unreadMessageCountsViews.add(record) return (index, record.1.signal()) } func removeUnreadMessageCountsView(_ index: Bag<(MutableUnreadMessageCountsView, ValuePipe)>.Index) { self.unreadMessageCountsViews.remove(index) } func addPeerMergedOperationLogView(_ view: MutablePeerMergedOperationLogView) -> (Bag<(MutablePeerMergedOperationLogView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.peerMergedOperationLogViews.add(record) return (index, record.1.signal()) } func removePeerMergedOperationLogView(_ index: Bag<(MutablePeerMergedOperationLogView, ValuePipe)>.Index) { self.peerMergedOperationLogViews.remove(index) } func addTimestampBasedMessageAttributesView(_ view: MutableTimestampBasedMessageAttributesView) -> (Bag<(MutableTimestampBasedMessageAttributesView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.timestampBasedMessageAttributesViews.add(record) return (index, record.1.signal()) } func removeTimestampBasedMessageAttributesView(_ index: Bag<(MutableTimestampBasedMessageAttributesView, ValuePipe)>.Index) { self.timestampBasedMessageAttributesViews.remove(index) } func addMessageView(_ view: MutableMessageView) -> (Bag<(MutableMessageView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.messageViews.add(record) return (index, record.1.signal()) } func removeMessageView(_ index: Bag<(MutableMessageView, ValuePipe)>.Index) { self.messageViews.remove(index) } func addPreferencesView(_ view: MutablePreferencesView) -> (Bag<(MutablePreferencesView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.preferencesViews.add(record) return (index, record.1.signal()) } func removePreferencesView(_ index: Bag<(MutablePreferencesView, ValuePipe)>.Index) { self.preferencesViews.remove(index) } func addMultiplePeersView(_ view: MutableMultiplePeersView) -> (Bag<(MutableMultiplePeersView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.multiplePeersViews.add(record) return (index, record.1.signal()) } func removeMultiplePeersView(_ index: Bag<(MutableMultiplePeersView, ValuePipe)>.Index) { self.multiplePeersViews.remove(index) } func addItemCollectionView(_ view: MutableItemCollectionsView) -> (Bag<(MutableItemCollectionsView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.itemCollectionsViews.add(record) return (index, record.1.signal()) } func removeItemCollectionView(_ index: Bag<(MutableItemCollectionsView, ValuePipe)>.Index) { self.itemCollectionsViews.remove(index) } func addCombinedView(_ view: CombinedMutableView) -> (Bag<(CombinedMutableView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.combinedViews.add(record) return (index, record.1.signal()) } func removeCombinedView(_ index: Bag<(CombinedMutableView, ValuePipe)>.Index) { self.combinedViews.remove(index) } func refreshViewsDueToExternalTransaction(postbox: Postbox, fetchUnsentMessageIds: () -> [MessageId], fetchSynchronizePeerReadStateOperations: () -> [PeerId: PeerReadStateSynchronizationOperation]) { var updateTrackedHoles = false for (mutableView, pipe) in self.messageHistoryViews.copyItems() { if mutableView.refreshDueToExternalTransaction(postbox: postbox) { pipe.putNext((MessageHistoryView(mutableView), .Generic)) updateTrackedHoles = true } } for (mutableView, pipe) in self.chatListViews.copyItems() { if mutableView.refreshDueToExternalTransaction(postbox: postbox) { mutableView.render(postbox: postbox, renderMessage: self.renderMessage, getPeer: { id in return self.getPeer(id) }, getPeerNotificationSettings: self.getPeerNotificationSettings, getPeerPresence: self.getPeerPresence) pipe.putNext((ChatListView(mutableView), .Generic)) } } if updateTrackedHoles { self.updateTrackedHoles() } if self.unsentMessageView.refreshDueToExternalTransaction(fetchUnsentMessageIds: fetchUnsentMessageIds) { self.unsentViewUpdated() } if self.synchronizeReadStatesView.refreshDueToExternalTransaction(fetchSynchronizePeerReadStateOperations: fetchSynchronizePeerReadStateOperations) { self.synchronizeReadStateViewUpdated() } for (mutableView, pipe) in self.peerViews.copyItems() { if mutableView.reset(postbox: postbox) { pipe.putNext(PeerView(mutableView)) } } } func updateViews(postbox: Postbox, transaction: PostboxTransaction) { var updateTrackedHoles = false if let currentUpdatedState = transaction.currentUpdatedState { for (mutableView, pipe) in self.postboxStateViews.copyItems() { if mutableView.replay(updatedState: currentUpdatedState) { pipe.putNext(PostboxStateView(mutableView)) } } } for (mutableView, pipe) in self.messageHistoryViews.copyItems() { var updated = false let previousPeerIds = mutableView.peerIds if mutableView.replay(postbox: postbox, transaction: transaction) { updated = true } var updateType: ViewUpdateType = .Generic switch mutableView.peerIds { case let .single(peerId): for key in transaction.currentPeerHoleOperations.keys { if key.peerId == peerId { updateType = .FillHole break } } case .associated: var ids = Set() switch mutableView.peerIds { case .single: assertionFailure() case let .associated(mainPeerId, associatedId): ids.insert(mainPeerId) if let associatedId = associatedId { ids.insert(associatedId.peerId) } } if !ids.isEmpty { for key in transaction.currentPeerHoleOperations.keys { if ids.contains(key.peerId) { updateType = .FillHole break } } } } mutableView.updatePeerIds(transaction: transaction) if mutableView.peerIds != previousPeerIds { updateType = .UpdateVisible let _ = mutableView.refreshDueToExternalTransaction(postbox: postbox) updated = true } if updated { updateTrackedHoles = true pipe.putNext((MessageHistoryView(mutableView), updateType)) } } for (mutableView, pipe) in self.messageViews.copyItems() { let operations = transaction.currentOperationsByPeerId[mutableView.messageId.peerId] if operations != nil || !transaction.updatedMedia.isEmpty || !transaction.currentUpdatedCachedPeerData.isEmpty { if mutableView.replay(operations ?? [], updatedMedia: transaction.updatedMedia, renderIntermediateMessage: self.renderMessage) { pipe.putNext(MessageView(mutableView)) } } } if !transaction.chatListOperations.isEmpty || !transaction.currentUpdatedPeerNotificationSettings.isEmpty || !transaction.currentUpdatedPeers.isEmpty || !transaction.currentInvalidateMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentOperationsByPeerId.isEmpty || transaction.replacedAdditionalChatListItems != nil || !transaction.currentUpdatedPeerPresences.isEmpty { for (mutableView, pipe) in self.chatListViews.copyItems() { let context = MutableChatListViewReplayContext() if mutableView.replay(postbox: postbox, operations: transaction.chatListOperations, updatedPeerNotificationSettings: transaction.currentUpdatedPeerNotificationSettings, updatedPeers: transaction.currentUpdatedPeers, updatedPeerPresences: transaction.currentUpdatedPeerPresences, transaction: transaction, context: context) { mutableView.complete(postbox: postbox, context: context) mutableView.render(postbox: postbox, renderMessage: self.renderMessage, getPeer: { id in return self.getPeer(id) }, getPeerNotificationSettings: self.getPeerNotificationSettings, getPeerPresence: self.getPeerPresence) pipe.putNext((ChatListView(mutableView), .Generic)) } } self.updateTrackedChatListHoles() } if updateTrackedHoles { self.updateTrackedHoles() } if self.unsentMessageView.replay(transaction.unsentMessageOperations) { self.unsentViewUpdated() } if self.synchronizeReadStatesView.replay(transaction.updatedSynchronizePeerReadStateOperations) { self.synchronizeReadStateViewUpdated() } for (view, pipe) in self.unreadMessageCountsViews.copyItems() { if view.replay(postbox: postbox, transaction: transaction) { pipe.putNext(UnreadMessageCountsView(view)) } } if let replaceContactPeerIds = transaction.replaceContactPeerIds { for (mutableView, pipe) in self.contactPeerIdsViews.copyItems() { if mutableView.replay(updateRemoteTotalCount: transaction.replaceRemoteContactCount, replace: replaceContactPeerIds) { pipe.putNext(ContactPeerIdsView(mutableView)) } } } for (mutableView, pipe) in self.contactPeersViews.copyItems() { if mutableView.replay(replacePeerIds: transaction.replaceContactPeerIds, updatedPeerPresences: transaction.currentUpdatedPeerPresences, getPeer: self.getPeer, getPeerPresence: self.getPeerPresence) { pipe.putNext(ContactPeersView(mutableView)) } } for (mutableView, pipe) in self.peerViews.copyItems() { if mutableView.replay(postbox: postbox, transaction: transaction) { pipe.putNext(PeerView(mutableView)) } } for (mutableView, pipe) in self.peerMergedOperationLogViews.copyItems() { if mutableView.replay(operations: transaction.currentPeerMergedOperationLogOperations, getOperations: self.operationLogGetOperations, getTailIndex: self.operationLogGetTailIndex) { pipe.putNext(PeerMergedOperationLogView(mutableView)) } } for (mutableView, pipe) in self.timestampBasedMessageAttributesViews.copyItems() { if mutableView.replay(operations: transaction.currentTimestampBasedMessageAttributesOperations, getHead: self.getTimestampBasedMessageAttributesHead) { pipe.putNext(TimestampBasedMessageAttributesView(mutableView)) } } for (mutableView, pipe) in self.preferencesViews.copyItems() { if mutableView.replay(postbox: postbox, transaction: transaction) { pipe.putNext(PreferencesView(mutableView)) } } for (mutableView, pipe) in self.multiplePeersViews.copyItems() { if mutableView.replay(updatedPeers: transaction.currentUpdatedPeers, updatedPeerPresences: transaction.currentUpdatedPeerPresences) { pipe.putNext(MultiplePeersView(mutableView)) } } for (mutableView, pipe) in self.itemCollectionsViews.copyItems() { if mutableView.replay(postbox: postbox, transaction: transaction) { pipe.putNext(ItemCollectionsView(mutableView)) } } for (mutableView, pipe) in self.combinedViews.copyItems() { if mutableView.replay(postbox: postbox, transaction: transaction) { pipe.putNext(mutableView.immutableView()) } } } private func updateTrackedChatListHoles() { var firstHoles = Set() for (view, _) in self.chatListViews.copyItems() { if let hole = view.firstHole() { firstHoles.insert(ChatListHolesEntry(groupId: view.groupId, hole: hole)) } } if self.chatListHolesView.update(holes: firstHoles) { for pipe in self.chatListHolesViewSubscribers.copyItems() { pipe.putNext(ChatListHolesView(self.chatListHolesView)) } } } private func updateTrackedHoles() { var firstHolesAndTags = Set() for (view, _) in self.messageHistoryViews.copyItems() { if let (hole, direction) = view.firstHole() { let space: MessageHistoryHoleSpace if let tag = view.tag { space = .tag(tag) } else { space = .everywhere } firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space)) } } if self.messageHistoryHolesView.update(firstHolesAndTags) { for subscriber in self.messageHistoryHolesViewSubscribers.copyItems() { subscriber.putNext(MessageHistoryHolesView(self.messageHistoryHolesView)) } } } private func unsentViewUpdated() { for subscriber in self.unsendMessageIdsViewSubscribers.copyItems() { subscriber.putNext(UnsentMessageIdsView(self.unsentMessageView.ids)) } } private func synchronizeReadStateViewUpdated() { for subscriber in self.synchronizePeerReadStatesViewSubscribers.copyItems() { subscriber.putNext(SynchronizePeerReadStatesView(self.synchronizeReadStatesView)) } } func messageHistoryHolesViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { subscriber.putNext(MessageHistoryHolesView(self.messageHistoryHolesView)) let pipe = ValuePipe() let index = self.messageHistoryHolesViewSubscribers.add(pipe) let pipeDisposable = pipe.signal().start(next: { view in subscriber.putNext(view) }) disposable.set(ActionDisposable { self.queue.async { pipeDisposable.dispose() self.messageHistoryHolesViewSubscribers.remove(index) } }) } return disposable } } func chatListHolesViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { subscriber.putNext(ChatListHolesView(self.chatListHolesView)) let pipe = ValuePipe() let index = self.chatListHolesViewSubscribers.add(pipe) let pipeDisposable = pipe.signal().start(next: { view in subscriber.putNext(view) }) disposable.set(ActionDisposable { self.queue.async { pipeDisposable.dispose() self.chatListHolesViewSubscribers.remove(index) } }) } return disposable } } func unsentMessageIdsViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { subscriber.putNext(UnsentMessageIdsView(self.unsentMessageView.ids)) let pipe = ValuePipe() let index = self.unsendMessageIdsViewSubscribers.add(pipe) let pipeDisposable = pipe.signal().start(next: { view in subscriber.putNext(view) }) disposable.set(ActionDisposable { self.queue.async { pipeDisposable.dispose() self.unsendMessageIdsViewSubscribers.remove(index) } }) } return disposable } } func synchronizePeerReadStatesViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { subscriber.putNext(SynchronizePeerReadStatesView(self.synchronizeReadStatesView)) let pipe = ValuePipe() let index = self.synchronizePeerReadStatesViewSubscribers.add(pipe) let pipeDisposable = pipe.signal().start(next: { view in subscriber.putNext(view) }) disposable.set(ActionDisposable { self.queue.async { pipeDisposable.dispose() self.synchronizePeerReadStatesViewSubscribers.remove(index) } }) } return disposable } } }