diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index 031f213a58..ffc83cb669 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -225,7 +225,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { var attributes: [MessageAttribute] = [] if let reaction = item.reaction { - attributes.append(ReactionsMessageAttribute(canViewList: false, reactions: [MessageReaction(value: reaction, count: 1, isSelected: true)], recentPeers: [])) + attributes.append(ReactionsMessageAttribute(canViewList: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: [])) } let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, isCentered: true) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index fa06a4f9f7..f5efaafca9 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -612,7 +612,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1992950669] = { return Api.Reaction.parse_reactionCustomEmoji($0) } dict[455247544] = { return Api.Reaction.parse_reactionEmoji($0) } dict[2046153753] = { return Api.Reaction.parse_reactionEmpty($0) } - dict[609529328] = { return Api.ReactionCount.parse_reactionCount($0) } + dict[-1546531968] = { return Api.ReactionCount.parse_reactionCount($0) } dict[-1551583367] = { return Api.ReceivedNotifyMessage.parse_receivedNotifyMessage($0) } dict[-1294306862] = { return Api.RecentMeUrl.parse_recentMeUrlChat($0) } dict[-347535331] = { return Api.RecentMeUrl.parse_recentMeUrlChatInvite($0) } @@ -829,10 +829,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1821035490] = { return Api.Update.parse_updateSavedGifs($0) } dict[1960361625] = { return Api.Update.parse_updateSavedRingtones($0) } dict[-337352679] = { return Api.Update.parse_updateServiceNotification($0) } - dict[1135492588] = { return Api.Update.parse_updateStickerSets($0) } + dict[834816008] = { return Api.Update.parse_updateStickerSets($0) } dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } dict[-2112423005] = { return Api.Update.parse_updateTheme($0) } dict[8703322] = { return Api.Update.parse_updateTranscribedAudio($0) } + dict[674706841] = { return Api.Update.parse_updateUserEmojiStatus($0) } dict[-1007549728] = { return Api.Update.parse_updateUserName($0) } dict[88680979] = { return Api.Update.parse_updateUserPhone($0) } dict[-232290676] = { return Api.Update.parse_updateUserPhoto($0) } diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index f21b0d678e..c6b278384d 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -68,15 +68,16 @@ public extension Api { } public extension Api { enum ReactionCount: TypeConstructorDescription { - case reactionCount(flags: Int32, reaction: Api.Reaction, count: Int32) + case reactionCount(flags: Int32, chosenOrder: Int32?, reaction: Api.Reaction, count: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .reactionCount(let flags, let reaction, let count): + case .reactionCount(let flags, let chosenOrder, let reaction, let count): if boxed { - buffer.appendInt32(609529328) + buffer.appendInt32(-1546531968) } serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(chosenOrder!, buffer: buffer, boxed: false)} reaction.serialize(buffer, true) serializeInt32(count, buffer: buffer, boxed: false) break @@ -85,25 +86,28 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .reactionCount(let flags, let reaction, let count): - return ("reactionCount", [("flags", String(describing: flags)), ("reaction", String(describing: reaction)), ("count", String(describing: count))]) + case .reactionCount(let flags, let chosenOrder, let reaction, let count): + return ("reactionCount", [("flags", String(describing: flags)), ("chosenOrder", String(describing: chosenOrder)), ("reaction", String(describing: reaction)), ("count", String(describing: count))]) } } public static func parse_reactionCount(_ reader: BufferReader) -> ReactionCount? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.Reaction? + var _2: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_2 = reader.readInt32() } + var _3: Api.Reaction? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Reaction + _3 = Api.parse(reader, signature: signature) as? Api.Reaction } - var _3: Int32? - _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil - let _c2 = _2 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.ReactionCount.reactionCount(flags: _1!, reaction: _2!, count: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ReactionCount.reactionCount(flags: _1!, chosenOrder: _2, reaction: _3!, count: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 3c325af00d..0a2d2faea1 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -644,10 +644,11 @@ public extension Api { case updateSavedGifs case updateSavedRingtones case updateServiceNotification(flags: Int32, inboxDate: Int32?, type: String, message: String, media: Api.MessageMedia, entities: [Api.MessageEntity]) - case updateStickerSets + case updateStickerSets(flags: Int32) case updateStickerSetsOrder(flags: Int32, order: [Int64]) case updateTheme(theme: Api.Theme) case updateTranscribedAudio(flags: Int32, peer: Api.Peer, msgId: Int32, transcriptionId: Int64, text: String) + case updateUserEmojiStatus(userId: Int64, emojiStatus: Api.EmojiStatus) case updateUserName(userId: Int64, firstName: String, lastName: String, username: String) case updateUserPhone(userId: Int64, phone: String) case updateUserPhoto(userId: Int64, date: Int32, photo: Api.UserProfilePhoto, previous: Api.Bool) @@ -1461,11 +1462,11 @@ public extension Api { item.serialize(buffer, true) } break - case .updateStickerSets: + case .updateStickerSets(let flags): if boxed { - buffer.appendInt32(1135492588) + buffer.appendInt32(834816008) } - + serializeInt32(flags, buffer: buffer, boxed: false) break case .updateStickerSetsOrder(let flags, let order): if boxed { @@ -1494,6 +1495,13 @@ public extension Api { serializeInt64(transcriptionId, buffer: buffer, boxed: false) serializeString(text, buffer: buffer, boxed: false) break + case .updateUserEmojiStatus(let userId, let emojiStatus): + if boxed { + buffer.appendInt32(674706841) + } + serializeInt64(userId, buffer: buffer, boxed: false) + emojiStatus.serialize(buffer, true) + break case .updateUserName(let userId, let firstName, let lastName, let username): if boxed { buffer.appendInt32(-1007549728) @@ -1736,14 +1744,16 @@ public extension Api { return ("updateSavedRingtones", []) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): return ("updateServiceNotification", [("flags", String(describing: flags)), ("inboxDate", String(describing: inboxDate)), ("type", String(describing: type)), ("message", String(describing: message)), ("media", String(describing: media)), ("entities", String(describing: entities))]) - case .updateStickerSets: - return ("updateStickerSets", []) + case .updateStickerSets(let flags): + return ("updateStickerSets", [("flags", String(describing: flags))]) case .updateStickerSetsOrder(let flags, let order): return ("updateStickerSetsOrder", [("flags", String(describing: flags)), ("order", String(describing: order))]) case .updateTheme(let theme): return ("updateTheme", [("theme", String(describing: theme))]) case .updateTranscribedAudio(let flags, let peer, let msgId, let transcriptionId, let text): return ("updateTranscribedAudio", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("transcriptionId", String(describing: transcriptionId)), ("text", String(describing: text))]) + case .updateUserEmojiStatus(let userId, let emojiStatus): + return ("updateUserEmojiStatus", [("userId", String(describing: userId)), ("emojiStatus", String(describing: emojiStatus))]) case .updateUserName(let userId, let firstName, let lastName, let username): return ("updateUserName", [("userId", String(describing: userId)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("username", String(describing: username))]) case .updateUserPhone(let userId, let phone): @@ -3377,7 +3387,15 @@ public extension Api { } } public static func parse_updateStickerSets(_ reader: BufferReader) -> Update? { - return Api.Update.updateStickerSets + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateStickerSets(flags: _1!) + } + else { + return nil + } } public static func parse_updateStickerSetsOrder(_ reader: BufferReader) -> Update? { var _1: Int32? @@ -3433,6 +3451,22 @@ public extension Api { return nil } } + public static func parse_updateUserEmojiStatus(_ reader: BufferReader) -> Update? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Api.EmojiStatus? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.EmojiStatus + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updateUserEmojiStatus(userId: _1!, emojiStatus: _2!) + } + else { + return nil + } + } public static func parse_updateUserName(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index a46513a518..9157386322 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -5726,13 +5726,17 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendReaction(flags: Int32, peer: Api.InputPeer, msgId: Int32, reaction: Api.Reaction?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendReaction(flags: Int32, peer: Api.InputPeer, msgId: Int32, reaction: [Api.Reaction]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1526634933) + buffer.appendInt32(-754091820) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {reaction!.serialize(buffer, true)} + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reaction!.count)) + for item in reaction! { + item.serialize(buffer, true) + }} return (FunctionDescription(name: "messages.sendReaction", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("reaction", String(describing: reaction))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? @@ -6466,23 +6470,6 @@ public extension Api.functions.payments { }) } } -public extension Api.functions.payments { - static func requestRecurringPayment(userId: Api.InputUser, recurringInitCharge: String, invoiceMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(342791565) - userId.serialize(buffer, true) - serializeString(recurringInitCharge, buffer: buffer, boxed: false) - invoiceMedia.serialize(buffer, true) - return (FunctionDescription(name: "payments.requestRecurringPayment", parameters: [("userId", String(describing: userId)), ("recurringInitCharge", String(describing: recurringInitCharge)), ("invoiceMedia", String(describing: invoiceMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in - let reader = BufferReader(buffer) - var result: Api.Updates? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Updates - } - return result - }) - } -} public extension Api.functions.payments { static func sendPaymentForm(flags: Int32, formId: Int64, invoice: Api.InputInvoice, requestedInfoId: String?, shippingOptionId: String?, credentials: Api.InputPaymentCredentials, tipAmount: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index 88eb9cfddb..2d5d9b5607 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -10,9 +10,9 @@ extension ReactionsMessageAttribute { let canViewList = (flags & (1 << 2)) != 0 var reactions = results.compactMap { result -> MessageReaction? in switch result { - case let .reactionCount(flags, reaction, count): + case let .reactionCount(_, chosenOrder, reaction, count): if let reaction = MessageReaction.Reaction(apiReaction: reaction) { - return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0) + return MessageReaction(value: reaction, count: count, chosenOrder: chosenOrder.flatMap(Int.init)) } else { return nil } @@ -37,17 +37,17 @@ extension ReactionsMessageAttribute { } if min { - var currentSelectedReaction: MessageReaction.Reaction? + var currentSelectedReactions: [MessageReaction.Reaction: Int] = [:] for reaction in self.reactions { - if reaction.isSelected { - currentSelectedReaction = reaction.value + if let chosenOrder = reaction.chosenOrder { + currentSelectedReactions[reaction.value] = chosenOrder break } } - if let currentSelectedReaction = currentSelectedReaction { + if !currentSelectedReactions.isEmpty { for i in 0 ..< reactions.count { - if reactions[i].value == currentSelectedReaction { - reactions[i].isSelected = true + if let chosenOrder = currentSelectedReactions[reactions[i].value] { + reactions[i].chosenOrder = chosenOrder } } } @@ -76,6 +76,52 @@ public func mergedMessageReactionsAndPeers(message: Message) -> (reactions: [Mes return (attribute.reactions, recentPeers) } +private func mergeReactions(reactions: [MessageReaction], recentPeers: [ReactionsMessageAttribute.RecentPeer], pending: [PendingReactionsMessageAttribute.PendingReaction], accountPeerId: PeerId) -> ([MessageReaction], [ReactionsMessageAttribute.RecentPeer]) { + var result = reactions + var recentPeers = recentPeers + + for pendingReaction in pending { + if let index = result.firstIndex(where: { $0.value == pendingReaction.value }) { + var merged = result[index] + if merged.chosenOrder == nil { + merged.chosenOrder = Int(Int32.max) + merged.count += 1 + } + result[index] = merged + } else { + result.append(MessageReaction(value: pendingReaction.value, count: 1, chosenOrder: Int(Int32.max))) + } + + if let index = recentPeers.firstIndex(where: { $0.value == pendingReaction.value && $0.peerId == accountPeerId }) { + recentPeers.remove(at: index) + } + recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: pendingReaction.value, isLarge: false, isUnseen: false, peerId: accountPeerId)) + } + + for i in (0 ..< result.count).reversed() { + if result[i].chosenOrder != nil { + if !pending.contains(where: { $0.value == result[i].value }) { + if let index = recentPeers.firstIndex(where: { $0.value == result[i].value && $0.peerId == accountPeerId }) { + recentPeers.remove(at: index) + } + + if result[i].count <= 1 { + result.remove(at: i) + } else { + result[i].count -= 1 + result[i].chosenOrder = nil + } + } + } + } + + if recentPeers.count > 3 { + recentPeers.removeFirst(recentPeers.count - 3) + } + + return (result, recentPeers) +} + public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? { var current: ReactionsMessageAttribute? var pending: PendingReactionsMessageAttribute? @@ -87,45 +133,14 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM } } - if let pending = pending { + if let pending = pending, let accountPeerId = pending.accountPeerId { var reactions = current?.reactions ?? [] var recentPeers = current?.recentPeers ?? [] - if let value = pending.value { - 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)) - } - } - if let accountPeerId = pending.accountPeerId { - for i in 0 ..< recentPeers.count { - if recentPeers[i].peerId == accountPeerId { - recentPeers.remove(at: i) - break - } - } - if let value = pending.value { - recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: value, isLarge: false, isUnseen: false, peerId: accountPeerId)) - } - } - for i in (0 ..< reactions.count).reversed() { - if reactions[i].isSelected, pending.value != reactions[i].value { - if reactions[i].count == 1 { - reactions.remove(at: i) - } else { - reactions[i].isSelected = false - reactions[i].count -= 1 - } - } - } + + let (updatedReactions, updatedRecentPeers) = mergeReactions(reactions: reactions, recentPeers: recentPeers, pending: pending.reactions, accountPeerId: accountPeerId) + reactions = updatedReactions + recentPeers = updatedRecentPeers + if !reactions.isEmpty { return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, reactions: reactions, recentPeers: recentPeers) } else { @@ -165,9 +180,9 @@ extension ReactionsMessageAttribute { canViewList: canViewList, reactions: results.compactMap { result -> MessageReaction? in switch result { - case let .reactionCount(flags, reaction, count): + case let .reactionCount(_, chosenOrder, reaction, count): if let reaction = MessageReaction.Reaction(apiReaction: reaction) { - return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0) + return MessageReaction(value: reaction, count: count, chosenOrder: chosenOrder.flatMap(Int.init)) } else { return nil } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index c7a54d91f0..4394e5e1ed 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -14,10 +14,10 @@ private func reactionGeneratedEvent(_ previousReactions: ReactionsMessageAttribu }) } let myUpdated = updatedReactions.reactions.filter { value in - return value.isSelected + return value.chosenOrder != nil }.first let myPrevious = prev.filter { value in - return value.isSelected + return value.chosenOrder != nil }.first let previousCount = prev.reduce(0, { @@ -28,7 +28,7 @@ private func reactionGeneratedEvent(_ previousReactions: ReactionsMessageAttribu }) let newReaction = updated.filter { - !$0.isSelected + $0.chosenOrder == nil }.first?.value if !updated.isEmpty && myUpdated == myPrevious, updatedCount >= previousCount, let value = newReaction { diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 02439b97e2..cd50840a64 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -18,20 +18,29 @@ public enum UpdateMessageReaction { } } -public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reaction: UpdateMessageReaction?, isLarge: Bool) -> Signal { +public func updateMessageReactionsInteractively(account: Account, messageId: MessageId, reactions: [UpdateMessageReaction], isLarge: Bool) -> Signal { return account.postbox.transaction { transaction -> Void in - let mappedReaction: MessageReaction.Reaction? + let isPremium = (transaction.getPeer(account.peerId) as? TelegramUser)?.isPremium ?? false + let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue + let maxCount: Int + if isPremium { + let limitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: isPremium) + maxCount = Int(limitsConfiguration.maxReactionsPerMessage) + } else { + maxCount = 1 + } - switch reaction { - case .none: - mappedReaction = nil - case let .custom(fileId, file): - mappedReaction = .custom(fileId) - if let file = file { - transaction.storeMediaIfNotPresent(media: file) + var mappedReactions: [PendingReactionsMessageAttribute.PendingReaction] = [] + for reaction in reactions { + switch reaction { + case let .custom(fileId, file): + mappedReactions.append(PendingReactionsMessageAttribute.PendingReaction(value: .custom(fileId))) + if let file = file { + transaction.storeMediaIfNotPresent(media: file) + } + case let .builtin(value): + mappedReactions.append(PendingReactionsMessageAttribute.PendingReaction(value: .builtin(value))) } - case let .builtin(value): - mappedReaction = .builtin(value) } transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction()) @@ -47,7 +56,20 @@ public func updateMessageReactionsInteractively(account: Account, messageId: Mes break loop } } - attributes.append(PendingReactionsMessageAttribute(accountPeerId: account.peerId, value: mappedReaction, isLarge: isLarge)) + + var mappedReactions = mappedReactions + + let updatedReactions = mergedMessageReactions(attributes: attributes + [PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge)])?.reactions ?? [] + let updatedOutgoingReactions = updatedReactions.filter(\.isSelected) + if updatedOutgoingReactions.count > maxCount { + let sortedOutgoingReactions = updatedOutgoingReactions.sorted(by: { $0.chosenOrder! < $1.chosenOrder! }) + mappedReactions = Array(sortedOutgoingReactions.suffix(maxCount).map { reaction -> PendingReactionsMessageAttribute.PendingReaction in + return PendingReactionsMessageAttribute.PendingReaction(value: reaction.value) + }) + } + + attributes.append(PendingReactionsMessageAttribute(accountPeerId: account.peerId, reactions: mappedReactions, isLarge: isLarge)) + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, 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)) }) } @@ -59,27 +81,29 @@ private enum RequestUpdateMessageReactionError { } private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { - return postbox.transaction { transaction -> (Peer, MessageReaction.Reaction?, Bool)? in + return postbox.transaction { transaction -> (Peer, [MessageReaction.Reaction]?, Bool)? in guard let peer = transaction.getPeer(messageId.peerId) else { return nil } guard let message = transaction.getMessage(messageId) else { return nil } - var value: MessageReaction.Reaction? + var reactions: [MessageReaction.Reaction]? var isLarge: Bool = false for attribute in message.attributes { if let attribute = attribute as? PendingReactionsMessageAttribute { - value = attribute.value + if !attribute.reactions.isEmpty { + reactions = attribute.reactions.map(\.value) + } isLarge = attribute.isLarge break } } - return (peer, value, isLarge) + return (peer, reactions, isLarge) } |> castError(RequestUpdateMessageReactionError.self) |> mapToSignal { peerAndValue in - guard let (peer, value, isLarge) = peerAndValue else { + guard let (peer, reactions, isLarge) = peerAndValue else { return .fail(.generic) } guard let inputPeer = apiInputPeer(peer) else { @@ -90,14 +114,14 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st } var flags: Int32 = 0 - if value != nil { + if reactions != nil { flags |= 1 << 0 if isLarge { flags |= 1 << 1 } } - let signal: Signal = network.request(Api.functions.messages.sendReaction(flags: flags, peer: inputPeer, msgId: messageId.id, reaction: value?.apiReaction)) + let signal: Signal = network.request(Api.functions.messages.sendReaction(flags: flags, peer: inputPeer, msgId: messageId.id, reaction: reactions?.map(\.apiReaction))) |> mapError { _ -> RequestUpdateMessageReactionError in return .generic } @@ -276,7 +300,7 @@ public extension EngineMessageReactionListContext.State { if let reactionsAttribute = message._asMessage().reactionsAttribute { for messageReaction in reactionsAttribute.reactions { if reaction == nil || messageReaction.value == reaction { - if messageReaction.isSelected { + if messageReaction.chosenOrder != nil { hasOutgoingReaction = true } totalCount += Int(messageReaction.count) diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index 5789a5043f..5376742337 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -13,6 +13,7 @@ public struct UserLimitsConfiguration: Equatable { public let maxUploadFileParts: Int32 public let maxAboutLength: Int32 public let maxAnimatedEmojisInText: Int32 + public let maxReactionsPerMessage: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -26,7 +27,8 @@ public struct UserLimitsConfiguration: Equatable { maxCaptionLength: 1024, maxUploadFileParts: 4000, maxAboutLength: 70, - maxAnimatedEmojisInText: 10 + maxAnimatedEmojisInText: 10, + maxReactionsPerMessage: 1 ) } @@ -41,7 +43,8 @@ public struct UserLimitsConfiguration: Equatable { maxCaptionLength: Int32, maxUploadFileParts: Int32, maxAboutLength: Int32, - maxAnimatedEmojisInText: Int32 + maxAnimatedEmojisInText: Int32, + maxReactionsPerMessage: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxChannelsCount = maxChannelsCount @@ -54,6 +57,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxUploadFileParts = maxUploadFileParts self.maxAboutLength = maxAboutLength self.maxAnimatedEmojisInText = maxAnimatedEmojisInText + self.maxReactionsPerMessage = maxReactionsPerMessage } } @@ -89,5 +93,6 @@ extension UserLimitsConfiguration { self.maxUploadFileParts = getValue("upload_max_fileparts", orElse: defaultValue.maxUploadFileParts) self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength) self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText) + self.maxReactionsPerMessage = getGeneralValue("reactions_user_max", orElse: 1) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index 0c0e306188..3e9a07c928 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -2,7 +2,7 @@ import Postbox import TelegramApi public struct MessageReaction: Equatable, PostboxCoding { - public enum Reaction: Hashable, Codable { + public enum Reaction: Hashable, Codable, PostboxCoding { case builtin(String) case custom(Int64) @@ -16,6 +16,14 @@ public struct MessageReaction: Equatable, PostboxCoding { } } + public init(decoder: PostboxDecoder) { + if let value = decoder.decodeOptionalStringForKey("v") { + self = .builtin(value) + } else { + self = .custom(decoder.decodeInt64ForKey("cfid", orElse: 0)) + } + } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) @@ -26,16 +34,29 @@ public struct MessageReaction: Equatable, PostboxCoding { try container.encode(fileId, forKey: "cfid") } } + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case let .builtin(value): + encoder.encodeString(value, forKey: "v") + case let .custom(fileId): + encoder.encodeInt64(fileId, forKey: "cfid") + } + } } public var value: Reaction public var count: Int32 - public var isSelected: Bool + public var chosenOrder: Int? - public init(value: Reaction, count: Int32, isSelected: Bool) { + public var isSelected: Bool { + return self.chosenOrder != nil + } + + public init(value: Reaction, count: Int32, chosenOrder: Int?) { self.value = value self.count = count - self.isSelected = isSelected + self.chosenOrder = chosenOrder } public init(decoder: PostboxDecoder) { @@ -45,7 +66,13 @@ public struct MessageReaction: Equatable, PostboxCoding { self.value = .custom(decoder.decodeInt64ForKey("cfid", orElse: 0)) } self.count = decoder.decodeInt32ForKey("c", orElse: 0) - self.isSelected = decoder.decodeInt32ForKey("s", orElse: 0) != 0 + if let chosenOrder = decoder.decodeOptionalInt32ForKey("cord") { + self.chosenOrder = Int(chosenOrder) + } else if let isSelected = decoder.decodeOptionalInt32ForKey("s"), isSelected != 0 { + self.chosenOrder = 0 + } else { + self.chosenOrder = nil + } } public func encode(_ encoder: PostboxEncoder) { @@ -56,7 +83,11 @@ public struct MessageReaction: Equatable, PostboxCoding { encoder.encodeInt64(fileId, forKey: "cfid") } encoder.encodeInt32(self.count, forKey: "c") - encoder.encodeInt32(self.isSelected ? 1 : 0, forKey: "s") + if let chosenOrder = self.chosenOrder { + encoder.encodeInt32(Int32(chosenOrder), forKey: "cord") + } else { + encoder.encodeNil(forKey: "cord") + } } } @@ -182,8 +213,24 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { } public final class PendingReactionsMessageAttribute: MessageAttribute { + public struct PendingReaction: Equatable, PostboxCoding { + public var value: MessageReaction.Reaction + + public init(value: MessageReaction.Reaction) { + self.value = value + } + + public init(decoder: PostboxDecoder) { + self.value = decoder.decodeObjectForKey("val", decoder: { MessageReaction.Reaction(decoder: $0) }) as! MessageReaction.Reaction + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.value, forKey: "val") + } + } + public let accountPeerId: PeerId? - public let value: MessageReaction.Reaction? + public let reactions: [PendingReaction] public let isLarge: Bool public var associatedPeerIds: [PeerId] { @@ -194,21 +241,15 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { } } - public init(accountPeerId: PeerId?, value: MessageReaction.Reaction?, isLarge: Bool) { + public init(accountPeerId: PeerId?, reactions: [PendingReaction], isLarge: Bool) { self.accountPeerId = accountPeerId - self.value = value + self.reactions = reactions self.isLarge = isLarge } required public init(decoder: PostboxDecoder) { self.accountPeerId = decoder.decodeOptionalInt64ForKey("ap").flatMap(PeerId.init) - if let value = decoder.decodeOptionalStringForKey("v") { - self.value = .builtin(value) - } else if let fileId = decoder.decodeOptionalInt64ForKey("cfid") { - self.value = .custom(fileId) - } else { - self.value = nil - } + self.reactions = decoder.decodeObjectArrayWithDecoderForKey("reac") self.isLarge = decoder.decodeInt32ForKey("l", orElse: 0) != 0 } @@ -218,16 +259,9 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { } else { encoder.encodeNil(forKey: "ap") } - if let value = self.value { - switch value { - case let .builtin(value): - encoder.encodeString(value, forKey: "v") - case let .custom(fileId): - encoder.encodeInt64(fileId, forKey: "cfid") - } - } else { - encoder.encodeNil(forKey: "v") - } + + encoder.encodeObjectArray(self.reactions, forKey: "reac") + encoder.encodeInt32(self.isLarge ? 1 : 0, forKey: "l") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 447f8e3107..c5cb21304d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -62,6 +62,7 @@ public enum EngineConfiguration { public let maxUploadFileParts: Int32 public let maxAboutLength: Int32 public let maxAnimatedEmojisInText: Int32 + public let maxReactionsPerMessage: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -78,7 +79,8 @@ public enum EngineConfiguration { maxCaptionLength: Int32, maxUploadFileParts: Int32, maxAboutLength: Int32, - maxAnimatedEmojisInText: Int32 + maxAnimatedEmojisInText: Int32, + maxReactionsPerMessage: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxChannelsCount = maxChannelsCount @@ -91,6 +93,7 @@ public enum EngineConfiguration { self.maxUploadFileParts = maxUploadFileParts self.maxAboutLength = maxAboutLength self.maxAnimatedEmojisInText = maxAnimatedEmojisInText + self.maxReactionsPerMessage = maxReactionsPerMessage } } } @@ -148,7 +151,8 @@ public extension EngineConfiguration.UserLimits { maxCaptionLength: userLimitsConfiguration.maxCaptionLength, maxUploadFileParts: userLimitsConfiguration.maxUploadFileParts, maxAboutLength: userLimitsConfiguration.maxAboutLength, - maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText + maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText, + maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage ) } } diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 85b40fb025..d015b7c0c8 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -330,7 +330,7 @@ public extension Message { } for attribute in self.attributes { if let attribute = attribute as? PendingReactionsMessageAttribute { - return attribute.value != nil + return !attribute.reactions.isEmpty } } return false diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3927111dd0..7c9d56f361 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1308,7 +1308,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.push(controller) } - controller.reactionSelected = { [weak controller] reaction, isLarge in + controller.reactionSelected = { [weak controller] chosenUpdatedReaction, isLarge in guard let strongSelf = self else { return } @@ -1317,7 +1317,97 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var updatedReaction: UpdateMessageReaction? = reaction + let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction + + let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) + var removedReaction: MessageReaction.Reaction? + var isFirst = false + + if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { + removedReaction = chosenReaction + updatedReactions.remove(at: index) + } else { + updatedReactions.append(chosenReaction) + isFirst = !currentReactions.contains(where: { $0.value == chosenReaction }) + } + + /*guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() + return + } + + switch allowedReactions { + case let .set(set): + if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { + itemNode.openMessageContextMenu() + return + } + case .all: + break + }*/ + + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + if item.message.id == message.id { + if removedReaction == nil && !updatedReactions.isEmpty { + itemNode.awaitingAppliedReaction = (chosenReaction, { [weak itemNode] in + guard let controller = controller else { + return + } + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) + + var hideTargetButton: UIView? + if isFirst { + hideTargetButton = targetView.superview + } + + controller.dismissWithReaction(value: chosenReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, addStandaloneReactionAnimation: { standaloneReactionAnimation in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, completion: { [weak itemNode, weak targetView] in + guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { + return + } + + let _ = strongSelf + let _ = itemNode + let _ = targetView + }) + } + }) + } else { + itemNode.awaitingAppliedReaction = (nil, { + controller?.dismiss() + }) + } + } + } + } + + let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in + switch reaction { + case let .builtin(value): + return .builtin(value) + case let .custom(fileId): + var customFile: TelegramMediaFile? + if case let .custom(customFileId, file) = chosenUpdatedReaction, fileId == customFileId { + customFile = file + } + return .custom(fileId: fileId, file: customFile) + } + } + + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: isLarge).start() + + /*let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) + var isFirst = true for attribute in topMessage.attributes { if let attribute = attribute as? ReactionsMessageAttribute { @@ -1336,49 +1426,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { - if item.message.id == message.id { - if let updatedReaction = updatedReaction { - itemNode.awaitingAppliedReaction = (updatedReaction.reaction, { [weak itemNode] in - guard let controller = controller else { - return - } - if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: updatedReaction.reaction) { - strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) - - var hideTargetButton: UIView? - if isFirst { - hideTargetButton = targetView.superview - } - - controller.dismissWithReaction(value: updatedReaction.reaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, addStandaloneReactionAnimation: { standaloneReactionAnimation in - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - }, completion: { [weak itemNode, weak targetView] in - guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { - return - } - - let _ = strongSelf - let _ = itemNode - let _ = targetView - }) - } - }) - } else if updatedReaction == nil { - itemNode.awaitingAppliedReaction = (nil, { - controller?.dismiss() - }) - } - } - } - } - let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction, isLarge: isLarge).start() + + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction, isLarge: isLarge).start()*/ } strongSelf.forEachController({ controller in @@ -1461,92 +1510,71 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var updatedReaction: UpdateMessageReaction? + let chosenReaction: MessageReaction.Reaction? + switch reaction { case .default: switch item.associatedData.defaultReaction { case .none: - updatedReaction = nil + chosenReaction = nil case let .builtin(value): - updatedReaction = .builtin(value) + chosenReaction = .builtin(value) case let .custom(fileId): - updatedReaction = .custom(fileId: fileId, file: nil) + chosenReaction = .custom(fileId) } case let .reaction(value): switch value { case let .builtin(value): - updatedReaction = .builtin(value) + chosenReaction = .builtin(value) case let .custom(fileId): - updatedReaction = .custom(fileId: fileId, file: nil) + chosenReaction = .custom(fileId) } } + guard let chosenReaction = chosenReaction else { + return + } + var removedReaction: MessageReaction.Reaction? var messageAlreadyHasThisReaction = false - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - for listReaction in attribute.reactions { - switch reaction { - case .default: - if listReaction.isSelected { - updatedReaction = nil - removedReaction = listReaction.value - } else if listReaction.value == updatedReaction?.reaction { - messageAlreadyHasThisReaction = true - } - case let .reaction(value): - if listReaction.value == value { - messageAlreadyHasThisReaction = true - - if listReaction.isSelected { - updatedReaction = nil - removedReaction = value - } - } - } - } - } else if let attribute = attribute as? PendingReactionsMessageAttribute { - if attribute.value != nil { - switch reaction { - case .default: - updatedReaction = nil - removedReaction = attribute.value - case let .reaction(value): - if attribute.value == value { - updatedReaction = nil - removedReaction = value - } - } - } - } + let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) + + if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { + removedReaction = chosenReaction + updatedReactions.remove(at: index) + } else { + updatedReactions.append(chosenReaction) + messageAlreadyHasThisReaction = currentReactions.contains(where: { $0.value == chosenReaction }) } - if let updatedReaction = updatedReaction { - guard let allowedReactions = allowedReactions else { + guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() + return + } + + switch allowedReactions { + case let .set(set): + if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { itemNode.openMessageContextMenu() return } - switch allowedReactions { - case let .set(set): - if !messageAlreadyHasThisReaction && !set.contains(updatedReaction.reaction) { - itemNode.openMessageContextMenu() - return - } - case .all: - break - } - + case .all: + break + } + + if removedReaction == nil && !updatedReactions.isEmpty { if strongSelf.selectPollOptionFeedback == nil { strongSelf.selectPollOptionFeedback = HapticFeedback() } strongSelf.selectPollOptionFeedback?.tap() - itemNode.awaitingAppliedReaction = (updatedReaction.reaction, { [weak itemNode] in + itemNode.awaitingAppliedReaction = (chosenReaction, { [weak itemNode] in guard let strongSelf = self else { return } - if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction.reaction) { + if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: chosenReaction) { for reaction in availableReactions.reactions { guard let centerAnimation = reaction.centerAnimation else { continue @@ -1555,7 +1583,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G continue } - if reaction.value == updatedReaction.reaction { + if reaction.value == chosenReaction { let standaloneReactionAnimation = StandaloneReactionAnimation() strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) @@ -1620,7 +1648,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction, isLarge: false).start() + let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in + switch reaction { + case let .builtin(value): + return .builtin(value) + case let .custom(fileId): + return .custom(fileId: fileId, file: nil) + } + } + + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reactions: mappedUpdatedReactions, isLarge: false).start() } }) }, activateMessagePinch: { [weak self] sourceNode in