Swiftgram/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift
2024-01-23 17:52:21 +01:00

280 lines
17 KiB
Swift

import Foundation
import TelegramPresentationData
import AccountContext
import Postbox
import TelegramCore
import SwiftSignalKit
import ContextUI
import Display
import UIKit
import ReactionListContextMenuContent
import UndoUI
import TooltipUI
import StickerPackPreviewUI
import TextNodeWithEntities
import ChatPresentationInterfaceState
import PromptUI
extension ChatControllerImpl {
func openMessageReactionContextMenu(message: Message, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?, value: MessageReaction.Reaction) {
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
var items: [ContextMenuItem] = []
let tags: [EngineMessage.CustomTag] = [ReactionsMessageAttribute.messageTag(reaction: value)]
if self.presentationInterfaceState.historyFilter?.customTags != tags {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_ReactionContextMenu_FilterByTag, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagFilter"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
guard let self else {
a(.default)
return
}
self.chatDisplayNode.historyNode.frozenMessageForScrollingReset = message.id
self.interfaceInteraction?.updateHistoryFilter { _ in
return ChatPresentationInterfaceState.HistoryFilter(customTags: tags, isActive: true)
}
a(.default)
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Title", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
guard let self else {
a(.default)
return
}
a(.default)
let _ = (self.context.engine.stickers.savedMessageTagData()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] savedMessageTags in
guard let self, let savedMessageTags else {
return
}
let reaction = value
//TODO:localize
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: "Edit Title", value: savedMessageTags.tags.first(where: { $0.reaction == reaction })?.title ?? "", characterLimit: 10, apply: { [weak self] value in
guard let self else {
return
}
if let value {
let _ = self.context.engine.stickers.setSavedMessageTagTitle(reaction: reaction, title: value.isEmpty ? nil : value).start()
}
})
self.interfaceInteraction?.presentController(promptController, nil)
})
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_ReactionContextMenu_RemoveTag, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagRemove"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, a in
a(.dismissWithoutContent)
guard let self else {
return
}
self.controllerInteraction?.updateMessageReaction(message, .reaction(value))
})))
self.canReadHistory.set(false)
let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
controller.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
self.window?.presentInGlobalOverlay(controller)
} else {
var customFileIds: [Int64] = []
if case let .custom(fileId) = value {
customFileIds.append(fileId)
}
let _ = (combineLatest(
self.context.engine.stickers.availableReactions(),
self.context.engine.stickers.resolveInlineStickers(fileIds: customFileIds)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] availableReactions, customEmoji in
guard let self else {
return
}
var dismissController: ((@escaping () -> Void) -> Void)?
var items = ContextController.Items(content: .custom(ReactionListContextMenuContent(
context: self.context,
displayReadTimestamps: false,
availableReactions: availableReactions,
animationCache: self.controllerInteraction!.presentationContext.animationCache,
animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer,
message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in
dismissController?({ [weak self] in
guard let self else {
return
}
self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil)
})
})))
var packReferences: [StickerPackReference] = []
var existingIds = Set<Int64>()
for (_, file) in customEmoji {
loop: for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference {
if case let .id(id, _) = packReference, !existingIds.contains(id) {
packReferences.append(packReference)
existingIds.insert(id)
}
break loop
}
}
}
self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let context = self.context
let presentationData = self.presentationData
let action = { [weak self] in
guard let packReference = packReferences.first, let self else {
return
}
self.chatDisplayNode.dismissTextInput()
let presentationData = self.presentationData
let controller = StickerPackScreen(context: context, updatedPresentationData: self.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(packReferences), parentNavigationController: self.effectiveNavigationController, actionPerformed: { [weak self] actions in
guard let self else {
return
}
if actions.count > 1, let first = actions.first {
if case .add = first.2 {
self.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in
return true
}))
} else if actions.allSatisfy({
if case .remove = $0.2 {
return true
} else {
return false
}
}) {
let isEmoji = actions[0].0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks
self.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_MultipleRemovedText(Int32(actions.count)) : presentationData.strings.StickerPackActionInfo_MultipleRemovedText(Int32(actions.count)), undo: true, info: actions[0].0, topItem: actions[0].1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in
if case .undo = action {
var itemsAndIndices: [(StickerPackCollectionInfo, [StickerPackItem], Int)] = actions.compactMap { action -> (StickerPackCollectionInfo, [StickerPackItem], Int)? in
if case let .remove(index) = action.2 {
return (action.0, action.1, index)
} else {
return nil
}
}
itemsAndIndices.sort(by: { $0.2 < $1.2 })
for (info, items, index) in itemsAndIndices.reversed() {
let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: index).startStandalone()
}
}
return true
}))
}
} else if let (info, items, action) = actions.first {
let isEmoji = info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks
switch action {
case .add:
self.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedTitle : presentationData.strings.StickerPackActionInfo_AddedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string : presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in
return true
}))
case let .remove(positionInList):
self.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in
if case .undo = action {
let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).startStandalone()
}
return true
}))
}
}
})
self.present(controller, in: .window(.root))
}
let presentationContext = self.controllerInteraction?.presentationContext
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
if !packReferences.isEmpty && !premiumConfiguration.isPremiumDisabled {
items.tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil)
if packReferences.count > 1 {
items.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action)
} else if let reference = packReferences.first {
var tipSignal: Signal<LoadedStickerPack, NoError>
tipSignal = context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false)
items.tipSignal = tipSignal
|> filter { result in
if case .result = result {
return true
} else {
return false
}
}
|> mapToSignal { result -> Signal<ContextController.Tip?, NoError> in
if case let .result(info, items, _) = result, let presentationContext = presentationContext {
let tip: ContextController.Tip = .animatedEmoji(
text: presentationData.strings.ChatContextMenu_ReactionEmojiSetSingle(info.title).string,
arguments: TextNodeWithEntities.Arguments(
context: context,
cache: presentationContext.animationCache,
renderer: presentationContext.animationRenderer,
placeholderColor: .clear,
attemptSynchronous: true
),
file: items.first?.file,
action: action)
return .single(tip)
} else {
return .complete()
}
}
}
}
self.canReadHistory.set(false)
let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)
controller.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
dismissController = { [weak controller] completion in
controller?.dismiss(completion: {
completion()
})
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
self.window?.presentInGlobalOverlay(controller)
})
}
}
}