Reactions update [skip ci]

This commit is contained in:
Ali 2022-01-25 14:08:52 +04:00
parent e694aa319d
commit f47fa686e9
18 changed files with 497 additions and 152 deletions

View File

@ -7250,3 +7250,5 @@ Sorry for the inconvenience.";
"Group.Members.Contacts" = "CONTACTS IN THIS GROUP"; "Group.Members.Contacts" = "CONTACTS IN THIS GROUP";
"Group.Members.Other" = "OTHERS MEMBERS"; "Group.Members.Other" = "OTHERS MEMBERS";
"Conversation.ReadAllReactions" = "Read All Reactions";

View File

@ -1481,12 +1481,12 @@ final class MessageHistoryTable: Table {
let updatedGroupInfo = self.updateMovingGroupInfoInNamespace(index: updatedIndex, updatedIndex: updatedIndex, groupingKey: message.groupingKey, previousInfo: previousMessage.groupInfo, updatedGroupInfos: &updatedGroupInfos) let updatedGroupInfo = self.updateMovingGroupInfoInNamespace(index: updatedIndex, updatedIndex: updatedIndex, groupingKey: message.groupingKey, previousInfo: previousMessage.groupInfo, updatedGroupInfos: &updatedGroupInfos)
if previousMessage.tags != message.tags || index != updatedIndex { if previousMessage.tags != message.tags || index != updatedIndex {
let isNewlyAdded = previousMessage.tags.isEmpty
if !previousMessage.tags.isEmpty { if !previousMessage.tags.isEmpty {
self.tagsTable.remove(tags: previousMessage.tags, index: index, updatedSummaries: &updatedMessageTagSummaries, invalidateSummaries: &invalidateMessageTagSummaries) self.tagsTable.remove(tags: previousMessage.tags, index: index, updatedSummaries: &updatedMessageTagSummaries, invalidateSummaries: &invalidateMessageTagSummaries)
} }
if !message.tags.isEmpty { if !message.tags.isEmpty {
self.tagsTable.add(tags: message.tags, index: message.index, isNewlyAdded: isNewlyAdded, updatedSummaries: &updatedMessageTagSummaries, invalidateSummaries: &invalidateMessageTagSummaries) //let isNewlyAdded = previousMessage.tags.isEmpty
self.tagsTable.add(tags: message.tags, index: message.index, isNewlyAdded: false, updatedSummaries: &updatedMessageTagSummaries, invalidateSummaries: &invalidateMessageTagSummaries)
} }
} }
if previousMessage.threadId != message.threadId || index != message.index { if previousMessage.threadId != message.threadId || index != message.index {

View File

@ -133,7 +133,7 @@ class MessageHistoryTagsSummaryTable: Table {
func removeMessage(key: MessageHistoryTagsSummaryKey, id: MessageId.Id, updatedSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) { func removeMessage(key: MessageHistoryTagsSummaryKey, id: MessageId.Id, updatedSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
if let current = self.get(key) { if let current = self.get(key) {
if current.count == 0 { if current.count == 0 {
self.invalidateTable.insert(InvalidatedMessageHistoryTagsSummaryKey(peerId: key.peerId, namespace: key.namespace, tagMask: key.tag), operations: &invalidateSummaries) //self.invalidateTable.insert(InvalidatedMessageHistoryTagsSummaryKey(peerId: key.peerId, namespace: key.namespace, tagMask: key.tag), operations: &invalidateSummaries)
} else { } else {
self.set(key, summary: current.withAddedCount(-1), updatedSummaries: &updatedSummaries) self.set(key, summary: current.withAddedCount(-1), updatedSummaries: &updatedSummaries)
} }

View File

@ -1086,6 +1086,7 @@ public class Account {
self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(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(managedReadReactionActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedReadReactionActions(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(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenReactionsOperations(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(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(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())

View File

@ -157,6 +157,7 @@ private var declaredEncodables: Void = {
declareEncodable(TelegramDeviceContactImportedData.self, f: { TelegramDeviceContactImportedData(decoder: $0) }) declareEncodable(TelegramDeviceContactImportedData.self, f: { TelegramDeviceContactImportedData(decoder: $0) })
declareEncodable(SecureFileMediaResource.self, f: { SecureFileMediaResource(decoder: $0) }) declareEncodable(SecureFileMediaResource.self, f: { SecureFileMediaResource(decoder: $0) })
declareEncodable(SynchronizeMarkAllUnseenPersonalMessagesOperation.self, f: { SynchronizeMarkAllUnseenPersonalMessagesOperation(decoder: $0) }) declareEncodable(SynchronizeMarkAllUnseenPersonalMessagesOperation.self, f: { SynchronizeMarkAllUnseenPersonalMessagesOperation(decoder: $0) })
declareEncodable(SynchronizeMarkAllUnseenReactionsOperation.self, f: { SynchronizeMarkAllUnseenReactionsOperation(decoder: $0) })
declareEncodable(SynchronizeAppLogEventsOperation.self, f: { SynchronizeAppLogEventsOperation(decoder: $0) }) declareEncodable(SynchronizeAppLogEventsOperation.self, f: { SynchronizeAppLogEventsOperation(decoder: $0) })
declareEncodable(TelegramMediaPoll.self, f: { TelegramMediaPoll(decoder: $0) }) declareEncodable(TelegramMediaPoll.self, f: { TelegramMediaPoll(decoder: $0) })
declareEncodable(TelegramMediaUnsupported.self, f: { TelegramMediaUnsupported(decoder: $0) }) declareEncodable(TelegramMediaUnsupported.self, f: { TelegramMediaUnsupported(decoder: $0) })

View File

@ -3313,39 +3313,6 @@ func replayFinalState(
attributes.append(updatedReactions) attributes.append(updatedReactions)
} }
if let eventTimestamp = eventTimestamp, !currentMessage.flags.contains(.Incoming), let chatPeer = currentMessage.peers[currentMessage.id.peerId] {
let _ = chatPeer
var previousCount = 0
if let previousReactions = previousReactions {
for reaction in previousReactions.reactions {
previousCount += Int(reaction.count)
}
}
var updatedCount = 0
for reaction in updatedReactions.reactions {
updatedCount += Int(reaction.count)
}
if updatedCount > previousCount {
if let topPeer = updatedReactions.recentPeers.last {
var wasPresentBefore = false
if let previousReactions = previousReactions {
for recentPeer in previousReactions.recentPeers {
if recentPeer.peerId == topPeer.peerId {
wasPresentBefore = true
break
}
}
}
if !wasPresentBefore, let reactionAuthor = transaction.getPeer(topPeer.peerId), transaction.isPeerContact(peerId: topPeer.peerId) {
generatedEvent = (reactionAuthor: reactionAuthor, message: currentMessage.withUpdatedAttributes(attributes), timestamp: eventTimestamp)
}
}
}
}
var tags = currentMessage.tags var tags = currentMessage.tags
if updatedReactions.hasUnseen { if updatedReactions.hasUnseen {
tags.insert(.unseenReaction) tags.insert(.unseenReaction)

View File

@ -1228,6 +1228,29 @@ public final class AccountViewTracker {
} }
} }
public func updateMarkAllReactionsSeen(peerId: PeerId) {
self.queue.async {
guard let account = self.account else {
return
}
let _ = (account.postbox.transaction { transaction -> Set<MessageId> in
let ids = Set(transaction.getMessageIndicesWithTag(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: .unseenReaction).map({ $0.id }))
if let summary = transaction.getMessageTagSummary(peerId: peerId, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud), summary.count > 0 {
var maxId: Int32 = summary.range.maxId
if let index = transaction.getTopPeerMessageIndex(peerId: peerId, namespace: Namespaces.Message.Cloud) {
maxId = index.id.id
}
transaction.replaceMessageTagSummary(peerId: peerId, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, count: 0, maxId: maxId)
addSynchronizeMarkAllUnseenReactionsOperation(transaction: transaction, peerId: peerId, maxId: summary.range.maxId)
}
return ids
}
|> deliverOn(self.queue)).start()
}
}
public func updateMarkReactionsSeenForMessageIds(messageIds: Set<MessageId>) { public func updateMarkReactionsSeenForMessageIds(messageIds: Set<MessageId>) {
self.queue.async { self.queue.async {
let addedMessageIds: [MessageId] = Array(messageIds) let addedMessageIds: [MessageId] = Array(messageIds)

View File

@ -283,40 +283,71 @@ private func synchronizeConsumeMessageContents(transaction: Transaction, postbox
} }
private func synchronizeReadMessageReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Void, NoError> { private func synchronizeReadMessageReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Void, NoError> {
guard let inputPeer = transaction.getPeer(id.peerId).flatMap(apiInputPeer) else { if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup {
return .complete() return network.request(Api.functions.messages.readMessageContents(id: [id.id]))
} |> map(Optional.init)
return network.request(Api.functions.messages.readReactions(peer: inputPeer)) |> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
|> map(Optional.init) return .single(nil)
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, NoError> in }
return .single(nil) |> mapToSignal { result -> Signal<Void, NoError> in
} if let result = result {
|> mapToSignal { result -> Signal<Void, NoError> in switch result {
if let result = result { case let .affectedMessages(pts, ptsCount):
switch result { stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
case let .affectedHistory(pts, ptsCount, _): }
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)]) }
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .readReaction, 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, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute, attribute.hasUnseen {
attributes[j] = attribute.withAllSeen()
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, 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))
})
} }
} }
return postbox.transaction { transaction -> Void in } else if id.peerId.namespace == Namespaces.Peer.CloudChannel {
transaction.setPendingMessageAction(type: .readReaction, id: id, action: nil) if let peer = transaction.getPeer(id.peerId), let inputChannel = apiInputChannel(peer) {
transaction.updateMessage(id, update: { currentMessage in return network.request(Api.functions.channels.readMessageContents(channel: inputChannel, id: [id.id]))
var storeForwardInfo: StoreMessageForwardInfo? |> `catch` { _ -> Signal<Api.Bool, NoError> in
if let forwardInfo = currentMessage.forwardInfo { return .single(.boolFalse)
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) }
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .readReaction, 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, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute, attribute.hasUnseen {
attributes[j] = attribute.withAllSeen()
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, 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))
})
} }
var attributes = currentMessage.attributes }
loop: for j in 0 ..< attributes.count { } else {
if let attribute = attributes[j] as? ReactionsMessageAttribute, attribute.hasUnseen { return .complete()
attributes[j] = attribute.withAllSeen()
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, 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 {
return .complete()
} }
} }

View File

@ -4,7 +4,6 @@ import Postbox
import SwiftSignalKit import SwiftSignalKit
import MtProtoKit import MtProtoKit
private final class ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper { private final class ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:] var operationDisposables: [Int32: Disposable] = [:]
@ -49,11 +48,11 @@ private final class ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHel
} }
} }
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> { private func withTakenOperation<T>(postbox: Postbox, peerId: PeerId, operationType: T.Type, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry? var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeMarkAllUnseenPersonalMessagesOperation { if let entry = entry, let _ = entry.mergedIndex, entry.contents is T {
result = entry.mergedEntry! result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else { } else {
@ -62,7 +61,8 @@ private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOpera
}) })
return f(transaction, result) return f(transaction, result)
} |> switchToLatest }
|> switchToLatest
} }
func managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> { func managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
@ -81,7 +81,7 @@ func managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: Postbox,
} }
for (entry, disposable) in beginOperations { for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, operationType: SynchronizeMarkAllUnseenPersonalMessagesOperation.self, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry { if let entry = entry {
if let operation = entry.contents as? SynchronizeMarkAllUnseenPersonalMessagesOperation { if let operation = entry.contents as? SynchronizeMarkAllUnseenPersonalMessagesOperation {
return synchronizeMarkAllUnseen(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peerId: entry.peerId, operation: operation) return synchronizeMarkAllUnseen(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peerId: entry.peerId, operation: operation)
@ -233,6 +233,141 @@ func markUnseenPersonalMessage(transaction: Transaction, id: MessageId, addSynch
} }
} }
func managedSynchronizeMarkAllUnseenReactionsOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenReactions
let helper = Atomic<ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper>(value: ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, operationType: SynchronizeMarkAllUnseenReactionsOperation.self, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeMarkAllUnseenReactionsOperation {
return synchronizeMarkAllUnseenReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peerId: entry.peerId, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
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 synchronizeMarkAllUnseenReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, operation: SynchronizeMarkAllUnseenReactionsOperation) -> Signal<Void, NoError> {
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else {
return .complete()
}
let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel)
let limit: Int32 = 100
let oneOperation: (Int32) -> Signal<Int32?, MTRpcError> = { maxId in
return network.request(Api.functions.messages.getUnreadReactions(peer: inputPeer, offsetId: maxId, addOffset: maxId == 0 ? 0 : -1, limit: limit, maxId: maxId == 0 ? 0 : (maxId + 1), minId: 1))
|> mapToSignal { result -> Signal<[MessageId], MTRpcError> in
switch result {
case let .messages(messages, _, _):
return .single(messages.compactMap({ $0.id() }))
case let .channelMessages(_, _, _, _, messages, _, _):
return .single(messages.compactMap({ $0.id() }))
case .messagesNotModified:
return .single([])
case let .messagesSlice(_, _, _, _, messages, _, _):
return .single(messages.compactMap({ $0.id() }))
}
}
|> mapToSignal { ids -> Signal<Int32?, MTRpcError> in
let filteredIds = ids.filter { $0.id <= operation.maxId }
if filteredIds.isEmpty {
return .single(ids.min()?.id)
}
if peerId.namespace == Namespaces.Peer.CloudChannel {
guard let inputChannel = inputChannel else {
return .single(nil)
}
return network.request(Api.functions.channels.readMessageContents(channel: inputChannel, id: filteredIds.map { $0.id }))
|> map { result -> Int32? in
if ids.count < limit {
return nil
} else {
return ids.min()?.id
}
}
} else {
return network.request(Api.functions.messages.readMessageContents(id: filteredIds.map { $0.id }))
|> map { result -> Int32? in
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
if ids.count < limit {
return nil
} else {
return ids.min()?.id
}
}
}
}
}
let currentMaxId = Atomic<Int32>(value: 0)
let loopOperations: Signal<Void, GetUnseenIdsError> = (
(
deferred {
return oneOperation(currentMaxId.with { $0 })
}
|> `catch` { error -> Signal<Int32?, GetUnseenIdsError> in
return .fail(.error(error))
}
)
|> mapToSignal { resultId -> Signal<Void, GetUnseenIdsError> in
if let resultId = resultId {
let previous = currentMaxId.swap(resultId)
if previous == resultId {
return .fail(.done)
} else {
return .complete()
}
} else {
return .fail(.done)
}
}
|> `catch` { error -> Signal<Void, GetUnseenIdsError> in
switch error {
case .done, .error:
return .fail(error)
}
}
|> restart
)
return loopOperations
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
func markUnseenReactionMessage(transaction: Transaction, id: MessageId, addSynchronizeAction: Bool) { func markUnseenReactionMessage(transaction: Transaction, id: MessageId, addSynchronizeAction: Bool) {
if let message = transaction.getMessage(id) { if let message = transaction.getMessage(id) {
var consume = false var consume = false

View File

@ -2,7 +2,6 @@ import Foundation
import Postbox import Postbox
import SwiftSignalKit import SwiftSignalKit
func addSynchronizeMarkAllUnseenPersonalMessagesOperation(transaction: Transaction, peerId: PeerId, maxId: MessageId.Id) { func addSynchronizeMarkAllUnseenPersonalMessagesOperation(transaction: Transaction, peerId: PeerId, maxId: MessageId.Id) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenPersonalMessages let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenPersonalMessages
@ -25,3 +24,25 @@ func addSynchronizeMarkAllUnseenPersonalMessagesOperation(transaction: Transacti
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeMarkAllUnseenPersonalMessagesOperation(maxId: maxId)) transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeMarkAllUnseenPersonalMessagesOperation(maxId: maxId))
} }
func addSynchronizeMarkAllUnseenReactionsOperation(transaction: Transaction, peerId: PeerId, maxId: MessageId.Id) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenReactions
var topLocalIndex: Int32?
var currentMaxId: MessageId.Id?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
topLocalIndex = entry.tagLocalIndex
if let operation = entry.contents as? SynchronizeMarkAllUnseenReactionsOperation {
currentMaxId = operation.maxId
}
return false
})
if let topLocalIndex = topLocalIndex {
if let currentMaxId = currentMaxId, currentMaxId >= maxId {
return
}
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeMarkAllUnseenReactionsOperation(maxId: maxId))
}

View File

@ -156,6 +156,7 @@ public struct OperationLogTags {
public static let SynchronizeAppLogEvents = PeerOperationLogTag(value: 18) public static let SynchronizeAppLogEvents = PeerOperationLogTag(value: 18)
public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19) public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19)
public static let SynchronizeChatListFilters = PeerOperationLogTag(value: 20) public static let SynchronizeChatListFilters = PeerOperationLogTag(value: 20)
public static let SynchronizeMarkAllUnseenReactions = PeerOperationLogTag(value: 21)
} }
public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {

View File

@ -15,3 +15,19 @@ public final class SynchronizeMarkAllUnseenPersonalMessagesOperation: PostboxCod
encoder.encodeInt32(self.maxId, forKey: "maxId") encoder.encodeInt32(self.maxId, forKey: "maxId")
} }
} }
public final class SynchronizeMarkAllUnseenReactionsOperation: PostboxCoding {
public let maxId: MessageId.Id
public init(maxId: MessageId.Id) {
self.maxId = maxId
}
public init(decoder: PostboxDecoder) {
self.maxId = decoder.decodeInt32ForKey("maxId", orElse: Int32.min + 1)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.maxId, forKey: "maxId")
}
}

View File

@ -166,6 +166,16 @@ public func clearPeerUnseenPersonalMessagesInteractively(account: Account, peerI
|> ignoreValues |> ignoreValues
} }
public func clearPeerUnseenReactionsInteractively(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
if peerId.namespace == Namespaces.Peer.SecretChat {
return
}
account.viewTracker.updateMarkAllReactionsSeen(peerId: peerId)
}
|> ignoreValues
}
public func markAllChatsAsReadInteractively(transaction: Transaction, viewTracker: AccountViewTracker, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?) { public func markAllChatsAsReadInteractively(transaction: Transaction, viewTracker: AccountViewTracker, groupId: PeerGroupId, filterPredicate: ChatListFilterPredicate?) {
for peerId in transaction.getUnreadChatListPeerIds(groupId: groupId, filterPredicate: filterPredicate) { for peerId in transaction.getUnreadChatListPeerIds(groupId: groupId, filterPredicate: filterPredicate) {
togglePeerUnreadMarkInteractively(transaction: transaction, viewTracker: viewTracker, peerId: peerId, setToValue: false) togglePeerUnreadMarkInteractively(transaction: transaction, viewTracker: viewTracker, peerId: peerId, setToValue: false)

View File

@ -5842,26 +5842,43 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
self.chatDisplayNode.navigateButtons.mentionsMenu = { [weak self] in self.chatDisplayNode.navigateButtons.mentionsButton.activated = { [weak self] gesture, _ in
guard let strongSelf = self else { guard let strongSelf = self else {
gesture.cancel()
return return
} }
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [ strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
ActionSheetButtonItem(title: strongSelf.presentationData.strings.WebSearch_RecentSectionClear, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated() var menuItems: [ContextMenuItem] = []
menuItems.append(.action(ContextMenuActionItem(
id: nil,
text: strongSelf.presentationData.strings.WebSearch_RecentSectionClear,
textColor: .primary,
textLayout: .singleLine,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: theme.contextMenu.primaryColor)
},
action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
return return
} }
let _ = clearPeerUnseenPersonalMessagesInteractively(account: strongSelf.context.account, peerId: peerId).start() let _ = clearPeerUnseenPersonalMessagesInteractively(account: strongSelf.context.account, peerId: peerId).start()
}) }
]), ActionSheetItemGroup(items: [ )))
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in let items = ContextController.Items(content: .list(menuItems))
actionSheet?.dismissAnimated()
}) let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageNavigationButtonContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, contentNode: strongSelf.chatDisplayNode.navigateButtons.mentionsButton.containerNode)), items: .single(items), recognizer: nil, gesture: gesture)
])])
strongSelf.chatDisplayNode.dismissInput() strongSelf.forEachController({ controller in
strongSelf.present(actionSheet, in: .window(.root)) if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
strongSelf.window?.presentInGlobalOverlay(controller)
} }
self.chatDisplayNode.navigateButtons.reactionsPressed = { [weak self] in self.chatDisplayNode.navigateButtons.reactionsPressed = { [weak self] in
@ -5872,7 +5889,72 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch result { switch result {
case let .result(messageId): case let .result(messageId):
if let messageId = messageId { if let messageId = messageId {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil)) strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), completion: {
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else {
return
}
guard item.message.id == messageId else {
return
}
var maybeUpdatedReaction: String?
if let attribute = item.message.reactionsAttribute {
for recentPeer in attribute.recentPeers {
if recentPeer.isUnseen {
maybeUpdatedReaction = recentPeer.value
break
}
}
}
guard let updatedReaction = maybeUpdatedReaction else {
return
}
guard let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) else {
return
}
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation()
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: strongSelf.context,
theme: strongSelf.presentationData.theme,
reaction: ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
listAnimation: centerAnimation,
largeListAnimation: reaction.activateAnimation,
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation
),
targetView: targetView,
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
break
}
}
}
})
} }
case .loading: case .loading:
break break
@ -5882,6 +5964,45 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
self.chatDisplayNode.navigateButtons.reactionsButton.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
gesture.cancel()
return
}
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
var menuItems: [ContextMenuItem] = []
menuItems.append(.action(ContextMenuActionItem(
id: nil,
text: strongSelf.presentationData.strings.Conversation_ReadAllReactions,
textColor: .primary,
textLayout: .singleLine,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: theme.contextMenu.primaryColor)
},
action: { _, f in
f(.dismissWithoutContent)
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
return
}
let _ = clearPeerUnseenReactionsInteractively(account: strongSelf.context.account, peerId: peerId).start()
}
)))
let items = ContextController.Items(content: .list(menuItems))
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageNavigationButtonContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, contentNode: strongSelf.chatDisplayNode.navigateButtons.reactionsButton.containerNode)), items: .single(items), recognizer: nil, gesture: gesture)
strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
strongSelf.window?.presentInGlobalOverlay(controller)
}
let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, completion in let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, completion in
guard let strongSelf = self, strongSelf.isNodeLoaded else { guard let strongSelf = self, strongSelf.isNodeLoaded else {
return return
@ -12385,7 +12506,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.loadingMessage.set(.single(nil)) self.loadingMessage.set(.single(nil))
self.messageIndexDisposable.set(nil) self.messageIndexDisposable.set(nil)
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition) self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition)
completion?() Queue.mainQueue().after(0.25, {
completion?()
})
if case let .id(_, maybeTimecode) = messageLocation, let timecode = maybeTimecode { if case let .id(_, maybeTimecode) = messageLocation, let timecode = maybeTimecode {
let _ = self.controllerInteraction?.openMessage(message, .timecode(timecode)) let _ = self.controllerInteraction?.openMessage(message, .timecode(timecode))

View File

@ -732,7 +732,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if strongSelf.canReadHistoryValue { if strongSelf.canReadHistoryValue && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds) strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds)
} else { } else {
strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds) strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds)
@ -1342,7 +1342,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds)
} }
if strongSelf.canReadHistoryValue && !strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.isEmpty { if strongSelf.canReadHistoryValue && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.isEmpty {
let messageIds = strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen let messageIds = strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen
strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.removeAll() strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.removeAll()
context?.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds) context?.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds)

View File

@ -12,7 +12,9 @@ enum ChatHistoryNavigationButtonType {
case reactions case reactions
} }
class ChatHistoryNavigationButtonNode: ASControlNode { class ChatHistoryNavigationButtonNode: ContextControllerSourceNode {
let containerNode: ContextExtractedContentContainingNode
private let buttonNode: HighlightTrackingButtonNode
private let backgroundNode: NavigationBackgroundNode private let backgroundNode: NavigationBackgroundNode
private let imageNode: ASImageNode private let imageNode: ASImageNode
private let badgeBackgroundNode: ASImageNode private let badgeBackgroundNode: ASImageNode
@ -22,9 +24,9 @@ class ChatHistoryNavigationButtonNode: ASControlNode {
didSet { didSet {
if (oldValue != nil) != (self.tapped != nil) { if (oldValue != nil) != (self.tapped != nil) {
if self.tapped != nil { if self.tapped != nil {
self.addTarget(self, action: #selector(onTap), forControlEvents: .touchUpInside) self.buttonNode.addTarget(self, action: #selector(self.onTap), forControlEvents: .touchUpInside)
} else { } else {
self.removeTarget(self, action: #selector(onTap), forControlEvents: .touchUpInside) self.buttonNode.removeTarget(self, action: #selector(self.onTap), forControlEvents: .touchUpInside)
} }
} }
} }
@ -44,6 +46,9 @@ class ChatHistoryNavigationButtonNode: ASControlNode {
init(theme: PresentationTheme, type: ChatHistoryNavigationButtonType) { init(theme: PresentationTheme, type: ChatHistoryNavigationButtonType) {
self.theme = theme self.theme = theme
self.type = type self.type = type
self.containerNode = ContextExtractedContentContainingNode()
self.buttonNode = HighlightTrackingButtonNode()
self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor)
@ -71,18 +76,30 @@ class ChatHistoryNavigationButtonNode: ASControlNode {
self.badgeTextNode.displaysAsynchronously = false self.badgeTextNode.displaysAsynchronously = false
super.init() super.init()
self.addSubnode(self.backgroundNode)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: 38.0 / 2.0, transition: .immediate)
self.addSubnode(self.imageNode)
self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))
self.addSubnode(self.badgeBackgroundNode) self.targetNodeForActivationProgress = self.buttonNode
self.addSubnode(self.badgeTextNode)
self.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) self.addSubnode(self.containerNode)
let size = CGSize(width: 38.0, height: 38.0)
self.containerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.containerNode.contentRect = CGRect(origin: CGPoint(), size: size)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
self.containerNode.contentNode.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.backgroundNode)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: size.width / 2.0, transition: .immediate)
self.buttonNode.addSubnode(self.imageNode)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
self.buttonNode.addSubnode(self.badgeBackgroundNode)
self.buttonNode.addSubnode(self.badgeTextNode)
self.frame = CGRect(origin: CGPoint(), size: size)
} }
func updateTheme(theme: PresentationTheme) { func updateTheme(theme: PresentationTheme) {

View File

@ -8,10 +8,8 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
private var theme: PresentationTheme private var theme: PresentationTheme
private var dateTimeFormat: PresentationDateTimeFormat private var dateTimeFormat: PresentationDateTimeFormat
private let reactionsButton: ChatHistoryNavigationButtonNode let reactionsButton: ChatHistoryNavigationButtonNode
private let reactionsButtonTapNode: ASDisplayNode let mentionsButton: ChatHistoryNavigationButtonNode
private let mentionsButton: ChatHistoryNavigationButtonNode
private let mentionsButtonTapNode: ASDisplayNode
private let downButton: ChatHistoryNavigationButtonNode private let downButton: ChatHistoryNavigationButtonNode
var downPressed: (() -> Void)? { var downPressed: (() -> Void)? {
@ -21,9 +19,7 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
} }
var reactionsPressed: (() -> Void)? var reactionsPressed: (() -> Void)?
var reactionsMenu: (() -> Void)?
var mentionsPressed: (() -> Void)? var mentionsPressed: (() -> Void)?
var mentionsMenu: (() -> Void)?
var displayDownButton: Bool = false { var displayDownButton: Bool = false {
didSet { didSet {
@ -78,12 +74,10 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
self.mentionsButton = ChatHistoryNavigationButtonNode(theme: theme, type: .mentions) self.mentionsButton = ChatHistoryNavigationButtonNode(theme: theme, type: .mentions)
self.mentionsButton.alpha = 0.0 self.mentionsButton.alpha = 0.0
self.mentionsButton.isHidden = true self.mentionsButton.isHidden = true
self.mentionsButtonTapNode = ASDisplayNode()
self.reactionsButton = ChatHistoryNavigationButtonNode(theme: theme, type: .reactions) self.reactionsButton = ChatHistoryNavigationButtonNode(theme: theme, type: .reactions)
self.reactionsButton.alpha = 0.0 self.reactionsButton.alpha = 0.0
self.reactionsButton.isHidden = true self.reactionsButton.isHidden = true
self.reactionsButtonTapNode = ASDisplayNode()
self.downButton = ChatHistoryNavigationButtonNode(theme: theme, type: .down) self.downButton = ChatHistoryNavigationButtonNode(theme: theme, type: .down)
self.downButton.alpha = 0.0 self.downButton.alpha = 0.0
@ -91,29 +85,23 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
super.init() super.init()
self.mentionsButton.isUserInteractionEnabled = false
self.addSubnode(self.reactionsButton) self.addSubnode(self.reactionsButton)
self.addSubnode(self.reactionsButtonTapNode)
self.addSubnode(self.mentionsButton) self.addSubnode(self.mentionsButton)
self.addSubnode(self.mentionsButtonTapNode)
self.addSubnode(self.downButton) self.addSubnode(self.downButton)
self.reactionsButton.tapped = { [weak self] in
self?.reactionsPressed?()
}
self.mentionsButton.tapped = { [weak self] in
self?.mentionsPressed?()
}
self.downButton.isGestureEnabled = false
} }
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
let reactionsTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.reactionsTap(_:)))
reactionsTapRecognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.reactionsButtonTapNode.view.addGestureRecognizer(reactionsTapRecognizer)
let mentionsTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.mentionsTap(_:)))
mentionsTapRecognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.mentionsButtonTapNode.view.addGestureRecognizer(mentionsTapRecognizer)
} }
func update(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat) { func update(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat) {
@ -121,6 +109,7 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
self.theme = theme self.theme = theme
self.dateTimeFormat = dateTimeFormat self.dateTimeFormat = dateTimeFormat
self.reactionsButton.updateTheme(theme: theme)
self.mentionsButton.updateTheme(theme: theme) self.mentionsButton.updateTheme(theme: theme)
self.downButton.updateTheme(theme: theme) self.downButton.updateTheme(theme: theme)
} }
@ -154,7 +143,6 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
self.mentionsButton.isHidden = false self.mentionsButton.isHidden = false
transition.updateAlpha(node: self.mentionsButton, alpha: 1.0) transition.updateAlpha(node: self.mentionsButton, alpha: 1.0)
transition.updateTransformScale(node: self.mentionsButton, scale: 1.0) transition.updateTransformScale(node: self.mentionsButton, scale: 1.0)
self.mentionsButtonTapNode.isHidden = false
} else { } else {
transition.updateAlpha(node: self.mentionsButton, alpha: 0.0, completion: { [weak self] completed in transition.updateAlpha(node: self.mentionsButton, alpha: 0.0, completion: { [weak self] completed in
guard let strongSelf = self, completed else { guard let strongSelf = self, completed else {
@ -163,14 +151,12 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
strongSelf.mentionsButton.isHidden = true strongSelf.mentionsButton.isHidden = true
}) })
transition.updateTransformScale(node: self.mentionsButton, scale: 0.2) transition.updateTransformScale(node: self.mentionsButton, scale: 0.2)
self.mentionsButtonTapNode.isHidden = true
} }
if self.reactionsCount != 0 { if self.reactionsCount != 0 {
self.reactionsButton.isHidden = false self.reactionsButton.isHidden = false
transition.updateAlpha(node: self.reactionsButton, alpha: 1.0) transition.updateAlpha(node: self.reactionsButton, alpha: 1.0)
transition.updateTransformScale(node: self.reactionsButton, scale: 1.0) transition.updateTransformScale(node: self.reactionsButton, scale: 1.0)
self.reactionsButtonTapNode.isHidden = false
} else { } else {
transition.updateAlpha(node: self.reactionsButton, alpha: 0.0, completion: { [weak self] completed in transition.updateAlpha(node: self.reactionsButton, alpha: 0.0, completion: { [weak self] completed in
guard let strongSelf = self, completed else { guard let strongSelf = self, completed else {
@ -179,16 +165,13 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
strongSelf.reactionsButton.isHidden = true strongSelf.reactionsButton.isHidden = true
}) })
transition.updateTransformScale(node: self.reactionsButton, scale: 0.2) transition.updateTransformScale(node: self.reactionsButton, scale: 0.2)
self.reactionsButtonTapNode.isHidden = true
} }
transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center) transition.updatePosition(node: self.downButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height), size: buttonSize).center)
transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center) transition.updatePosition(node: self.mentionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize).center)
self.mentionsButtonTapNode.frame = CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize)
transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset - reactionsOffset), size: buttonSize).center) transition.updatePosition(node: self.reactionsButton, position: CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset - reactionsOffset), size: buttonSize).center)
self.reactionsButtonTapNode.frame = CGRect(origin: CGPoint(x: 0.0, y: completeSize.height - buttonSize.height - mentionsOffset), size: buttonSize)
return completeSize return completeSize
} }
@ -206,26 +189,6 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
} }
return nil return nil
} }
@objc private func reactionsTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state, let gesture = recognizer.lastRecognizedGestureAndLocation?.0 {
if case .tap = gesture {
self.reactionsPressed?()
} else if case .longTap = gesture {
self.reactionsMenu?()
}
}
}
@objc private func mentionsTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state, let gesture = recognizer.lastRecognizedGestureAndLocation?.0 {
if case .tap = gesture {
self.mentionsPressed?()
} else if case .longTap = gesture {
self.mentionsMenu?()
}
}
}
final class SnapshotState { final class SnapshotState {
fileprivate let downButtonSnapshotView: UIView? fileprivate let downButtonSnapshotView: UIView?

View File

@ -160,3 +160,37 @@ final class ChatMessageReactionContextExtractedContentSource: ContextExtractedCo
} }
} }
final class ChatMessageNavigationButtonContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = true
let blurBackground: Bool = true
let centerActionsHorizontally: Bool = true
private weak var chatNode: ChatControllerNode?
private let contentNode: ContextExtractedContentContainingNode
var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false)
}
init(chatNode: ChatControllerNode, contentNode: ContextExtractedContentContainingNode) {
self.chatNode = chatNode
self.contentNode = contentNode
}
func takeView() -> ContextControllerTakeViewInfo? {
guard let chatNode = self.chatNode else {
return nil
}
return ContextControllerTakeViewInfo(contentContainingNode: self.contentNode, contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
}
func putBack() -> ContextControllerPutBackViewInfo? {
guard let chatNode = self.chatNode else {
return nil
}
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
}
}