From 9b190d7e6b4fe8dfcc95ccc9570d72aef51ae428 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 16 Aug 2019 05:02:43 +0300 Subject: [PATCH] Scheduled messages fixes --- .../GalleryUI/Sources/GalleryController.swift | 13 +- .../Postbox/Postbox/MessageHistoryView.swift | 41 +- .../Postbox/MessageHistoryViewState.swift | 34 +- .../Postbox/MessageOfInterestHolesView.swift | 4 +- submodules/Postbox/Postbox/Postbox.swift | 16 +- .../TelegramCore/TelegramCore/Account.swift | 4 +- .../TelegramCore/AccountManager.swift | 2 +- .../TelegramCore/AccountViewTracker.swift | 91 ++- .../TelegramCore/EnqueueMessage.swift | 13 +- .../HistoryViewChannelStateValidation.swift | 610 --------------- .../HistoryViewStateValidation.swift | 704 ++++++++++++++++++ .../TelegramCore/TelegramCore/Holes.swift | 227 +++--- ...edSynchronizeEmojiKeywordsOperations.swift | 2 +- .../TelegramCore/Namespaces.swift | 3 + .../TelegramCore/ScheduledMessages.swift | 178 ++++- .../TelegramCore/SearchStickers.swift | 11 +- .../project.pbxproj | 12 +- .../TelegramUI/ChatController.swift | 58 +- .../ChatHistoryViewForLocation.swift | 1 - .../TelegramUI/ChatMessageItem.swift | 8 +- .../ChatScheduleTimeControllerNode.swift | 15 +- .../PeerMessagesMediaPlaylist.swift | 24 +- 22 files changed, 1170 insertions(+), 901 deletions(-) delete mode 100644 submodules/TelegramCore/TelegramCore/HistoryViewChannelStateValidation.swift create mode 100644 submodules/TelegramCore/TelegramCore/HistoryViewStateValidation.swift diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 52dc67ace8..5b4c9ef05d 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -368,16 +368,13 @@ public class GalleryController: ViewController { switch source { case .peerMessagesAtId: if let tags = tagsForMessage(message!) { - var excludeNamespaces: [MessageId.Namespace] - if message!.id.namespace == Namespaces.Message.ScheduledCloud { - excludeNamespaces = [Namespaces.Message.Cloud, Namespaces.Message.Local, Namespaces.Message.SecretIncoming] + let namespaces: HistoryViewNamespaces + if Namespaces.Message.allScheduled.contains(message!.id.namespace) { + namespaces = .just(Namespaces.Message.allScheduled) } else { - excludeNamespaces = [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal] + namespaces = .not(Namespaces.Message.allScheduled) } - - let view = context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, excludeNamespaces: excludeNamespaces, orderStatistics: [.combinedLocation]) - - return view + return context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, namespaces: namespaces, orderStatistics: [.combinedLocation]) |> mapToSignal { (view, _, _) -> Signal in let mapped = GalleryMessageHistoryView.view(view) return .single(mapped) diff --git a/submodules/Postbox/Postbox/MessageHistoryView.swift b/submodules/Postbox/Postbox/MessageHistoryView.swift index bdc9608b73..c21ad9de35 100644 --- a/submodules/Postbox/Postbox/MessageHistoryView.swift +++ b/submodules/Postbox/Postbox/MessageHistoryView.swift @@ -224,10 +224,27 @@ public enum HistoryViewInputAnchor: Equatable { case unread } +public enum HistoryViewNamespaces { + case all + case just(Set) + case not(Set) + + public func contains(_ namespace: MessageId.Namespace) -> Bool { + switch self { + case .all: + return true + case let .just(namespaces): + return namespaces.contains(namespace) + case let .not(namespaces): + return !namespaces.contains(namespace) + } + } +} + final class MutableMessageHistoryView { private(set) var peerIds: MessageHistoryViewPeerIds let tag: MessageTags? - let excludeNamespaces: [MessageId.Namespace] + let namespaces: HistoryViewNamespaces private let orderStatistics: MessageHistoryViewOrderStatistics private let anchor: HistoryViewInputAnchor @@ -242,7 +259,7 @@ final class MutableMessageHistoryView { fileprivate(set) var sampledState: HistoryViewSample - init(postbox: Postbox, orderStatistics: MessageHistoryViewOrderStatistics, peerIds: MessageHistoryViewPeerIds, anchor inputAnchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState?, transientReadStates: MessageHistoryViewReadState?, tag: MessageTags?, excludeNamespaces: [MessageId.Namespace], count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], additionalDatas: [AdditionalMessageHistoryViewDataEntry], getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32) { + init(postbox: Postbox, orderStatistics: MessageHistoryViewOrderStatistics, peerIds: MessageHistoryViewPeerIds, anchor inputAnchor: HistoryViewInputAnchor, combinedReadStates: MessageHistoryViewReadState?, transientReadStates: MessageHistoryViewReadState?, tag: MessageTags?, namespaces: HistoryViewNamespaces, count: Int, topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?], additionalDatas: [AdditionalMessageHistoryViewDataEntry], getMessageCountInRange: (MessageIndex, MessageIndex) -> Int32) { self.anchor = inputAnchor self.orderStatistics = orderStatistics @@ -250,17 +267,17 @@ final class MutableMessageHistoryView { self.combinedReadStates = combinedReadStates self.transientReadStates = transientReadStates self.tag = tag - self.excludeNamespaces = excludeNamespaces + self.namespaces = namespaces self.fillCount = count self.topTaggedMessages = topTaggedMessages self.additionalDatas = additionalDatas - self.state = HistoryViewState(postbox: postbox, inputAnchor: inputAnchor, tag: tag, excludeNamespaces: excludeNamespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds) + self.state = HistoryViewState(postbox: postbox, inputAnchor: inputAnchor, tag: tag, namespaces: namespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds) if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, excludeNamespaces: excludeNamespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, namespaces: namespaces, statistics: self.orderStatistics, halfLimit: count + 1, locations: peerIds, postbox: postbox, holes: holes)) self.sampledState = self.state.sample(postbox: postbox) case .loadHole: break @@ -272,12 +289,12 @@ final class MutableMessageHistoryView { } private func reset(postbox: Postbox) { - self.state = HistoryViewState(postbox: postbox, inputAnchor: self.anchor, tag: self.tag, excludeNamespaces: self.excludeNamespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds) + self.state = HistoryViewState(postbox: postbox, inputAnchor: self.anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds) if case let .loading(loadingState) = self.state { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, excludeNamespaces: self.excludeNamespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) case .loadHole: break } @@ -286,7 +303,7 @@ final class MutableMessageHistoryView { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, excludeNamespaces: self.excludeNamespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) case .loadHole: break } @@ -472,7 +489,7 @@ final class MutableMessageHistoryView { let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, excludeNamespaces: self.excludeNamespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) + self.state = .loaded(HistoryViewLoadedState(anchor: anchor, tag: self.tag, namespaces: self.namespaces, statistics: self.orderStatistics, halfLimit: self.fillCount + 1, locations: self.peerIds, postbox: postbox, holes: holes)) case .loadHole: break } @@ -672,6 +689,7 @@ final class MutableMessageHistoryView { public final class MessageHistoryView { public let tagMask: MessageTags? + public let namespaces: HistoryViewNamespaces public let anchorIndex: MessageHistoryAnchorIndex public let earlierId: MessageIndex? public let laterId: MessageIndex? @@ -686,6 +704,7 @@ public final class MessageHistoryView { init(_ mutableView: MutableMessageHistoryView) { self.tagMask = mutableView.tag + self.namespaces = mutableView.namespaces var entries: [MessageHistoryEntry] switch mutableView.sampledState { case .loading: @@ -714,7 +733,7 @@ public final class MessageHistoryView { entries = [] if let transientReadStates = mutableView.transientReadStates, case let .peer(states) = transientReadStates { for entry in state.entries { - if !mutableView.excludeNamespaces.contains(entry.message.id.namespace) { + if mutableView.namespaces.contains(entry.message.id.namespace) { let read: Bool if entry.message.flags.contains(.Incoming) { read = false @@ -728,7 +747,7 @@ public final class MessageHistoryView { } } else { for entry in state.entries { - if !mutableView.excludeNamespaces.contains(entry.message.id.namespace) { + if mutableView.namespaces.contains(entry.message.id.namespace) { entries.append(MessageHistoryEntry(message: entry.message, isRead: false, location: entry.location, monthLocation: entry.monthLocation, attributes: entry.attributes)) } } diff --git a/submodules/Postbox/Postbox/MessageHistoryViewState.swift b/submodules/Postbox/Postbox/MessageHistoryViewState.swift index f6f5d5d38c..10a6084cfb 100644 --- a/submodules/Postbox/Postbox/MessageHistoryViewState.swift +++ b/submodules/Postbox/Postbox/MessageHistoryViewState.swift @@ -748,7 +748,7 @@ struct HistoryViewLoadedSample { final class HistoryViewLoadedState { let anchor: HistoryViewAnchor let tag: MessageTags? - let excludeNamespaces: [MessageId.Namespace] + let namespaces: HistoryViewNamespaces let statistics: MessageHistoryViewOrderStatistics let halfLimit: Int let seedConfiguration: SeedConfiguration @@ -756,11 +756,11 @@ final class HistoryViewLoadedState { var holes: HistoryViewHoles var spacesWithRemovals = Set() - init(anchor: HistoryViewAnchor, tag: MessageTags?, excludeNamespaces: [MessageId.Namespace], statistics: MessageHistoryViewOrderStatistics, halfLimit: Int, locations: MessageHistoryViewPeerIds, postbox: Postbox, holes: HistoryViewHoles) { + init(anchor: HistoryViewAnchor, tag: MessageTags?, namespaces: HistoryViewNamespaces, statistics: MessageHistoryViewOrderStatistics, halfLimit: Int, locations: MessageHistoryViewPeerIds, postbox: Postbox, holes: HistoryViewHoles) { precondition(halfLimit >= 3) self.anchor = anchor self.tag = tag - self.excludeNamespaces = excludeNamespaces + self.namespaces = namespaces self.statistics = statistics self.halfLimit = halfLimit self.seedConfiguration = postbox.seedConfiguration @@ -781,7 +781,7 @@ final class HistoryViewLoadedState { var spaces: [PeerIdAndNamespace] = [] for peerId in peerIds { for namespace in postbox.messageHistoryIndexTable.existingNamespaces(peerId: peerId) { - if !excludeNamespaces.contains(namespace) { + if namespaces.contains(namespace) { spaces.append(PeerIdAndNamespace(peerId: peerId, namespace: namespace)) } } @@ -1178,7 +1178,7 @@ final class HistoryViewLoadedState { } } -private func fetchHoles(postbox: Postbox, locations: MessageHistoryViewPeerIds, tag: MessageTags?, excludeNamespaces: [MessageId.Namespace]) -> [PeerIdAndNamespace: IndexSet] { +private func fetchHoles(postbox: Postbox, locations: MessageHistoryViewPeerIds, tag: MessageTags?, namespaces: HistoryViewNamespaces) -> [PeerIdAndNamespace: IndexSet] { var holesBySpace: [PeerIdAndNamespace: IndexSet] = [:] var peerIds: [PeerId] = [] switch locations { @@ -1193,7 +1193,7 @@ private func fetchHoles(postbox: Postbox, locations: MessageHistoryViewPeerIds, let holeSpace = tag.flatMap(MessageHistoryHoleSpace.tag) ?? .everywhere for peerId in peerIds { for namespace in postbox.messageHistoryHoleIndexTable.existingNamespaces(peerId: peerId, holeSpace: holeSpace) { - if !excludeNamespaces.contains(namespace) { + if namespaces.contains(namespace) { let indices = postbox.messageHistoryHoleIndexTable.closest(peerId: peerId, namespace: namespace, space: holeSpace, range: 1 ... (Int32.max - 1)) if !indices.isEmpty { let peerIdAndNamespace = PeerIdAndNamespace(peerId: peerId, namespace: namespace) @@ -1217,11 +1217,11 @@ final class HistoryViewLoadingState { let halfLimit: Int var holes: HistoryViewHoles - init(postbox: Postbox, locations: MessageHistoryViewPeerIds, tag: MessageTags?, excludeNamespaces: [MessageId.Namespace], messageId: MessageId, halfLimit: Int) { + init(postbox: Postbox, locations: MessageHistoryViewPeerIds, tag: MessageTags?, namespaces: HistoryViewNamespaces, messageId: MessageId, halfLimit: Int) { self.messageId = messageId self.tag = tag self.halfLimit = halfLimit - self.holes = HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces)) + self.holes = HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)) } func insertHole(space: PeerIdAndNamespace, range: ClosedRange) -> Bool { @@ -1261,14 +1261,14 @@ enum HistoryViewState { case loaded(HistoryViewLoadedState) case loading(HistoryViewLoadingState) - init(postbox: Postbox, inputAnchor: HistoryViewInputAnchor, tag: MessageTags?, excludeNamespaces: [MessageId.Namespace], statistics: MessageHistoryViewOrderStatistics, halfLimit: Int, locations: MessageHistoryViewPeerIds) { + init(postbox: Postbox, inputAnchor: HistoryViewInputAnchor, tag: MessageTags?, namespaces: HistoryViewNamespaces, statistics: MessageHistoryViewOrderStatistics, halfLimit: Int, locations: MessageHistoryViewPeerIds) { switch inputAnchor { case let .index(index): - self = .loaded(HistoryViewLoadedState(anchor: .index(index), tag: tag, excludeNamespaces: excludeNamespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces)))) + self = .loaded(HistoryViewLoadedState(anchor: .index(index), tag: tag, namespaces: namespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) case .lowerBound: - self = .loaded(HistoryViewLoadedState(anchor: .lowerBound, tag: tag, excludeNamespaces: excludeNamespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces)))) + self = .loaded(HistoryViewLoadedState(anchor: .lowerBound, tag: tag, namespaces: namespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) case .upperBound: - self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, excludeNamespaces: excludeNamespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces)))) + self = .loaded(HistoryViewLoadedState(anchor: .upperBound, tag: tag, namespaces: namespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) case .unread: let anchorPeerId: PeerId switch locations { @@ -1301,26 +1301,26 @@ enum HistoryViewState { } } if let messageId = messageId { - let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces, messageId: messageId, halfLimit: halfLimit) + let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces, messageId: messageId, halfLimit: halfLimit) let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, excludeNamespaces: excludeNamespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) + self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, namespaces: namespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) case .loadHole: self = .loading(loadingState) } } else { - self = .loaded(HistoryViewLoadedState(anchor: anchor ?? .upperBound, tag: tag, excludeNamespaces: excludeNamespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces)))) + self = .loaded(HistoryViewLoadedState(anchor: anchor ?? .upperBound, tag: tag, namespaces: namespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: HistoryViewHoles(holesBySpace: fetchHoles(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces)))) } } else { preconditionFailure() } case let .message(messageId): - let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, excludeNamespaces: excludeNamespaces, messageId: messageId, halfLimit: halfLimit) + let loadingState = HistoryViewLoadingState(postbox: postbox, locations: locations, tag: tag, namespaces: namespaces, messageId: messageId, halfLimit: halfLimit) let sampledState = loadingState.checkAndSample(postbox: postbox) switch sampledState { case let .ready(anchor, holes): - self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, excludeNamespaces: excludeNamespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) + self = .loaded(HistoryViewLoadedState(anchor: anchor, tag: tag, namespaces: namespaces, statistics: statistics, halfLimit: halfLimit, locations: locations, postbox: postbox, holes: holes)) case .loadHole: self = .loading(loadingState) } diff --git a/submodules/Postbox/Postbox/MessageOfInterestHolesView.swift b/submodules/Postbox/Postbox/MessageOfInterestHolesView.swift index 3458f527fc..b07fe3c366 100644 --- a/submodules/Postbox/Postbox/MessageOfInterestHolesView.swift +++ b/submodules/Postbox/Postbox/MessageOfInterestHolesView.swift @@ -52,7 +52,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { } } self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, excludeNamespaces: [], count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) let _ = self.updateFromView() } @@ -117,7 +117,7 @@ final class MutableMessageOfInterestHolesView: MutablePostboxView { if self.anchor != anchor { self.anchor = anchor - self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, excludeNamespaces: [], count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) + self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, namespaces: .all, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0}) return self.updateFromView() } else if self.wrappedView.replay(postbox: postbox, transaction: transaction) { return self.updateFromView() diff --git a/submodules/Postbox/Postbox/Postbox.swift b/submodules/Postbox/Postbox/Postbox.swift index 5992af79f4..005b38a090 100644 --- a/submodules/Postbox/Postbox/Postbox.swift +++ b/submodules/Postbox/Postbox/Postbox.swift @@ -2219,7 +2219,7 @@ public final class Postbox { return peerIds } - public func aroundMessageOfInterestHistoryViewForChatLocation(_ chatLocation: ChatLocation, count: Int, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, excludeNamespaces: [MessageId.Namespace], orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageOfInterestHistoryViewForChatLocation(_ chatLocation: ChatLocation, count: Int, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: HistoryViewNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.transactionSignal(userInteractive: true, { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, tagMask: tagMask) @@ -2263,26 +2263,26 @@ public final class Postbox { } } } - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, excludeNamespaces: excludeNamespaces, orderStatistics: orderStatistics, additionalData: additionalData) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: anchor, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) }) } - public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, messageId: MessageId, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, excludeNamespaces: [MessageId.Namespace], orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, messageId: MessageId, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: HistoryViewNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.transactionSignal { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, tagMask: tagMask) - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: .message(messageId), fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, excludeNamespaces: excludeNamespaces, orderStatistics: orderStatistics, additionalData: additionalData) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: .message(messageId), fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) } } - public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, anchor: HistoryViewInputAnchor, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, excludeNamespaces: [MessageId.Namespace], orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, anchor: HistoryViewInputAnchor, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: HistoryViewNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { return self.transactionSignal { subscriber, transaction in let peerIds = self.peerIdsForLocation(chatLocation, tagMask: tagMask) - return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: anchor, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, excludeNamespaces: excludeNamespaces, orderStatistics: orderStatistics, additionalData: additionalData) + return self.syncAroundMessageHistoryViewForPeerId(subscriber: subscriber, peerIds: peerIds, count: count, anchor: anchor, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: topTaggedMessageIdNamespaces, tagMask: tagMask, namespaces: namespaces, orderStatistics: orderStatistics, additionalData: additionalData) } } - private func syncAroundMessageHistoryViewForPeerId(subscriber: Subscriber<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>, peerIds: MessageHistoryViewPeerIds, count: Int, anchor: HistoryViewInputAnchor, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, excludeNamespaces: [MessageId.Namespace], orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Disposable { + private func syncAroundMessageHistoryViewForPeerId(subscriber: Subscriber<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>, peerIds: MessageHistoryViewPeerIds, count: Int, anchor: HistoryViewInputAnchor, fixedCombinedReadStates: MessageHistoryViewReadState?, topTaggedMessageIdNamespaces: Set, tagMask: MessageTags?, namespaces: HistoryViewNamespaces, orderStatistics: MessageHistoryViewOrderStatistics, additionalData: [AdditionalMessageHistoryViewData]) -> Disposable { var topTaggedMessages: [MessageId.Namespace: MessageHistoryTopTaggedMessage?] = [:] var mainPeerId: PeerId? switch peerIds { @@ -2371,7 +2371,7 @@ public final class Postbox { readStates = transientReadStates } - let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, peerIds: peerIds, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tagMask, excludeNamespaces: excludeNamespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in + let mutableView = MutableMessageHistoryView(postbox: self, orderStatistics: orderStatistics, peerIds: peerIds, anchor: anchor, combinedReadStates: readStates, transientReadStates: transientReadStates, tag: tagMask, namespaces: namespaces, count: count, topTaggedMessages: topTaggedMessages, additionalDatas: additionalDataEntries, getMessageCountInRange: { lowerBound, upperBound in if let tagMask = tagMask { return Int32(self.messageHistoryTable.getMessageCountInRange(peerId: lowerBound.id.peerId, namespace: lowerBound.id.namespace, tag: tagMask, lowerBound: lowerBound, upperBound: upperBound)) } else { diff --git a/submodules/TelegramCore/TelegramCore/Account.swift b/submodules/TelegramCore/TelegramCore/Account.swift index 1376734ed0..13ef1a2cfd 100644 --- a/submodules/TelegramCore/TelegramCore/Account.swift +++ b/submodules/TelegramCore/TelegramCore/Account.swift @@ -248,8 +248,7 @@ let telegramPostboxSeedConfiguration: SeedConfiguration = { var messageHoles: [PeerId.Namespace: [MessageId.Namespace: Set]] = [:] for peerNamespace in peerIdNamespacesWithInitialCloudMessageHoles { messageHoles[peerNamespace] = [ - Namespaces.Message.Cloud: Set(MessageTags.all), - Namespaces.Message.ScheduledCloud: Set(MessageTags.all) + Namespaces.Message.Cloud: Set(MessageTags.all) ] } @@ -1245,6 +1244,7 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedApplyPendingMessageReactionsActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) let importantBackgroundOperations: [Signal] = [ managedSynchronizeChatInputStateOperations(postbox: self.postbox, network: self.network) |> map { $0 ? AccountRunningImportantTasks.other : [] }, diff --git a/submodules/TelegramCore/TelegramCore/AccountManager.swift b/submodules/TelegramCore/TelegramCore/AccountManager.swift index 7ad94b7401..4e7f876d51 100644 --- a/submodules/TelegramCore/TelegramCore/AccountManager.swift +++ b/submodules/TelegramCore/TelegramCore/AccountManager.swift @@ -143,7 +143,6 @@ private var declaredEncodables: Void = { declareEncodable(EmojiKeywordCollectionInfo.self, f: { EmojiKeywordCollectionInfo(decoder: $0) }) declareEncodable(EmojiKeywordItem.self, f: { EmojiKeywordItem(decoder: $0) }) declareEncodable(SynchronizeEmojiKeywordsOperation.self, f: { SynchronizeEmojiKeywordsOperation(decoder: $0) }) - declareEncodable(CloudPhotoSizeMediaResource.self, f: { CloudPhotoSizeMediaResource(decoder: $0) }) declareEncodable(CloudDocumentSizeMediaResource.self, f: { CloudDocumentSizeMediaResource(decoder: $0) }) declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) }) @@ -153,6 +152,7 @@ private var declaredEncodables: Void = { declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) }) declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) }) declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) }) + declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/TelegramCore/AccountViewTracker.swift b/submodules/TelegramCore/TelegramCore/AccountViewTracker.swift index a054419a87..529d8e677d 100644 --- a/submodules/TelegramCore/TelegramCore/AccountViewTracker.swift +++ b/submodules/TelegramCore/TelegramCore/AccountViewTracker.swift @@ -189,7 +189,7 @@ private func wrappedHistoryViewAdditionalData(chatLocation: ChatLocation, additi switch chatLocation { case let .peer(peerId): if peerId.namespace == Namespaces.Peer.CloudChannel { - if result.index(where: { if case .peerChatState = $0 { return true } else { return false } }) == nil { + if result.firstIndex(where: { if case .peerChatState = $0 { return true } else { return false } }) == nil { result.append(.peerChatState(peerId)) } } @@ -872,9 +872,7 @@ public final class AccountViewTracker { strongSelf.updatePolls(viewId: viewId, messageIds: pollMessageIds, messages: pollMessageDict) if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel { strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0) - }/* else if case .group = chatLocation { - strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0) - }*/ + } } } }, disposed: { [weak self] viewId in @@ -887,8 +885,6 @@ public final class AccountViewTracker { if peerId.namespace == Namespaces.Peer.CloudChannel { strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil) } - /*case .group: - strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil)*/ } } } @@ -915,53 +911,54 @@ public final class AccountViewTracker { } public func scheduledMessagesViewForLocation(_ chatLocation: ChatLocation) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { - let signal = self.aroundMessageHistoryViewForLocation(chatLocation, index: .upperBound, anchorIndex: .upperBound, count: 200, fixedCombinedReadStates: nil, tagMask: nil, excludeNamespaces: [Namespaces.Message.Cloud, Namespaces.Message.Local], orderStatistics: []) - return withState(signal, { [weak self] () -> Int32 in - if let strongSelf = self { - return OSAtomicIncrement32(&strongSelf.nextViewId) - } else { - return -1 - } - }, next: { [weak self] next, viewId in - if let strongSelf = self { - strongSelf.queue.async { - let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: messageIds, localWebpages: localWebpages) - let (pollMessageIds, pollMessageDict) = pollMessages(entries: next.0.entries) - strongSelf.updatePolls(viewId: viewId, messageIds: pollMessageIds, messages: pollMessageDict) - strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0) + if let account = self.account { + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: nil, namespaces: .just(Namespaces.Message.allScheduled), orderStatistics: [], additionalData: []) + return withState(signal, { [weak self] () -> Int32 in + if let strongSelf = self { + return OSAtomicIncrement32(&strongSelf.nextViewId) + } else { + return -1 } - } - }, disposed: { [weak self] viewId in - if let strongSelf = self { - strongSelf.queue.async { - strongSelf.updatePendingWebpages(viewId: viewId, messageIds: [], localWebpages: [:]) - strongSelf.updatePolls(viewId: viewId, messageIds: [], messages: [:]) - strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil) + }, next: { [weak self] next, viewId in + if let strongSelf = self { + strongSelf.queue.async { + let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries) + strongSelf.updatePendingWebpages(viewId: viewId, messageIds: messageIds, localWebpages: localWebpages) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0) + } } - } - }) + }, disposed: { [weak self] viewId in + if let strongSelf = self { + strongSelf.queue.async { + strongSelf.updatePendingWebpages(viewId: viewId, messageIds: [], localWebpages: [:]) + strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil) + } + } + }) + } else { + return .never() + } } - public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, tagMask: MessageTags? = nil, excludeNamespaces: [MessageId.Namespace] = [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal], orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { - let signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, excludeNamespaces: excludeNamespaces, orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) + let signal = account.postbox.aroundMessageOfInterestHistoryViewForChatLocation(chatLocation, count: count, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal) } else { return .never() } } - public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, messageId: MessageId, tagMask: MessageTags? = nil, excludeNamespaces: [MessageId.Namespace] = [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal], orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundIdMessageHistoryViewForLocation(_ chatLocation: ChatLocation, count: Int, messageId: MessageId, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { - let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, count: count, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, excludeNamespaces: excludeNamespaces, orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) + let signal = account.postbox.aroundIdMessageHistoryViewForLocation(chatLocation, count: count, messageId: messageId, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal) } else { return .never() } } - public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags? = nil, excludeNamespaces: [MessageId.Namespace] = [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal], orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { + public func aroundMessageHistoryViewForLocation(_ chatLocation: ChatLocation, index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags? = nil, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> { if let account = self.account { let inputAnchor: HistoryViewInputAnchor switch index { @@ -972,7 +969,7 @@ public final class AccountViewTracker { case let .message(index): inputAnchor = .index(index) } - let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, count: count, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, excludeNamespaces: excludeNamespaces, orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) + let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: inputAnchor, count: count, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud], tagMask: tagMask, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: orderStatistics, additionalData: wrappedHistoryViewAdditionalData(chatLocation: chatLocation, additionalData: additionalData)) return wrappedMessageHistorySignal(chatLocation: chatLocation, signal: signal) } else { return .never() @@ -1188,18 +1185,18 @@ public final class AccountViewTracker { let pendingKey: PostboxViewKey = .pendingMessageActionsSummary(type: .consumeUnseenPersonalMessage, peerId: peerId, namespace: Namespaces.Message.Cloud) let summaryKey: PostboxViewKey = .historyTagSummaryView(tag: .unseenPersonalMessage, peerId: peerId, namespace: Namespaces.Message.Cloud) return account.postbox.combinedView(keys: [pendingKey, summaryKey]) - |> map { views -> Int32 in - var count: Int32 = 0 - if let view = views.views[pendingKey] as? PendingMessageActionsSummaryView { - count -= view.count + |> map { views -> Int32 in + var count: Int32 = 0 + if let view = views.views[pendingKey] as? PendingMessageActionsSummaryView { + count -= view.count + } + if let view = views.views[summaryKey] as? MessageHistoryTagSummaryView { + if let unseenCount = view.count { + count += unseenCount } - if let view = views.views[summaryKey] as? MessageHistoryTagSummaryView { - if let unseenCount = view.count { - count += unseenCount - } - } - return max(0, count) - } |> distinctUntilChanged + } + return max(0, count) + } |> distinctUntilChanged } else { return .never() } diff --git a/submodules/TelegramCore/TelegramCore/EnqueueMessage.swift b/submodules/TelegramCore/TelegramCore/EnqueueMessage.swift index ac1022b9ee..3b835c5dd0 100644 --- a/submodules/TelegramCore/TelegramCore/EnqueueMessage.swift +++ b/submodules/TelegramCore/TelegramCore/EnqueueMessage.swift @@ -387,14 +387,17 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } var messageNamespace = Namespaces.Message.Local + var effectiveTimestamp = timestamp for attribute in attributes { - if attribute is OutgoingScheduleInfoMessageAttribute { + if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute { messageNamespace = Namespaces.Message.ScheduledLocal + effectiveTimestamp = attribute.scheduleTime break } } - storeMessages.append(StoreMessage(peerId: peerId, namespace: messageNamespace, globallyUniqueId: randomId, groupingKey: localGroupingKey, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: mediaList)) + + storeMessages.append(StoreMessage(peerId: peerId, namespace: messageNamespace, globallyUniqueId: randomId, groupingKey: localGroupingKey, timestamp: effectiveTimestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: mediaList)) case let .forward(source, grouping, requestedAttributes): let sourceMessage = transaction.getMessage(source) if let sourceMessage = sourceMessage, let author = sourceMessage.author ?? sourceMessage.peers[sourceMessage.id.peerId] { @@ -506,12 +509,14 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, var messageNamespace = Namespaces.Message.Local var entitiesAttribute: TextEntitiesMessageAttribute? + var effectiveTimestamp = timestamp for attribute in attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entitiesAttribute = attribute } - if attribute is OutgoingScheduleInfoMessageAttribute { + if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute { messageNamespace = Namespaces.Message.ScheduledLocal + effectiveTimestamp = attribute.scheduleTime } } @@ -543,7 +548,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, augmentedMediaList = augmentedMediaList.map(convertForwardedMediaForSecretChat) } - storeMessages.append(StoreMessage(peerId: peerId, namespace: messageNamespace, globallyUniqueId: randomId, groupingKey: localGroupingKey, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: sourceMessage.text, attributes: attributes, media: augmentedMediaList)) + storeMessages.append(StoreMessage(peerId: peerId, namespace: messageNamespace, globallyUniqueId: randomId, groupingKey: localGroupingKey, timestamp: effectiveTimestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: sourceMessage.text, attributes: attributes, media: augmentedMediaList)) } } } diff --git a/submodules/TelegramCore/TelegramCore/HistoryViewChannelStateValidation.swift b/submodules/TelegramCore/TelegramCore/HistoryViewChannelStateValidation.swift deleted file mode 100644 index 02ac52a32e..0000000000 --- a/submodules/TelegramCore/TelegramCore/HistoryViewChannelStateValidation.swift +++ /dev/null @@ -1,610 +0,0 @@ -import Foundation -#if os(macOS) - import PostboxMac - import SwiftSignalKitMac - import MtProtoKitMac - import TelegramApiMac -#else - import Postbox - import SwiftSignalKit - import TelegramApi - #if BUCK - import MtProtoKit - #else - import MtProtoKitDynamic - #endif -#endif - -private final class HistoryStateValidationBatch { - private let disposable: Disposable - let invalidatedState: HistoryState - - var cancelledMessageIds = Set() - - init(disposable: Disposable, invalidatedState: HistoryState) { - self.disposable = disposable - self.invalidatedState = invalidatedState - } - - deinit { - disposable.dispose() - } -} - -private final class HistoryStateValidationContext { - var batchReferences: [MessageId: HistoryStateValidationBatch] = [:] -} - -private enum HistoryState { - case channel(PeerId, ChannelState) - //case group(PeerGroupId, TelegramPeerGroupState) - - var hasInvalidationIndex: Bool { - switch self { - case let .channel(_, state): - return state.invalidatedPts != nil - /*case let .group(_, state): - return state.invalidatedStateIndex != nil*/ - } - } - - func isMessageValid(_ message: Message) -> Bool { - switch self { - case let .channel(_, state): - if let invalidatedPts = state.invalidatedPts { - var messagePts: Int32? - inner: for attribute in message.attributes { - if let attribute = attribute as? ChannelMessageStateVersionAttribute { - messagePts = attribute.pts - break inner - } - } - var requiresValidation = false - if let messagePts = messagePts { - if messagePts < invalidatedPts { - requiresValidation = true - } - } else { - requiresValidation = true - } - - return !requiresValidation - } else { - return true - } - /*case let .group(_, state): - if let invalidatedStateIndex = state.invalidatedStateIndex { - var messageStateIndex: Int32? - inner: for attribute in message.attributes { - if let attribute = attribute as? PeerGroupMessageStateVersionAttribute { - messageStateIndex = attribute.stateIndex - break inner - } - } - var requiresValidation = false - if let messageStateIndex = messageStateIndex { - if messageStateIndex < invalidatedStateIndex { - requiresValidation = true - } - } else { - requiresValidation = true - } - return !requiresValidation - } else { - return true - }*/ - } - } - - func matchesPeerId(_ peerId: PeerId) -> Bool { - switch self { - case let .channel(statePeerId, state): - return statePeerId == peerId - /*case .group: - return true*/ - } - } -} - -private func slicedForValidationMessages(_ messages: [MessageId]) -> [[MessageId]] { - let block = 64 - - if messages.count <= block { - return [messages] - } else { - var result: [[MessageId]] = [] - var offset = 0 - while offset < messages.count { - result.append(Array(messages[offset ..< min(offset + block, messages.count)])) - offset += block - } - return result - } -} - -final class HistoryViewStateValidationContexts { - private let queue: Queue - private let postbox: Postbox - private let network: Network - private let accountPeerId: PeerId - - private var contexts: [Int32: HistoryStateValidationContext] = [:] - - init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId) { - self.queue = queue - self.postbox = postbox - self.network = network - self.accountPeerId = accountPeerId - } - - func updateView(id: Int32, view: MessageHistoryView?) { - assert(self.queue.isCurrent()) - guard let view = view, view.tagMask == nil || view.tagMask == MessageTags.unseenPersonalMessage else { - if self.contexts[id] != nil { - self.contexts.removeValue(forKey: id) - } - return - } - var historyState: HistoryState? - for entry in view.additionalData { - if case let .peerChatState(peerId, chatState) = entry { - if let chatState = chatState as? ChannelState { - historyState = .channel(peerId, chatState) - } - break - } - } - - if let historyState = historyState, historyState.hasInvalidationIndex { - var rangesToInvalidate: [[MessageId]] = [] - let addToRange: (MessageId, inout [[MessageId]]) -> Void = { id, ranges in - if ranges.isEmpty { - ranges = [[id]] - } else { - ranges[ranges.count - 1].append(id) - } - } - - let addRangeBreak: (inout [[MessageId]]) -> Void = { ranges in - if ranges.last?.count != 0 { - ranges.append([]) - } - } - - for entry in view.entries { - if historyState.matchesPeerId(entry.message.id.peerId) && entry.message.id.namespace == Namespaces.Message.Cloud { - if !historyState.isMessageValid(entry.message) { - addToRange(entry.message.id, &rangesToInvalidate) - } else { - addRangeBreak(&rangesToInvalidate) - } - } - } - - if !rangesToInvalidate.isEmpty && rangesToInvalidate[rangesToInvalidate.count - 1].isEmpty { - rangesToInvalidate.removeLast() - } - - var invalidatedMessageIds = Set() - - if !rangesToInvalidate.isEmpty { - let context: HistoryStateValidationContext - if let current = self.contexts[id] { - context = current - } else { - context = HistoryStateValidationContext() - self.contexts[id] = context - } - - var addedRanges: [[MessageId]] = [] - for messages in rangesToInvalidate { - for id in messages { - invalidatedMessageIds.insert(id) - - if context.batchReferences[id] != nil { - addRangeBreak(&addedRanges) - } else { - addToRange(id, &addedRanges) - } - } - addRangeBreak(&addedRanges) - } - - if !addedRanges.isEmpty && addedRanges[addedRanges.count - 1].isEmpty { - addedRanges.removeLast() - } - - for rangeMessages in addedRanges { - for messages in slicedForValidationMessages(rangeMessages) { - let disposable = MetaDisposable() - let batch = HistoryStateValidationBatch(disposable: disposable, invalidatedState: historyState) - for messageId in messages { - context.batchReferences[messageId] = batch - } - - disposable.set((validateBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, tag: view.tagMask, messageIds: messages, historyState: historyState) - |> deliverOn(self.queue)).start(completed: { [weak self, weak batch] in - if let strongSelf = self, let context = strongSelf.contexts[id], let batch = batch { - var completedMessageIds: [MessageId] = [] - for (messageId, messageBatch) in context.batchReferences { - if messageBatch === batch { - completedMessageIds.append(messageId) - } - } - for messageId in completedMessageIds { - context.batchReferences.removeValue(forKey: messageId) - } - } - })) - } - } - } - - if let context = self.contexts[id] { - var removeIds: [MessageId] = [] - - for batchMessageId in context.batchReferences.keys { - if !invalidatedMessageIds.contains(batchMessageId) { - removeIds.append(batchMessageId) - } - } - - for messageId in removeIds { - context.batchReferences.removeValue(forKey: messageId) - } - } - } - } -} - -private func hashForMessages(_ messages: [Message], withChannelIds: Bool) -> Int32 { - var acc: UInt32 = 0 - - let sorted = messages.sorted(by: { $0.index > $1.index }) - - for message in sorted { - if withChannelIds { - acc = (acc &* 20261) &+ UInt32(message.id.peerId.id) - } - - acc = (acc &* 20261) &+ UInt32(message.id.id) - var timestamp = message.timestamp - inner: for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - timestamp = attribute.date - break inner - } - } - acc = (acc &* 20261) &+ UInt32(timestamp) - } - return Int32(bitPattern: acc & UInt32(0x7FFFFFFF)) -} - -private func hashForMessages(_ messages: [StoreMessage], withChannelIds: Bool) -> Int32 { - var acc: UInt32 = 0 - - for message in messages { - if case let .Id(id) = message.id { - if withChannelIds { - acc = (acc &* 20261) &+ UInt32(id.peerId.id) - } - acc = (acc &* 20261) &+ UInt32(id.id) - var timestamp = message.timestamp - inner: for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - timestamp = attribute.date - break inner - } - } - acc = (acc &* 20261) &+ UInt32(timestamp) - } - } - return Int32(bitPattern: acc & UInt32(0x7FFFFFFF)) -} - -private enum ValidatedMessages { - case notModified - case messages([Api.Message], [Api.Chat], [Api.User], Int32?) -} - -private func validateBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messageIds: [MessageId], historyState: HistoryState) -> Signal { - return postbox.transaction { transaction -> Signal in - var previousMessages: [Message] = [] - var previous: [MessageId: Message] = [:] - for messageId in messageIds { - if let message = transaction.getMessage(messageId) { - previousMessages.append(message) - previous[message.id] = message - } - } - - var signal: Signal - switch historyState { - case let .channel(peerId, _): - let hash = hashForMessages(previousMessages, withChannelIds: false) - Logger.shared.log("HistoryValidation", "validate batch for \(peerId): \(previousMessages.map({ $0.id }))") - if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { - let requestSignal: Signal - if let tag = tag { - if tag == MessageTags.unseenPersonalMessage { - requestSignal = network.request(Api.functions.messages.getUnreadMentions(peer: inputPeer, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1)) - } else { - assertionFailure() - requestSignal = .complete() - } - } else { - requestSignal = network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: messageIds[messageIds.count - 1].id + 1, offsetDate: 0, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) - } - - signal = requestSignal - |> map { result -> ValidatedMessages in - let messages: [Api.Message] - let chats: [Api.Chat] - let users: [Api.User] - var channelPts: Int32? - - switch result { - case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers): - messages = apiMessages - chats = apiChats - users = apiUsers - case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): - messages = apiMessages - chats = apiChats - users = apiUsers - case let .channelMessages(_, pts, _, apiMessages, apiChats, apiUsers): - messages = apiMessages - chats = apiChats - users = apiUsers - channelPts = pts - case .messagesNotModified: - return .notModified - } - return .messages(messages, chats, users, channelPts) - } - } else { - return .complete() - } - } - - return signal - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - guard let result = result else { - return .complete() - } - switch result { - case let .messages(messages, chats, users, channelPts): - var storeMessages: [StoreMessage] = [] - - for message in messages { - if let storeMessage = StoreMessage(apiMessage: message) { - var attributes = storeMessage.attributes - - if let channelPts = channelPts { - attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) - } - - storeMessages.append(storeMessage.withUpdatedAttributes(attributes)) - } - } - - var validMessageIds = Set() - for message in storeMessages { - if case let .Id(id) = message.id { - validMessageIds.insert(id) - } - } - - var maybeRemovedMessageIds: [MessageId] = [] - for id in previous.keys { - if !validMessageIds.contains(id) { - maybeRemovedMessageIds.append(id) - } - } - - let actuallyRemovedMessagesSignal: Signal, NoError> - if maybeRemovedMessageIds.isEmpty { - actuallyRemovedMessagesSignal = .single(Set()) - } else { - actuallyRemovedMessagesSignal = postbox.transaction { transaction -> Signal, NoError> in - switch historyState { - case let .channel(peerId, _): - if let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel) { - return network.request(Api.functions.channels.getMessages(channel: inputChannel, id: maybeRemovedMessageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) }))) - |> map { result -> Set in - let apiMessages: [Api.Message] - switch result { - case let .channelMessages(_, _, _, messages, _, _): - apiMessages = messages - case let .messages(messages, _, _): - apiMessages = messages - case let .messagesSlice(_, _, _, messages, _, _): - apiMessages = messages - case .messagesNotModified: - return Set() - } - var ids = Set() - for message in apiMessages { - if let parsedMessage = StoreMessage(apiMessage: message), case let .Id(id) = parsedMessage.id { - if let tag = tag { - if parsedMessage.tags.contains(tag) { - ids.insert(id) - } - } else { - ids.insert(id) - } - } - } - return Set(maybeRemovedMessageIds).subtracting(ids) - } - |> `catch` { _ -> Signal, NoError> in - return .single(Set(maybeRemovedMessageIds)) - } - } - } - return .single(Set(maybeRemovedMessageIds)) - } - |> switchToLatest - } - - return actuallyRemovedMessagesSignal - |> mapToSignal { removedMessageIds -> Signal in - return postbox.transaction { transaction -> Void in - var validMessageIds = Set() - for message in storeMessages { - if case let .Id(id) = message.id { - validMessageIds.insert(id) - let previousMessage = previous[id] ?? transaction.getMessage(id) - - if let previousMessage = previousMessage { - var updatedTimestamp = message.timestamp - inner: for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - updatedTimestamp = attribute.date - break inner - } - } - - var timestamp = previousMessage.timestamp - inner: for attribute in previousMessage.attributes { - if let attribute = attribute as? EditedMessageAttribute { - timestamp = attribute.date - break inner - } - } - - transaction.updateMessage(id, update: { currentMessage in - if updatedTimestamp != timestamp { - var updatedLocalTags = message.localTags - if currentMessage.localTags.contains(.OutgoingLiveLocation) { - updatedLocalTags.insert(.OutgoingLiveLocation) - } - return .update(message.withUpdatedLocalTags(updatedLocalTags)) - } else { - var storeForwardInfo: StoreMessageForwardInfo? - if let forwardInfo = currentMessage.forwardInfo { - storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) - } - var attributes = currentMessage.attributes - if let channelPts = channelPts { - for i in (0 ..< attributes.count).reversed() { - if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { - attributes.remove(at: i) - } - } - attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) - } - - let updatedFlags = StoreMessageFlags(currentMessage.flags) - - return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: updatedFlags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) - } - }) - - if previous[id] == nil { - print("\(id) missing") - } - } else { - let _ = transaction.addMessages([message], location: .Random) - } - } - } - - if let tag = tag { - for (_, previousMessage) in previous { - if !validMessageIds.contains(previousMessage.id) { - transaction.updateMessage(previousMessage.id, update: { currentMessage in - var updatedTags = currentMessage.tags - updatedTags.remove(tag) - var storeForwardInfo: StoreMessageForwardInfo? - if let forwardInfo = currentMessage.forwardInfo { - storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) - } - var attributes = currentMessage.attributes - for i in (0 ..< attributes.count).reversed() { - switch historyState { - case .channel: - if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { - attributes.remove(at: i) - } - } - } - switch historyState { - case let .channel(_, channelState): - attributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) - } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) - }) - } - } - } - - for id in removedMessageIds { - if !validMessageIds.contains(id) { - if let tag = tag { - transaction.updateMessage(id, update: { currentMessage in - var updatedTags = currentMessage.tags - updatedTags.remove(tag) - var storeForwardInfo: StoreMessageForwardInfo? - if let forwardInfo = currentMessage.forwardInfo { - storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) - } - var attributes = currentMessage.attributes - for i in (0 ..< attributes.count).reversed() { - switch historyState { - case .channel: - if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { - attributes.remove(at: i) - } - } - } - switch historyState { - case let .channel(_, channelState): - attributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) - } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) - }) - } else { - switch historyState { - case .channel: - deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) - Logger.shared.log("HistoryValidation", "deleting message \(id) in \(id.peerId)") - } - } - } - } - } - } - case .notModified: - return postbox.transaction { transaction -> Void in - for id in previous.keys { - transaction.updateMessage(id, update: { currentMessage in - var storeForwardInfo: StoreMessageForwardInfo? - if let forwardInfo = currentMessage.forwardInfo { - storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) - } - var attributes = currentMessage.attributes - for i in (0 ..< attributes.count).reversed() { - switch historyState { - case .channel: - if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { - attributes.remove(at: i) - } - } - } - switch historyState { - case let .channel(_, channelState): - attributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) - } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) - }) - } - } - } - } - } |> switchToLatest -} diff --git a/submodules/TelegramCore/TelegramCore/HistoryViewStateValidation.swift b/submodules/TelegramCore/TelegramCore/HistoryViewStateValidation.swift new file mode 100644 index 0000000000..cae44ac762 --- /dev/null +++ b/submodules/TelegramCore/TelegramCore/HistoryViewStateValidation.swift @@ -0,0 +1,704 @@ +import Foundation +#if os(macOS) + import PostboxMac + import SwiftSignalKitMac + import MtProtoKitMac + import TelegramApiMac +#else + import Postbox + import SwiftSignalKit + import TelegramApi + #if BUCK + import MtProtoKit + #else + import MtProtoKitDynamic + #endif +#endif + +private final class HistoryStateValidationBatch { + private let disposable: Disposable + let invalidatedState: HistoryState? + + var cancelledMessageIds = Set() + + init(disposable: Disposable, invalidatedState: HistoryState? = nil) { + self.disposable = disposable + self.invalidatedState = invalidatedState + } + + deinit { + self.disposable.dispose() + } +} + +private final class HistoryStateValidationContext { + var batchReferences: [MessageId: HistoryStateValidationBatch] = [:] + var batch: HistoryStateValidationBatch? +} + +private enum HistoryState { + case channel(PeerId, ChannelState) + //case group(PeerGroupId, TelegramPeerGroupState) + case scheduledMessages(PeerId) + + var hasInvalidationIndex: Bool { + switch self { + case let .channel(_, state): + return state.invalidatedPts != nil + /*case let .group(_, state): + return state.invalidatedStateIndex != nil*/ + case .scheduledMessages: + return false + } + } + + func isMessageValid(_ message: Message) -> Bool { + switch self { + case let .channel(_, state): + if let invalidatedPts = state.invalidatedPts { + var messagePts: Int32? + inner: for attribute in message.attributes { + if let attribute = attribute as? ChannelMessageStateVersionAttribute { + messagePts = attribute.pts + break inner + } + } + var requiresValidation = false + if let messagePts = messagePts { + if messagePts < invalidatedPts { + requiresValidation = true + } + } else { + requiresValidation = true + } + + return !requiresValidation + } else { + return true + } + /*case let .group(_, state): + if let invalidatedStateIndex = state.invalidatedStateIndex { + var messageStateIndex: Int32? + inner: for attribute in message.attributes { + if let attribute = attribute as? PeerGroupMessageStateVersionAttribute { + messageStateIndex = attribute.stateIndex + break inner + } + } + var requiresValidation = false + if let messageStateIndex = messageStateIndex { + if messageStateIndex < invalidatedStateIndex { + requiresValidation = true + } + } else { + requiresValidation = true + } + return !requiresValidation + } else { + return true + }*/ + case .scheduledMessages: + return false + } + } + + func matchesPeerId(_ peerId: PeerId) -> Bool { + switch self { + case let .channel(statePeerId, _): + return statePeerId == peerId + /*case .group: + return true*/ + case let .scheduledMessages(statePeerId): + return statePeerId == peerId + } + } +} + +private func slicedForValidationMessages(_ messages: [MessageId]) -> [[MessageId]] { + let block = 64 + + if messages.count <= block { + return [messages] + } else { + var result: [[MessageId]] = [] + var offset = 0 + while offset < messages.count { + result.append(Array(messages[offset ..< min(offset + block, messages.count)])) + offset += block + } + return result + } +} + +final class HistoryViewStateValidationContexts { + private let queue: Queue + private let postbox: Postbox + private let network: Network + private let accountPeerId: PeerId + + private var contexts: [Int32: HistoryStateValidationContext] = [:] + + init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId) { + self.queue = queue + self.postbox = postbox + self.network = network + self.accountPeerId = accountPeerId + } + + func updateView(id: Int32, view: MessageHistoryView?) { + assert(self.queue.isCurrent()) + guard let view = view, view.tagMask == nil || view.tagMask == MessageTags.unseenPersonalMessage else { + if self.contexts[id] != nil { + self.contexts.removeValue(forKey: id) + } + return + } + var historyState: HistoryState? + for entry in view.additionalData { + if case let .peerChatState(peerId, chatState) = entry { + if let chatState = chatState as? ChannelState { + historyState = .channel(peerId, chatState) + } + break + } + } + + if let historyState = historyState, historyState.hasInvalidationIndex { + var rangesToInvalidate: [[MessageId]] = [] + let addToRange: (MessageId, inout [[MessageId]]) -> Void = { id, ranges in + if ranges.isEmpty { + ranges = [[id]] + } else { + ranges[ranges.count - 1].append(id) + } + } + + let addRangeBreak: (inout [[MessageId]]) -> Void = { ranges in + if ranges.last?.count != 0 { + ranges.append([]) + } + } + + for entry in view.entries { + if historyState.matchesPeerId(entry.message.id.peerId) && entry.message.id.namespace == Namespaces.Message.Cloud { + if !historyState.isMessageValid(entry.message) { + addToRange(entry.message.id, &rangesToInvalidate) + } else { + addRangeBreak(&rangesToInvalidate) + } + } + } + + if !rangesToInvalidate.isEmpty && rangesToInvalidate[rangesToInvalidate.count - 1].isEmpty { + rangesToInvalidate.removeLast() + } + + var invalidatedMessageIds = Set() + + if !rangesToInvalidate.isEmpty { + let context: HistoryStateValidationContext + if let current = self.contexts[id] { + context = current + } else { + context = HistoryStateValidationContext() + self.contexts[id] = context + } + + var addedRanges: [[MessageId]] = [] + for messages in rangesToInvalidate { + for id in messages { + invalidatedMessageIds.insert(id) + + if context.batchReferences[id] != nil { + addRangeBreak(&addedRanges) + } else { + addToRange(id, &addedRanges) + } + } + addRangeBreak(&addedRanges) + } + + if !addedRanges.isEmpty && addedRanges[addedRanges.count - 1].isEmpty { + addedRanges.removeLast() + } + + for rangeMessages in addedRanges { + for messages in slicedForValidationMessages(rangeMessages) { + let disposable = MetaDisposable() + let batch = HistoryStateValidationBatch(disposable: disposable, invalidatedState: historyState) + for messageId in messages { + context.batchReferences[messageId] = batch + } + + disposable.set((validateChannelMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, tag: view.tagMask, messageIds: messages, historyState: historyState) + |> deliverOn(self.queue)).start(completed: { [weak self, weak batch] in + if let strongSelf = self, let context = strongSelf.contexts[id], let batch = batch { + var completedMessageIds: [MessageId] = [] + for (messageId, messageBatch) in context.batchReferences { + if messageBatch === batch { + completedMessageIds.append(messageId) + } + } + for messageId in completedMessageIds { + context.batchReferences.removeValue(forKey: messageId) + } + } + })) + } + } + } + + if let context = self.contexts[id] { + var removeIds: [MessageId] = [] + + for batchMessageId in context.batchReferences.keys { + if !invalidatedMessageIds.contains(batchMessageId) { + removeIds.append(batchMessageId) + } + } + + for messageId in removeIds { + context.batchReferences.removeValue(forKey: messageId) + } + } + } else if view.namespaces.contains(Namespaces.Message.ScheduledCloud) { + let context: HistoryStateValidationContext + if let current = self.contexts[id] { + context = current + } else { + context = HistoryStateValidationContext() + self.contexts[id] = context + } + + let disposable = MetaDisposable() + let batch = HistoryStateValidationBatch(disposable: disposable) + context.batch = batch + + disposable.set((validateScheduledMessagesBatch(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, tag: nil, messages: [], historyState: .scheduledMessages(self.accountPeerId)) + |> deliverOn(self.queue)).start(completed: { [weak self, weak batch] in + if let strongSelf = self, let context = strongSelf.contexts[id], let batch = batch { + var completedMessageIds: [MessageId] = [] + for (messageId, messageBatch) in context.batchReferences { + if messageBatch === batch { + completedMessageIds.append(messageId) + } + } + for messageId in completedMessageIds { + context.batchReferences.removeValue(forKey: messageId) + } + } + })) + } + } +} + +private func hashForMessages(_ messages: [Message], withChannelIds: Bool) -> Int32 { + var acc: UInt32 = 0 + + let sorted = messages.sorted(by: { $0.index > $1.index }) + + for message in sorted { + if withChannelIds { + acc = (acc &* 20261) &+ UInt32(message.id.peerId.id) + } + + acc = (acc &* 20261) &+ UInt32(message.id.id) + var timestamp = message.timestamp + inner: for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + timestamp = attribute.date + break inner + } + } + acc = (acc &* 20261) &+ UInt32(timestamp) + } + return Int32(bitPattern: acc & UInt32(0x7FFFFFFF)) +} + +private func hashForMessages(_ messages: [StoreMessage], withChannelIds: Bool) -> Int32 { + var acc: UInt32 = 0 + + for message in messages { + if case let .Id(id) = message.id { + if withChannelIds { + acc = (acc &* 20261) &+ UInt32(id.peerId.id) + } + acc = (acc &* 20261) &+ UInt32(id.id) + var timestamp = message.timestamp + inner: for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + timestamp = attribute.date + break inner + } + } + acc = (acc &* 20261) &+ UInt32(timestamp) + } + } + return Int32(bitPattern: acc & UInt32(0x7FFFFFFF)) +} + +private enum ValidatedMessages { + case notModified + case messages([Api.Message], [Api.Chat], [Api.User], Int32?) +} + +private func validateChannelMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messageIds: [MessageId], historyState: HistoryState) -> Signal { + return postbox.transaction { transaction -> Signal in + var previousMessages: [Message] = [] + var previous: [MessageId: Message] = [:] + for messageId in messageIds { + if let message = transaction.getMessage(messageId) { + previousMessages.append(message) + previous[message.id] = message + } + } + + var signal: Signal + switch historyState { + case let .channel(peerId, _): + let hash = hashForMessages(previousMessages, withChannelIds: false) + Logger.shared.log("HistoryValidation", "validate batch for \(peerId): \(previousMessages.map({ $0.id }))") + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + let requestSignal: Signal + if let tag = tag { + if tag == MessageTags.unseenPersonalMessage { + requestSignal = network.request(Api.functions.messages.getUnreadMentions(peer: inputPeer, offsetId: messageIds[messageIds.count - 1].id + 1, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1)) + } else { + assertionFailure() + requestSignal = .complete() + } + } else { + requestSignal = network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: messageIds[messageIds.count - 1].id + 1, offsetDate: 0, addOffset: 0, limit: Int32(messageIds.count), maxId: messageIds[messageIds.count - 1].id + 1, minId: messageIds[0].id - 1, hash: hash)) + } + + signal = requestSignal + |> map { result -> ValidatedMessages in + let messages: [Api.Message] + let chats: [Api.Chat] + let users: [Api.User] + var channelPts: Int32? + + switch result { + case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .channelMessages(_, pts, _, apiMessages, apiChats, apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + channelPts = pts + case .messagesNotModified: + return .notModified + } + return .messages(messages, chats, users, channelPts) + } + } else { + return .complete() + } + default: + signal = .complete() + } + + return validateBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, tag: tag, historyState: historyState, signal: signal, previous: previous) + } |> switchToLatest +} + +private func validateScheduledMessagesBatch(postbox: Postbox, network: Network, accountPeerId: PeerId, tag: MessageTags?, messages: [Message], historyState: HistoryState) -> Signal { + return postbox.transaction { transaction -> Signal in + var signal: Signal + switch historyState { + case let .scheduledMessages(peerId): + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + signal = network.request(Api.functions.messages.getScheduledHistory(peer: inputPeer, hash: 0)) + |> map { result -> ValidatedMessages in + let messages: [Api.Message] + let chats: [Api.Chat] + let users: [Api.User] + + switch result { + case let .messages(messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .messagesSlice(_, _, _, messages: apiMessages, chats: apiChats, users: apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case let .channelMessages(_, pts, _, apiMessages, apiChats, apiUsers): + messages = apiMessages + chats = apiChats + users = apiUsers + case .messagesNotModified: + return .notModified + } + return .messages(messages, chats, users, nil) + } + } else { + signal = .complete() + } + default: + signal = .complete() + } + return validateBatch(postbox: postbox, network: network, transaction: transaction, accountPeerId: accountPeerId, tag: tag, historyState: historyState, signal: signal, previous: [:]) + } |> switchToLatest +} + +private func validateBatch(postbox: Postbox, network: Network, transaction: Transaction, accountPeerId: PeerId, tag: MessageTags?, historyState: HistoryState, signal: Signal, previous: [MessageId: Message]) -> Signal { + return signal + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .complete() + } + switch result { + case let .messages(messages, _, users, channelPts): + var storeMessages: [StoreMessage] = [] + + for message in messages { + if let storeMessage = StoreMessage(apiMessage: message) { + var attributes = storeMessage.attributes + if let channelPts = channelPts { + attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) + } + storeMessages.append(storeMessage.withUpdatedAttributes(attributes)) + } + } + + var validMessageIds = Set() + for message in storeMessages { + if case let .Id(id) = message.id { + validMessageIds.insert(id) + } + } + + var maybeRemovedMessageIds: [MessageId] = [] + for id in previous.keys { + if !validMessageIds.contains(id) { + maybeRemovedMessageIds.append(id) + } + } + + let actuallyRemovedMessagesSignal: Signal, NoError> + if maybeRemovedMessageIds.isEmpty { + actuallyRemovedMessagesSignal = .single(Set()) + } else { + switch historyState { + case let .channel(peerId, _): + actuallyRemovedMessagesSignal = postbox.transaction { transaction -> Signal, NoError> in + if let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel) { + return network.request(Api.functions.channels.getMessages(channel: inputChannel, id: maybeRemovedMessageIds.map({ Api.InputMessage.inputMessageID(id: $0.id) }))) + |> map { result -> Set in + let apiMessages: [Api.Message] + switch result { + case let .channelMessages(_, _, _, messages, _, _): + apiMessages = messages + case let .messages(messages, _, _): + apiMessages = messages + case let .messagesSlice(_, _, _, messages, _, _): + apiMessages = messages + case .messagesNotModified: + return Set() + } + var ids = Set() + for message in apiMessages { + if let parsedMessage = StoreMessage(apiMessage: message), case let .Id(id) = parsedMessage.id { + if let tag = tag { + if parsedMessage.tags.contains(tag) { + ids.insert(id) + } + } else { + ids.insert(id) + } + } + } + return Set(maybeRemovedMessageIds).subtracting(ids) + } + |> `catch` { _ -> Signal, NoError> in + return .single(Set(maybeRemovedMessageIds)) + } + } + return .single(Set(maybeRemovedMessageIds)) + } |> switchToLatest + default: + actuallyRemovedMessagesSignal = .single(Set(maybeRemovedMessageIds)) + } + } + + return actuallyRemovedMessagesSignal + |> mapToSignal { removedMessageIds -> Signal in + return postbox.transaction { transaction -> Void in + var validMessageIds = Set() + for message in storeMessages { + if case let .Id(id) = message.id { + validMessageIds.insert(id) + let previousMessage = previous[id] ?? transaction.getMessage(id) + + if let previousMessage = previousMessage { + var updatedTimestamp = message.timestamp + inner: for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + updatedTimestamp = attribute.date + break inner + } + } + + var timestamp = previousMessage.timestamp + inner: for attribute in previousMessage.attributes { + if let attribute = attribute as? EditedMessageAttribute { + timestamp = attribute.date + break inner + } + } + + transaction.updateMessage(id, update: { currentMessage in + if updatedTimestamp != timestamp { + var updatedLocalTags = message.localTags + if currentMessage.localTags.contains(.OutgoingLiveLocation) { + updatedLocalTags.insert(.OutgoingLiveLocation) + } + return .update(message.withUpdatedLocalTags(updatedLocalTags)) + } else { + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) + } + var attributes = currentMessage.attributes + if let channelPts = channelPts { + for i in (0 ..< attributes.count).reversed() { + if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { + attributes.remove(at: i) + } + } + attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) + } + + let updatedFlags = StoreMessageFlags(currentMessage.flags) + + return .update(StoreMessage(id: message.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: updatedFlags, tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + } + }) + + if previous[id] == nil { + print("\(id) missing") + } + } else { + let _ = transaction.addMessages([message], location: .Random) + } + } + } + + if let tag = tag { + for (_, previousMessage) in previous { + if !validMessageIds.contains(previousMessage.id) { + transaction.updateMessage(previousMessage.id, update: { currentMessage in + var updatedTags = currentMessage.tags + updatedTags.remove(tag) + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) + } + var attributes = currentMessage.attributes + for i in (0 ..< attributes.count).reversed() { + switch historyState { + case .channel: + if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { + attributes.remove(at: i) + } + default: + break + } + } + switch historyState { + case let .channel(_, channelState): + attributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) + default: + break + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + } + } + + for id in removedMessageIds { + if !validMessageIds.contains(id) { + if let tag = tag { + transaction.updateMessage(id, update: { currentMessage in + var updatedTags = currentMessage.tags + updatedTags.remove(tag) + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) + } + var attributes = currentMessage.attributes + for i in (0 ..< attributes.count).reversed() { + switch historyState { + case .channel: + if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { + attributes.remove(at: i) + } + default: + break + } + } + switch historyState { + case let .channel(_, channelState): + attributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) + default: + break + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } else { + switch historyState { + case .channel: + deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [id]) + Logger.shared.log("HistoryValidation", "deleting message \(id) in \(id.peerId)") + default: + break + } + } + } + } + } + } + case .notModified: + return postbox.transaction { transaction -> Void in + for id in previous.keys { + transaction.updateMessage(id, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature) + } + var attributes = currentMessage.attributes + for i in (0 ..< attributes.count).reversed() { + switch historyState { + case .channel: + if let _ = attributes[i] as? ChannelMessageStateVersionAttribute { + attributes.remove(at: i) + } + default: + break + } + } + switch historyState { + case let .channel(_, channelState): + attributes.append(ChannelMessageStateVersionAttribute(pts: channelState.pts)) + default: + break + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + } + } + } +} diff --git a/submodules/TelegramCore/TelegramCore/Holes.swift b/submodules/TelegramCore/TelegramCore/Holes.swift index cc5eb728f7..859f709377 100644 --- a/submodules/TelegramCore/TelegramCore/Holes.swift +++ b/submodules/TelegramCore/TelegramCore/Holes.swift @@ -151,16 +151,57 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH var implicitelyFillHole = false let minMaxRange: ClosedRange - if namespace == Namespaces.Message.ScheduledCloud { - switch direction { - case .aroundId, .range: - implicitelyFillHole = true - } - minMaxRange = 1 ... (Int32.max - 1) - request = source.request(Api.functions.messages.getScheduledHistory(peer: inputPeer, hash: 0)) - } else { - switch space { - case .everywhere: + switch space { + case .everywhere: + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = limit + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 + + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId + } else { + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() + } + } + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 + minMaxRange = 1 ... Int32.max - 1 + } + + request = source.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) + case let .tag(tag): + assert(tag.containsSingleElement) + if tag == .unseenPersonalMessage { let offsetId: Int32 let addOffset: Int32 let selectedLimit = limit @@ -203,122 +244,72 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH addOffset = Int32(-selectedLimit / 2) maxId = Int32.max minId = 1 + minMaxRange = 1 ... Int32.max - 1 } - request = source.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: offsetId, offsetDate: 0, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) - case let .tag(tag): - assert(tag.containsSingleElement) - if tag == .unseenPersonalMessage { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = limit - let maxId: Int32 - let minId: Int32 - - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } + request = source.request(Api.functions.messages.getUnreadMentions(peer: inputPeer, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) + } else if tag == .liveLocation { + let selectedLimit = limit + + switch direction { + case .aroundId, .range: + implicitelyFillHole = true + } + minMaxRange = 1 ... (Int32.max - 1) + request = source.request(Api.functions.messages.getRecentLocations(peer: inputPeer, limit: Int32(selectedLimit), hash: 0)) + } else if let filter = messageFilterForTagMask(tag) { + let offsetId: Int32 + let addOffset: Int32 + let selectedLimit = limit + let maxId: Int32 + let minId: Int32 + + switch direction { + case let .range(start, end): + if start.id <= end.id { + offsetId = start.id <= 1 ? 1 : (start.id - 1) + addOffset = Int32(-selectedLimit) + maxId = end.id + minId = start.id - 1 + + let rangeStartId = start.id + let rangeEndId = min(end.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 - - minMaxRange = 1 ... Int32.max - 1 - } - - request = source.request(Api.functions.messages.getUnreadMentions(peer: inputPeer, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId)) - } else if tag == .liveLocation { - let selectedLimit = limit - - switch direction { - case .aroundId, .range: - implicitelyFillHole = true - } - minMaxRange = 1 ... (Int32.max - 1) - request = source.request(Api.functions.messages.getRecentLocations(peer: inputPeer, limit: Int32(selectedLimit), hash: 0)) - } else if let filter = messageFilterForTagMask(tag) { - let offsetId: Int32 - let addOffset: Int32 - let selectedLimit = limit - let maxId: Int32 - let minId: Int32 - - switch direction { - case let .range(start, end): - if start.id <= end.id { - offsetId = start.id <= 1 ? 1 : (start.id - 1) - addOffset = Int32(-selectedLimit) - maxId = end.id - minId = start.id - 1 - - let rangeStartId = start.id - let rangeEndId = min(end.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } + } else { + offsetId = start.id == Int32.max ? start.id : (start.id + 1) + addOffset = 0 + maxId = start.id == Int32.max ? start.id : (start.id + 1) + minId = end.id + + let rangeStartId = end.id + let rangeEndId = min(start.id, Int32.max - 1) + if rangeStartId <= rangeEndId { + minMaxRange = rangeStartId ... rangeEndId } else { - offsetId = start.id == Int32.max ? start.id : (start.id + 1) - addOffset = 0 - maxId = start.id == Int32.max ? start.id : (start.id + 1) - minId = end.id - - let rangeStartId = end.id - let rangeEndId = min(start.id, Int32.max - 1) - if rangeStartId <= rangeEndId { - minMaxRange = rangeStartId ... rangeEndId - } else { - minMaxRange = rangeStartId ... rangeStartId - assertionFailure() - } + minMaxRange = rangeStartId ... rangeStartId + assertionFailure() } - case let .aroundId(id): - offsetId = id.id - addOffset = Int32(-selectedLimit / 2) - maxId = Int32.max - minId = 1 - - minMaxRange = 1 ... (Int32.max - 1) - } + } + case let .aroundId(id): + offsetId = id.id + addOffset = Int32(-selectedLimit / 2) + maxId = Int32.max + minId = 1 - request = source.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, filter: filter, minDate: 0, maxDate: 0, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) - } else { - assertionFailure() - minMaxRange = 1 ... 1 - request = .never() - } + minMaxRange = 1 ... (Int32.max - 1) + } + + request = source.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, filter: filter, minDate: 0, maxDate: 0, offsetId: offsetId, addOffset: addOffset, limit: Int32(selectedLimit), maxId: maxId, minId: minId, hash: 0)) + } else { + assertionFailure() + minMaxRange = 1 ... 1 + request = .never() } } diff --git a/submodules/TelegramCore/TelegramCore/ManagedSynchronizeEmojiKeywordsOperations.swift b/submodules/TelegramCore/TelegramCore/ManagedSynchronizeEmojiKeywordsOperations.swift index 4773e2d9c7..c9ca8718cb 100644 --- a/submodules/TelegramCore/TelegramCore/ManagedSynchronizeEmojiKeywordsOperations.swift +++ b/submodules/TelegramCore/TelegramCore/ManagedSynchronizeEmojiKeywordsOperations.swift @@ -63,7 +63,7 @@ private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: return postbox.transaction { transaction -> Signal in var result: PeerMergedOperationLogEntry? transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizeEmojiKeywords, tagLocalIndex: tagLocalIndex, { entry in - if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeEmojiKeywordsOperation { + if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeEmojiKeywordsOperation { result = entry.mergedEntry! return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) } else { diff --git a/submodules/TelegramCore/TelegramCore/Namespaces.swift b/submodules/TelegramCore/TelegramCore/Namespaces.swift index 352a42aef6..1b6fc51ff3 100644 --- a/submodules/TelegramCore/TelegramCore/Namespaces.swift +++ b/submodules/TelegramCore/TelegramCore/Namespaces.swift @@ -12,6 +12,8 @@ public struct Namespaces { public static let SecretIncoming: Int32 = 2 public static let ScheduledCloud: Int32 = 3 public static let ScheduledLocal: Int32 = 4 + + public static let allScheduled: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal]) } public struct Media { @@ -110,6 +112,7 @@ public extension LocalMessageTags { public extension PendingMessageActionType { static let consumeUnseenPersonalMessage = PendingMessageActionType(rawValue: 0) static let updateReaction = PendingMessageActionType(rawValue: 1) + static let sendScheduledMessageImmediately = PendingMessageActionType(rawValue: 2) } let peerIdNamespacesWithInitialCloudMessageHoles = [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel] diff --git a/submodules/TelegramCore/TelegramCore/ScheduledMessages.swift b/submodules/TelegramCore/TelegramCore/ScheduledMessages.swift index a57f91700b..23f66776b6 100644 --- a/submodules/TelegramCore/TelegramCore/ScheduledMessages.swift +++ b/submodules/TelegramCore/TelegramCore/ScheduledMessages.swift @@ -9,21 +9,171 @@ import Foundation import TelegramApi #endif -public func sendScheduledMessageNow(account: Account, messageId: MessageId) -> Signal { - return account.postbox.transaction { transaction -> Signal in - if let _ = transaction.getMessage(messageId), let peer = transaction.getPeer(messageId.peerId), let inputPeer = apiInputPeer(peer) { - return account.network.request(Api.functions.messages.sendScheduledMessages(peer: inputPeer, id: [messageId.id])) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { updates -> Signal in - if let updates = updates { - account.stateManager.addUpdates(updates) +final class SendScheduledMessageImmediatelyAction: PendingMessageActionData { + init() { + } + + init(decoder: PostboxDecoder) { + } + + func encode(_ encoder: PostboxEncoder) { + } + + func isEqual(to: PendingMessageActionData) -> Bool { + if let _ = to as? SendScheduledMessageImmediatelyAction { + return true + } else { + return false + } + } +} + +public func sendScheduledMessageNowInteractively(postbox: Postbox, messageId: MessageId) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendScheduledMessageImmediately, id: messageId, action: SendScheduledMessageImmediatelyAction()) + } + |> ignoreValues +} + +private final class ManagedApplyPendingScheduledMessagesActionsHelper { + var operationDisposables: [MessageId: Disposable] = [:] + + func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = [] + + var hasRunningOperationForPeerId = Set() + var validIds = Set() + for entry in entries { + if !hasRunningOperationForPeerId.contains(entry.id.peerId) { + hasRunningOperationForPeerId.insert(entry.id.peerId) + validIds.insert(entry.id) + + if self.operationDisposables[entry.id] == nil { + let disposable = MetaDisposable() + beginOperations.append((entry, disposable)) + self.operationDisposables[entry.id] = disposable } - return .complete() } } - return .complete() - } |> switchToLatest + + var removeMergedIds: [MessageId] = [] + for (id, disposable) in self.operationDisposables { + if !validIds.contains(id) { + removeMergedIds.append(id) + disposeOperations.append(disposable) + } + } + + for id in removeMergedIds { + self.operationDisposables.removeValue(forKey: id) + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + let disposables = Array(self.operationDisposables.values) + self.operationDisposables.removeAll() + return disposables + } +} + +private func withTakenAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PendingMessageActionsEntry? + + if let action = transaction.getPendingMessageAction(type: type, id: id) as? SendScheduledMessageImmediatelyAction { + result = PendingMessageActionsEntry(id: id, action: action) + } + + return f(transaction, result) + } + |> switchToLatest +} + +func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedApplyPendingScheduledMessagesActionsHelper()) + + let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendScheduledMessageImmediately) + let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in + var entries: [PendingMessageActionsEntry] = [] + if let v = view.views[actionsKey] as? PendingMessageActionsView { + entries = v.entries + } + + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in + return helper.update(entries: entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenAction(postbox: postbox, type: .sendScheduledMessageImmediately, id: entry.id, { transaction, entry -> Signal in + if let entry = entry { + if let _ = entry.action as? SendScheduledMessageImmediatelyAction { + return sendScheduledMessageNow(postbox: postbox, network: network, stateManager: stateManager, messageId: entry.id) + |> `catch` { _ -> Signal in + return .complete() + } + } else { + assertionFailure() + } + } + return .complete() + }) + |> then( + postbox.transaction { transaction -> Void in + transaction.deleteMessages([entry.id]) + } + |> ignoreValues + ) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + +private enum SendScheduledMessageNowError { + case generic +} + +private func sendScheduledMessageNow(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { + return postbox.transaction { transaction -> Peer? in + guard let peer = transaction.getPeer(messageId.peerId) else { + return nil + } + return peer + } + |> introduceError(SendScheduledMessageNowError.self) + |> mapToSignal { peer -> Signal in + guard let peer = peer else { + return .fail(.generic) + } + guard let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + return network.request(Api.functions.messages.sendScheduledMessages(peer: inputPeer, id: [messageId.id])) + |> mapError { _ -> SendScheduledMessageNowError in + return .generic + } + |> mapToSignal { updates -> Signal in + stateManager.addUpdates(updates) + return .complete() + } + } } diff --git a/submodules/TelegramCore/TelegramCore/SearchStickers.swift b/submodules/TelegramCore/TelegramCore/SearchStickers.swift index 8d365f88ba..42406113dd 100644 --- a/submodules/TelegramCore/TelegramCore/SearchStickers.swift +++ b/submodules/TelegramCore/TelegramCore/SearchStickers.swift @@ -97,7 +97,7 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers) { if let item = entry.contents as? SavedStickerItem { for representation in item.stringRepresentations { - if representation == query { + if representation.hasPrefix(query) { result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations)) break } @@ -115,7 +115,7 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker if let item = entry.contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile { if !currentItems.contains(file.fileId) { for case let .Sticker(sticker) in file.attributes { - if sticker.displayText == query { + if sticker.displayText.hasPrefix(query) { matchingRecentItemsIds.insert(file.fileId) } recentItemsIds.insert(file.fileId) @@ -130,9 +130,14 @@ public func searchStickers(account: Account, query: String, scope: SearchSticker } } + var searchQuery: ItemCollectionSearchQuery = .exact(ValueBoxKey(query)) + if query == "\u{2764}" { + searchQuery = .matching([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{fe0f}")]) + } + var installedItems: [FoundStickerItem] = [] var installedAnimatedItems: [FoundStickerItem] = [] - for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: .exact(ValueBoxKey(query))) { + for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) { if let item = item as? StickerPackItem { if !currentItems.contains(item.file.fileId) { var stringRepresentations: [String] = [] diff --git a/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj b/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj index f126a85d63..20424800a3 100644 --- a/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj +++ b/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj @@ -511,8 +511,8 @@ D098908022942E3B0053F151 /* ActiveSessionsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D098907E22942E3B0053F151 /* ActiveSessionsContext.swift */; }; D099D7461EEF0C2700A3128C /* ChannelMessageStateVersionAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7451EEF0C2700A3128C /* ChannelMessageStateVersionAttribute.swift */; }; D099D7471EEF0C2700A3128C /* ChannelMessageStateVersionAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7451EEF0C2700A3128C /* ChannelMessageStateVersionAttribute.swift */; }; - D099D7491EEF418D00A3128C /* HistoryViewChannelStateValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7481EEF418D00A3128C /* HistoryViewChannelStateValidation.swift */; }; - D099D74A1EEF418D00A3128C /* HistoryViewChannelStateValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7481EEF418D00A3128C /* HistoryViewChannelStateValidation.swift */; }; + D099D7491EEF418D00A3128C /* HistoryViewStateValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7481EEF418D00A3128C /* HistoryViewStateValidation.swift */; }; + D099D74A1EEF418D00A3128C /* HistoryViewStateValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099D7481EEF418D00A3128C /* HistoryViewStateValidation.swift */; }; D099E222229420D600561B75 /* BlockedPeersContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099E221229420D600561B75 /* BlockedPeersContext.swift */; }; D099E223229420D600561B75 /* BlockedPeersContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099E221229420D600561B75 /* BlockedPeersContext.swift */; }; D099EA1C1DE72867001AF5A8 /* PeerCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA1B1DE72867001AF5A8 /* PeerCommands.swift */; }; @@ -1104,7 +1104,7 @@ D093D805206539D000BC3599 /* SaveSecureIdValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveSecureIdValue.swift; sourceTree = ""; }; D098907E22942E3B0053F151 /* ActiveSessionsContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsContext.swift; sourceTree = ""; }; D099D7451EEF0C2700A3128C /* ChannelMessageStateVersionAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMessageStateVersionAttribute.swift; sourceTree = ""; }; - D099D7481EEF418D00A3128C /* HistoryViewChannelStateValidation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryViewChannelStateValidation.swift; sourceTree = ""; }; + D099D7481EEF418D00A3128C /* HistoryViewStateValidation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryViewStateValidation.swift; sourceTree = ""; }; D099E221229420D600561B75 /* BlockedPeersContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedPeersContext.swift; sourceTree = ""; }; D099EA1B1DE72867001AF5A8 /* PeerCommands.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerCommands.swift; sourceTree = ""; }; D09A2FE51D7CD4940018FB72 /* TelegramChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramChannel.swift; sourceTree = ""; }; @@ -1667,7 +1667,7 @@ D0BEAF5C1E54941B00BD963D /* Authorization.swift */, D0BEAF5F1E54ACF900BD963D /* AccountManager.swift */, D0528E591E658B3600E2FEF5 /* ManagedLocalInputActivities.swift */, - D099D7481EEF418D00A3128C /* HistoryViewChannelStateValidation.swift */, + D099D7481EEF418D00A3128C /* HistoryViewStateValidation.swift */, D0B1671C1F9EA2C300976B40 /* ChatHistoryPreloadManager.swift */, D06ECFC720B810D300C576C2 /* TermsOfService.swift */, D051DB16215ECC4D00F30F92 /* AppChangelog.swift */, @@ -2381,7 +2381,7 @@ D08984FB2118816A00918162 /* Reachability.m in Sources */, D0DA1D321F7043D50034E892 /* ManagedPendingPeerNotificationSettings.swift in Sources */, 0925903722F0D02D003D6283 /* ManagedAnimatedEmojiUpdates.swift in Sources */, - D099D7491EEF418D00A3128C /* HistoryViewChannelStateValidation.swift in Sources */, + D099D7491EEF418D00A3128C /* HistoryViewStateValidation.swift in Sources */, C23BC3871E9BE3CA00D79F92 /* ImportContact.swift in Sources */, D0D376E622DCCFD600FA7D7C /* SlowMode.swift in Sources */, D00422D321677F4500719B67 /* ManagedAccountPresence.swift in Sources */, @@ -2808,7 +2808,7 @@ D0E35A131DE4C69100BC6096 /* OutgoingChatContextResultMessageAttribute.swift in Sources */, D0B418961D7E0580004562A4 /* TelegramMediaFile.swift in Sources */, D08CAA881ED81DD40000FDA8 /* LocalizationInfo.swift in Sources */, - D099D74A1EEF418D00A3128C /* HistoryViewChannelStateValidation.swift in Sources */, + D099D74A1EEF418D00A3128C /* HistoryViewStateValidation.swift in Sources */, D0AF32231FAC95C20097362B /* StandaloneUploadedMedia.swift in Sources */, D04554A721B43440007A6DD9 /* CancelAccountReset.swift in Sources */, D001F3EC1E128A1C007A8C60 /* Holes.swift in Sources */, diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 181b8f0744..565a98fca5 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1452,44 +1452,38 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.sendScheduledMessagesNow(messageIds) } }, editScheduledMessagesTime: { [weak self] messageIds in - if let strongSelf = self { + if let strongSelf = self, let messageId = messageIds.first { let mode: ChatScheduleTimeControllerMode if case let .peer(peerId) = strongSelf.presentationInterfaceState.chatLocation, peerId == strongSelf.context.account.peerId { mode = .reminders } else { mode = .scheduledMessages } - let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, completion: { [weak self] scheduleTime in - if let strongSelf = self, let messageId = messageIds.first { - let signal = (strongSelf.context.account.postbox.transaction { transaction -> Message? in - return transaction.getMessage(messageId) - } - |> introduceError(RequestEditMessageError.self) - |> mapToSignal({ message -> Signal in - if let message = message { - var entities: TextEntitiesMessageAttribute? - for attribute in message.attributes { - if let attribute = attribute as? TextEntitiesMessageAttribute { - entities = attribute - break - } - } - return requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep - , entities: entities, disableUrlPreview: false, scheduleTime: scheduleTime) - } else { - return .complete() - } - })) - - strongSelf.editMessageDisposable.set((signal |> deliverOnMainQueue).start(next: { result in - - }, error: { error in - - })) + + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) + } |> deliverOnMainQueue).start(next: { [weak self] message in + guard let strongSelf = self, let message = message else { + return } + let controller = ChatScheduleTimeController(context: strongSelf.context, mode: mode, currentTime: message.timestamp, completion: { [weak self] scheduleTime in + if let strongSelf = self { + var entities: TextEntitiesMessageAttribute? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + entities = attribute + break + } + } + let signal = requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep, entities: entities, disableUrlPreview: false, scheduleTime: scheduleTime) + strongSelf.editMessageDisposable.set((signal |> deliverOnMainQueue).start(next: { result in + }, error: { error in + })) + } + }) + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(controller, in: .window(.root)) }) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) } }, performTextSelectionAction: { [weak self] _, text, action in guard let strongSelf = self else { @@ -1625,7 +1619,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if !isScheduledMessages && peerId.namespace != Namespaces.Peer.SecretChat { - hasScheduledMessages = context.account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil, tagMask: nil, excludeNamespaces: [Namespaces.Message.Cloud, Namespaces.Message.Local], orderStatistics: []) + hasScheduledMessages = context.account.viewTracker.scheduledMessagesViewForLocation(chatLocation) |> map { view, _, _ in return !view.entries.isEmpty } @@ -5278,7 +5272,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func sendScheduledMessagesNow(_ messageId: [MessageId]) { - let _ = sendScheduledMessageNow(account: self.context.account, messageId: messageId.first!).start() + let _ = sendScheduledMessageNowInteractively(postbox: self.context.account.postbox, messageId: messageId.first!).start() } private func sendMessages(_ messages: [EnqueueMessage], commit: Bool = false) { diff --git a/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift index 94512dcb1d..ae12d1ee33 100644 --- a/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/TelegramUI/ChatHistoryViewForLocation.swift @@ -30,7 +30,6 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, account: A if scheduled { var preloaded = false var fadeIn = false - let count = 100 return account.viewTracker.scheduledMessagesViewForLocation(chatLocation) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation) diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift b/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift index ea364c24e0..b73a136ef1 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageItem.swift @@ -328,13 +328,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { self.effectiveAuthorId = effectiveAuthor?.id - let timestamp: Int32 - if let scheduleTime = content.firstMessage.scheduleTime { - timestamp = scheduleTime - } else { - timestamp = content.index.timestamp - } - self.header = ChatMessageDateHeader(timestamp: timestamp, scheduled: associatedData.isScheduledMessages, presentationData: presentationData, context: context, action: { timestamp in + self.header = ChatMessageDateHeader(timestamp: content.index.timestamp, scheduled: associatedData.isScheduledMessages, presentationData: presentationData, context: context, action: { timestamp in var calendar = NSCalendar.current calendar.timeZone = TimeZone(abbreviation: "UTC")! let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) diff --git a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift index d88cd85bc8..aeaa6bbd34 100644 --- a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift @@ -265,8 +265,11 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel insets.top = max(10.0, insets.top) let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleAreaHeight: CGFloat = 54.0 - let contentHeight = titleAreaHeight + bottomInset + 285.0 + let titleHeight: CGFloat = 54.0 + var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight) + contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) let sideInset = floor((layout.size.width - width) / 2.0) @@ -281,11 +284,11 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - let titleSize = self.titleNode.measure(CGSize(width: width, height: titleAreaHeight)) + let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) - let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleAreaHeight)) + let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) transition.updateFrame(node: self.cancelButton, frame: cancelFrame) @@ -293,9 +296,9 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel let buttonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - buttonHeight - insets.bottom - 10.0, width: contentFrame.width, height: buttonHeight)) - self.pickerView.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: 216.0)) + self.pickerView.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: contentHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) } } diff --git a/submodules/TelegramUI/TelegramUI/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/TelegramUI/PeerMessagesMediaPlaylist.swift index 498479162c..232b7875c7 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMessagesMediaPlaylist.swift @@ -10,6 +10,15 @@ import MusicAlbumArtResources private enum PeerMessagesMediaPlaylistLoadAnchor { case messageId(MessageId) case index(MessageIndex) + + var id: MessageId { + switch self { + case let .messageId(id): + return id + case let .index(index): + return index.id + } + } } private enum PeerMessagesMediaPlaylistNavigation { @@ -477,6 +486,14 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { private func loadItem(anchor: PeerMessagesMediaPlaylistLoadAnchor, navigation: PeerMessagesMediaPlaylistNavigation) { self.loadingItem = true self.updateState() + + let namespaces: HistoryViewNamespaces + if Namespaces.Message.allScheduled.contains(anchor.id.namespace) { + namespaces = .just(Namespaces.Message.allScheduled) + } else { + namespaces = .not(Namespaces.Message.allScheduled) + } + switch anchor { case let .messageId(messageId): if case let .messages(peerId, tagMask, _) = self.messagesLocation { @@ -486,7 +503,8 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { guard let message = message else { return .single(nil) } - return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(message.index), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, excludeNamespaces: [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal], orderStatistics: []) + + return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(message.index), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in if let (message, aroundMessages, _) = navigatedMessageFromView(view.0, anchorIndex: message.index, position: .exact) { return .single((message, aroundMessages)) @@ -560,7 +578,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } let historySignal = inputIndex |> mapToSignal { inputIndex -> Signal<(Message, [Message])?, NoError> in - return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(inputIndex), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, excludeNamespaces: [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal], orderStatistics: []) + return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: .index(inputIndex), count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in let position: NavigatedMessageFromViewPosition switch navigation { @@ -590,7 +608,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } else { viewIndex = .lowerBound } - return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: viewIndex, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, excludeNamespaces: [Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal], orderStatistics: []) + return self.postbox.aroundMessageHistoryViewForLocation(.peer(peerId), anchor: viewIndex, count: 10, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: []) |> mapToSignal { view -> Signal<(Message, [Message])?, NoError> in let position: NavigatedMessageFromViewPosition switch navigation {