import Foundation import SwiftSignalKit public enum ViewUpdateType : Equatable { case Initial case InitialUnread(MessageIndex) case Generic case FillHole case UpdateVisible } final class ViewTracker { private let queue: Queue private var chatListViews = Bag<(MutableChatListView, ValuePipe<(ChatListView, ViewUpdateType)>)>() private var messageHistoryViews = Bag<(MutableMessageHistoryView, ValuePipe<(MessageHistoryView, ViewUpdateType)>)>() private var contactPeerIdsViews = Bag<(MutableContactPeerIdsView, ValuePipe)>() private let messageHistoryHolesView = MutableMessageHistoryHolesView() private let messageHistoryHolesViewSubscribers = Bag>() private let externalMessageHistoryHolesView = MutableMessageHistoryExternalHolesView() private let externalMessageHistoryHolesViewSubscribers = Bag>() private let chatListHolesView = MutableChatListHolesView() private let chatListHolesViewSubscribers = Bag>() private let forumTopicListHolesView = MutableForumTopicListHolesView() private let forumTopicListHolesViewSubscribers = 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 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)>() private var failedMessageIdsViews = Bag<(MutableFailedMessageIdsView, ValuePipe)>() init( queue: Queue, unsentMessageIds: [MessageId], synchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation] ) { self.queue = queue 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) { #if DEBUG assert(self.messageHistoryViews.get(index) != nil) #endif 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 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) if view.views.keys.contains(where: { key in switch key { case .messageHistoryThreadIndex: return true default: return false } }) { self.updateTrackedForumTopicListHoles() } return (index, record.1.signal()) } func removeCombinedView(_ index: Bag<(CombinedMutableView, ValuePipe)>.Index) { self.combinedViews.remove(index) } func refreshViewsDueToExternalTransaction(postbox: PostboxImpl, currentTransaction: Transaction, 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, currentTransaction: currentTransaction) { mutableView.render(postbox: postbox) 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: PostboxImpl, currentTransaction: Transaction, 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, threadId): for key in transaction.currentPeerHoleOperations.keys { if key.peerId == peerId && key.threadId == threadId { updateType = .FillHole break } } case .associated: var ids = Set() switch mutableView.peerIds { case .single, .external: 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 } } } case .external: 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(postbox: postbox, operations: operations ?? [], updatedMedia: transaction.updatedMedia) { pipe.putNext(MessageView(mutableView)) } } } for (mutableView, pipe) in self.chatListViews.copyItems() { let context = MutableChatListViewReplayContext() if mutableView.replay(postbox: postbox, currentTransaction: currentTransaction, 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) 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.peerViews.copyItems() { if mutableView.replay(postbox: postbox, transaction: transaction) { pipe.putNext(PeerView(mutableView)) } } for (mutableView, pipe) in self.peerMergedOperationLogViews.copyItems() { if mutableView.replay(postbox: postbox, operations: transaction.currentPeerMergedOperationLogOperations) { pipe.putNext(PeerMergedOperationLogView(mutableView)) } } for (mutableView, pipe) in self.timestampBasedMessageAttributesViews.copyItems() { if mutableView.replay(postbox: postbox, operations: transaction.currentTimestampBasedMessageAttributesOperations) { 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()) } } for (view, pipe) in self.failedMessageIdsViews.copyItems() { if view.replay(postbox: postbox, transaction: transaction) { pipe.putNext(view.immutableView()) } } self.updateTrackedForumTopicListHoles() } private func updateTrackedChatListHoles() { var firstHoles = Set() for (view, _) in self.chatListViews.copyItems() { if let hole = view.firstHole() { firstHoles.insert(ChatListHolesEntry(groupId: hole.0, hole: hole.1)) } } if self.chatListHolesView.update(holes: firstHoles) { for pipe in self.chatListHolesViewSubscribers.copyItems() { pipe.putNext(ChatListHolesView(self.chatListHolesView)) } } } private func updateTrackedForumTopicListHoles() { var firstHoles = Set() for (views) in self.combinedViews.copyItems() { for (key, view) in views.0.views { if case .messageHistoryThreadIndex = key, let view = view as? MutableMessageHistoryThreadIndexView { if let hole = view.topHole() { firstHoles.insert(hole) } } else if case .savedMessagesIndex = key, let view = view as? MutableMessageHistorySavedMessagesIndexView { if let hole = view.topHole() { firstHoles.insert(hole) } } } } if self.forumTopicListHolesView.update(holes: firstHoles) { for pipe in self.forumTopicListHolesViewSubscribers.copyItems() { pipe.putNext(ForumTopicListHolesView(self.forumTopicListHolesView)) } } } private func updateTrackedHoles() { var firstHolesAndTags = Set() for (view, _) in self.messageHistoryViews.copyItems() { if let (hole, direction, count, userId) = view.firstHole() { let space: MessageHistoryHoleOperationSpace if let tag = view.tag { switch tag { case let .tag(value): space = .tag(value) case let .customTag(value, regularTag): space = .customTag(value, regularTag) } } else { space = .everywhere } firstHolesAndTags.insert(MessageHistoryHolesViewEntry(hole: hole, direction: direction, space: space, count: count, userId: userId)) } } 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 forumTopicListHolesViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { subscriber.putNext(ForumTopicListHolesView(self.forumTopicListHolesView)) let pipe = ValuePipe() let index = self.forumTopicListHolesViewSubscribers.add(pipe) let pipeDisposable = pipe.signal().start(next: { view in subscriber.putNext(view) }) disposable.set(ActionDisposable { self.queue.async { pipeDisposable.dispose() self.forumTopicListHolesViewSubscribers.remove(index) } }) } return disposable } } func unsentMessageIdsViewSignal() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { postboxLog("unsentMessageIdsViewSignal started with \(self.unsentMessageView.ids)") 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 } } func addFailedMessageIdsView(_ view: MutableFailedMessageIdsView) -> (Bag<(MutableFailedMessageIdsView, ValuePipe)>.Index, Signal) { let record = (view, ValuePipe()) let index = self.failedMessageIdsViews.add(record) return (index, record.1.signal()) } func removeFailedMessageIdsView(_ index: Bag<(MutableFailedMessageIdsView, ValuePipe)>.Index) { self.failedMessageIdsViews.remove(index) } }