Initial implementation for reactions API

This commit is contained in:
Peter 2019-08-09 20:43:39 +03:00
parent 0f72e95e24
commit 8f463038e4
8 changed files with 307 additions and 19 deletions

View File

@ -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<AccountRunningImportantTasks, NoError>] = [

View File

@ -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
}()

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -15,29 +15,241 @@ import MtProtoKitDynamic
#endif
#endif
final class UpdateMessageReactionsAction: PendingMessageActionData {
init() {
}
public enum RequestUpdateMessageReactionError {
init(decoder: PostboxDecoder) {
}
func encode(_ encoder: PostboxEncoder) {
}
func isEqual(to: PendingMessageActionData) -> Bool {
if let _ = to as? UpdateMessageReactionsAction {
return true
} else {
return false
}
}
}
public func updateMessageReactionsInteractively(postbox: Postbox, messageId: MessageId, reactions: [String]) -> Signal<Never, NoError> {
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<Never, RequestUpdateMessageReactionError> {
return account.postbox.loadedPeerWithId(messageId.peerId)
|> take(1)
private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Never, RequestUpdateMessageReactionError> {
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<Never, RequestUpdateMessageReactionError> in
account.stateManager.addUpdates(result)
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<PeerId>()
var validIds = Set<MessageId>()
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<Never, NoError>) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Signal<Never, NoError> 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<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedApplyPendingMessageReactionsActionsHelper>(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<Never, NoError> 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<Never, NoError> {
return requestUpdateMessageReaction(postbox: postbox, network: network, stateManager: stateManager, messageId: id)
|> `catch` { _ -> Signal<Never, NoError> 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
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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?