diff --git a/submodules/TelegramCore/TelegramCore/Account.swift b/submodules/TelegramCore/TelegramCore/Account.swift index 09546839d5..78f4263066 100644 --- a/submodules/TelegramCore/TelegramCore/Account.swift +++ b/submodules/TelegramCore/TelegramCore/Account.swift @@ -1242,6 +1242,7 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) 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()) let importantBackgroundOperations: [Signal] = [ diff --git a/submodules/TelegramCore/TelegramCore/AccountManager.swift b/submodules/TelegramCore/TelegramCore/AccountManager.swift index 27c70efda7..14ccc37334 100644 --- a/submodules/TelegramCore/TelegramCore/AccountManager.swift +++ b/submodules/TelegramCore/TelegramCore/AccountManager.swift @@ -40,6 +40,7 @@ private var declaredEncodables: Void = { declareEncodable(TextEntitiesMessageAttribute.self, f: { TextEntitiesMessageAttribute(decoder: $0) }) declareEncodable(ReplyMessageAttribute.self, f: { ReplyMessageAttribute(decoder: $0) }) declareEncodable(ReactionsMessageAttribute.self, f: { ReactionsMessageAttribute(decoder: $0) }) + declareEncodable(PendingReactionsMessageAttribute.self, f: { PendingReactionsMessageAttribute(decoder: $0) }) declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) }) declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) }) declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) }) @@ -149,6 +150,7 @@ private var declaredEncodables: Void = { declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) }) declareEncodable(AccountBackupDataAttribute.self, f: { AccountBackupDataAttribute(decoder: $0) }) declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(decoder: $0) }) + declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/TelegramCore/AccountStateManagementUtils.swift b/submodules/TelegramCore/TelegramCore/AccountStateManagementUtils.swift index dbcdc36e10..fab54d3a8e 100644 --- a/submodules/TelegramCore/TelegramCore/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/TelegramCore/AccountStateManagementUtils.swift @@ -647,8 +647,7 @@ func finalStateWithDifference(postbox: Postbox, network: Network, state: Account } private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { - var result: [Api.Update] = [] - + var otherUpdates: [Api.Update] = [] var updatesByChannel: [PeerId: [Api.Update]] = [:] for update in updates { @@ -675,7 +674,7 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { updatesByChannel[peerId]!.append(update) } } else { - result.append(update) + otherUpdates.append(update) } case let .updateEditChannelMessage(message, _, _): if let peerId = apiMessagePeerId(message) { @@ -685,7 +684,7 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { updatesByChannel[peerId]!.append(update) } } else { - result.append(update) + otherUpdates.append(update) } case let .updateChannelWebPage(channelId, _, _, _): let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId) @@ -702,10 +701,12 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { updatesByChannel[peerId]!.append(update) } default: - result.append(update) + otherUpdates.append(update) } } + var result: [Api.Update] = [] + for (_, updates) in updatesByChannel { let sortedUpdates = updates.sorted(by: { lhs, rhs in var lhsPts: Int32? @@ -747,6 +748,7 @@ private func sortedUpdates(_ updates: [Api.Update]) -> [Api.Update] { }) result.append(contentsOf: sortedUpdates) } + result.append(contentsOf: otherUpdates) return result } diff --git a/submodules/TelegramCore/TelegramCore/EditedMessageAttribute.swift b/submodules/TelegramCore/TelegramCore/EditedMessageAttribute.swift index a1b9e4f541..6f9f3a9a2e 100644 --- a/submodules/TelegramCore/TelegramCore/EditedMessageAttribute.swift +++ b/submodules/TelegramCore/TelegramCore/EditedMessageAttribute.swift @@ -7,16 +7,20 @@ import Foundation public class EditedMessageAttribute: MessageAttribute { public let date: Int32 + public let isHidden: Bool - init(date: Int32) { + init(date: Int32, isHidden: Bool) { self.date = date + self.isHidden = isHidden } required public init(decoder: PostboxDecoder) { self.date = decoder.decodeInt32ForKey("d", orElse: 0) + self.isHidden = decoder.decodeInt32ForKey("h", orElse: 0) != 0 } public func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.date, forKey: "d") + encoder.encodeInt32(self.isHidden ? 1 : 0, forKey: "h") } } diff --git a/submodules/TelegramCore/TelegramCore/MessageReactions.swift b/submodules/TelegramCore/TelegramCore/MessageReactions.swift index b229541251..b73ffb3e73 100644 --- a/submodules/TelegramCore/TelegramCore/MessageReactions.swift +++ b/submodules/TelegramCore/TelegramCore/MessageReactions.swift @@ -15,29 +15,241 @@ import MtProtoKitDynamic #endif #endif +final class UpdateMessageReactionsAction: PendingMessageActionData { + init() { + } + + init(decoder: PostboxDecoder) { + } + + func encode(_ encoder: PostboxEncoder) { + } + + func isEqual(to: PendingMessageActionData) -> Bool { + if let _ = to as? UpdateMessageReactionsAction { + return true + } else { + return false + } + } +} -public enum RequestUpdateMessageReactionError { +public func updateMessageReactionsInteractively(postbox: Postbox, messageId: MessageId, reactions: [String]) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction()) + transaction.updateMessage(messageId, 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 + loop: for j in 0 ..< attributes.count { + if let _ = attributes[j] as? PendingReactionsMessageAttribute { + attributes.remove(at: j) + break loop + } + } + attributes.append(PendingReactionsMessageAttribute(values: reactions)) + 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)) + }) + } + |> ignoreValues +} + +private enum RequestUpdateMessageReactionError { case generic } -public func requestUpdateMessageReaction(account: Account, messageId: MessageId, reactions: [String]) -> Signal { - return account.postbox.loadedPeerWithId(messageId.peerId) - |> take(1) +private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { + return postbox.transaction { transaction -> (Peer, [String])? in + guard let peer = transaction.getPeer(messageId.peerId) else { + return nil + } + guard let message = transaction.getMessage(messageId) else { + return nil + } + var values: [String] = [] + for attribute in message.attributes { + if let attribute = attribute as? PendingReactionsMessageAttribute { + values = attribute.values + break + } + } + return (peer, values) + } |> introduceError(RequestUpdateMessageReactionError.self) - |> mapToSignal { peer in + |> mapToSignal { peerAndValues in + guard let (peer, values) = peerAndValues else { + return .fail(.generic) + } guard let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } if messageId.namespace != Namespaces.Message.Cloud { return .fail(.generic) } - return account.network.request(Api.functions.messages.sendReaction(peer: inputPeer, msgId: messageId.id, reaction: reactions)) + return network.request(Api.functions.messages.sendReaction(peer: inputPeer, msgId: messageId.id, reaction: values)) |> mapError { _ -> RequestUpdateMessageReactionError in return .generic } |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) - return .complete() + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction()) + transaction.updateMessage(messageId, 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) + } + let reactions = mergedMessageReactions(attributes: currentMessage.attributes) + var attributes = currentMessage.attributes + for j in (0 ..< attributes.count).reversed() { + if attributes[j] is PendingReactionsMessageAttribute || attributes[j] is ReactionsMessageAttribute { + attributes.remove(at: j) + } + } + if let reactions = reactions { + attributes.append(reactions) + } + 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)) + }) + stateManager.addUpdates(result) + } + |> introduceError(RequestUpdateMessageReactionError.self) + |> ignoreValues } } } + +private final class ManagedApplyPendingMessageReactionsActionsHelper { + 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 + } + } + } + + 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? UpdateMessageReactionsAction { + result = PendingMessageActionsEntry(id: id, action: action) + } + + return f(transaction, result) + } + |> switchToLatest +} + +func managedApplyPendingMessageReactionsActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedApplyPendingMessageReactionsActionsHelper()) + + let actionsKey = PostboxViewKey.pendingMessageActions(type: .updateReaction) + 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: .updateReaction, id: entry.id, { transaction, entry -> Signal in + if let entry = entry { + if let _ = entry.action as? UpdateMessageReactionsAction { + return synchronizeMessageReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id) + } else { + assertionFailure() + } + } + return .complete() + }) + |> then( + postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .updateReaction, id: entry.id, action: nil) + } + |> 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 func synchronizeMessageReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal { + return requestUpdateMessageReaction(postbox: postbox, network: network, stateManager: stateManager, messageId: id) + |> `catch` { _ -> Signal in + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .updateReaction, id: id, action: nil) + 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 + loop: for j in 0 ..< attributes.count { + if let _ = attributes[j] as? PendingReactionsMessageAttribute { + attributes.remove(at: j) + break loop + } + } + 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)) + }) + } + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/TelegramCore/Namespaces.swift b/submodules/TelegramCore/TelegramCore/Namespaces.swift index 367b947145..3b3980e134 100644 --- a/submodules/TelegramCore/TelegramCore/Namespaces.swift +++ b/submodules/TelegramCore/TelegramCore/Namespaces.swift @@ -107,6 +107,7 @@ public extension LocalMessageTags { public extension PendingMessageActionType { static let consumeUnseenPersonalMessage = PendingMessageActionType(rawValue: 0) + static let updateReaction = PendingMessageActionType(rawValue: 1) } let peerIdNamespacesWithInitialCloudMessageHoles = [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel] @@ -135,9 +136,9 @@ public struct OperationLogTags { } public extension PeerSummaryCounterTags { - public static let regularChatsAndPrivateGroups = PeerSummaryCounterTags(rawValue: 1 << 0) - public static let publicGroups = PeerSummaryCounterTags(rawValue: 1 << 1) - public static let channels = PeerSummaryCounterTags(rawValue: 1 << 2) + static let regularChatsAndPrivateGroups = PeerSummaryCounterTags(rawValue: 1 << 0) + static let publicGroups = PeerSummaryCounterTags(rawValue: 1 << 1) + static let channels = PeerSummaryCounterTags(rawValue: 1 << 2) } private enum PreferencesKeyValues: Int32 { diff --git a/submodules/TelegramCore/TelegramCore/ReactionsMessageAttribute.swift b/submodules/TelegramCore/TelegramCore/ReactionsMessageAttribute.swift index 1f113c3df7..83f97059b7 100644 --- a/submodules/TelegramCore/TelegramCore/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/TelegramCore/ReactionsMessageAttribute.swift @@ -31,7 +31,7 @@ public struct MessageReaction: Equatable, PostboxCoding { } } -public class ReactionsMessageAttribute: MessageAttribute { +public final class ReactionsMessageAttribute: MessageAttribute { public let reactions: [MessageReaction] init(reactions: [MessageReaction]) { @@ -77,6 +77,72 @@ public class ReactionsMessageAttribute: MessageAttribute { } } +public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? { + var current: ReactionsMessageAttribute? + var pending: PendingReactionsMessageAttribute? + for attribute in attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + current = attribute + } else if let attribute = attribute as? PendingReactionsMessageAttribute { + pending = attribute + } + } + + if let pending = pending { + var reactions = current?.reactions ?? [] + for value in pending.values { + var found = false + for i in 0 ..< reactions.count { + if reactions[i].value == value { + found = true + if !reactions[i].isSelected { + reactions[i].isSelected = true + reactions[i].count += 1 + } + } + } + if !found { + reactions.append(MessageReaction(value: value, count: 1, isSelected: true)) + } + } + for i in (0 ..< reactions.count).reversed() { + if reactions[i].isSelected, !pending.values.contains(reactions[i].value) { + if reactions[i].count == 1 { + reactions.remove(at: i) + } else { + reactions[i].isSelected = false + reactions[i].count -= 1 + } + } + } + if !reactions.isEmpty { + return ReactionsMessageAttribute(reactions: reactions) + } else { + return nil + } + } else if let current = current { + return current + } else { + return nil + } +} + +public final class PendingReactionsMessageAttribute: MessageAttribute { + public let values: [String] + + init(values: [String]) { + self.values = values + } + + required public init(decoder: PostboxDecoder) { + self.values = decoder.decodeStringArrayForKey("v") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeStringArray(self.values, forKey: "v") + } +} + extension ReactionsMessageAttribute { convenience init(apiReactions: Api.MessageReactions) { switch apiReactions { diff --git a/submodules/TelegramCore/TelegramCore/StoreMessage_Telegram.swift b/submodules/TelegramCore/TelegramCore/StoreMessage_Telegram.swift index ccdafffa3a..b83067cd53 100644 --- a/submodules/TelegramCore/TelegramCore/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/TelegramCore/StoreMessage_Telegram.swift @@ -506,7 +506,7 @@ extension StoreMessage { } if let editDate = editDate { - attributes.append(EditedMessageAttribute(date: editDate)) + attributes.append(EditedMessageAttribute(date: editDate, isHidden: (flags & (1 << 21)) != 0)) } var entitiesAttribute: TextEntitiesMessageAttribute?