Files
TelegramSwift/Telegram-Mac/ChatMessageMenuItems.swift
Mikhail Filimonov a4d14dc3d7 - bugfixes
2024-06-05 18:08:18 +04:00

1172 lines
66 KiB
Swift

//
// ChatMessageMenuItems.swift
// Telegram
//
// Created by Mikhail Filimonov on 08.12.2021.
// Copyright © 2021 Telegram. All rights reserved.
//
import Foundation
import Cocoa
import TGUIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import ObjcUtils
import Translate
import InAppSettings
import InputView
final class ChatMenuItemsData {
let chatInteraction: ChatInteraction
let message: Message
let accountPeer: Peer
let resourceData: MediaResourceData?
let chatState: ChatState
let chatMode: ChatMode
let isLogInteraction: Bool
let disableSelectAbility: Bool
let canPinMessage: Bool
let pinnedMessage: ChatPinnedMessage?
let peer: Peer?
let peerId: PeerId
let fileFinderPath: String?
let isStickerSaved: Bool?
let savedStickersCount: Int
let savedGifsCount: Int
let dialogs: [Peer]
let recentUsedPeers: [Peer]
let favoritePeers: [Peer]
let updatingMessageMedia: [MessageId: ChatUpdatingMessageMedia]
let recentMedia: [RecentMediaItem]
let additionalData: MessageEntryAdditionalData
let availableReactions: AvailableReactions?
let file: TelegramMediaFile?
let image: TelegramMediaImage?
let textLayout: (TextViewLayout?, LinkType?)?
let notifications: NotificationSoundList?
let cachedData: CachedPeerData?
let groupped:[Message]?
let folders: [(ChatListFilter, [Peer])]
let isMediaStory: Bool
let isRead: Bool
init(chatInteraction: ChatInteraction, message: Message, accountPeer: Peer, resourceData: MediaResourceData?, chatState: ChatState, chatMode: ChatMode, disableSelectAbility: Bool, isLogInteraction: Bool, canPinMessage: Bool, pinnedMessage: ChatPinnedMessage?, peer: Peer?, peerId: PeerId, fileFinderPath: String?, isStickerSaved: Bool?, dialogs: [Peer], recentUsedPeers: [Peer], favoritePeers: [Peer], recentMedia: [RecentMediaItem], updatingMessageMedia: [MessageId: ChatUpdatingMessageMedia], additionalData: MessageEntryAdditionalData, file: TelegramMediaFile?, image: TelegramMediaImage?, textLayout: (TextViewLayout?, LinkType?)?, availableReactions: AvailableReactions?, notifications: NotificationSoundList?, cachedData: CachedPeerData?, savedStickersCount: Int, savedGifsCount: Int, groupped: [Message]?, folders: [(ChatListFilter, [Peer])], isMediaStory: Bool, isRead: Bool) {
self.chatInteraction = chatInteraction
self.message = message
self.accountPeer = accountPeer
self.resourceData = resourceData
self.chatState = chatState
self.chatMode = chatMode
self.disableSelectAbility = disableSelectAbility
self.isLogInteraction = isLogInteraction
self.canPinMessage = canPinMessage
self.pinnedMessage = pinnedMessage
self.peer = peer
self.peerId = peerId
self.fileFinderPath = fileFinderPath
self.isStickerSaved = isStickerSaved
self.dialogs = dialogs
self.recentUsedPeers = recentUsedPeers
self.favoritePeers = favoritePeers
self.recentMedia = recentMedia
self.updatingMessageMedia = updatingMessageMedia
self.additionalData = additionalData
self.file = file
self.image = image
self.textLayout = textLayout
self.availableReactions = availableReactions
self.notifications = notifications
self.cachedData = cachedData
self.savedStickersCount = savedStickersCount
self.savedGifsCount = savedGifsCount
self.groupped = groupped
self.folders = folders
self.isMediaStory = isMediaStory
self.isRead = isRead
}
}
func chatMenuItemsData(for message: Message, textLayout: (TextViewLayout?, LinkType?)?, entry: ChatHistoryEntry?, chatInteraction: ChatInteraction) -> Signal<ChatMenuItemsData, NoError> {
let context = chatInteraction.context
let account = context.account
let chatMode = chatInteraction.presentation.chatMode
let chatState = chatInteraction.presentation.state
let disableSelectAbility = chatInteraction.disableSelectAbility
let isLogInteraction = chatInteraction.isLogInteraction
let pinnedMessage = chatInteraction.presentation.pinnedMessageId
let peerId = chatInteraction.peerId
let peer = chatInteraction.peer
let canPinMessage = chatInteraction.presentation.canPinMessage && peerId.namespace != Namespaces.Peer.SecretChat
let additionalData = entry?.additionalData ?? MessageEntryAdditionalData()
let storyMedia = message.media.first as? TelegramMediaStory
let isMediaStory = storyMedia?.storyId.peerId == context.peerId ? false : storyMedia != nil
let incoming: Bool = message.isIncoming(context.account, false)
let isRead: Bool
if case let .MessageEntry(_, _, _isRead, _, _, _, _) = entry {
isRead = _isRead || incoming
} else if case let .groupedPhotos(entries, _) = entry, case let .MessageEntry(_, _, _isRead, _, _, _, _) = entries.first {
isRead = _isRead || incoming
} else {
isRead = incoming
}
var file: TelegramMediaFile? = nil
var image: TelegramMediaImage? = nil
if let media = message.anyMedia as? TelegramMediaFile {
file = media
} else if let media = message.anyMedia as? TelegramMediaImage {
image = media
} else if let media = message.anyMedia as? TelegramMediaWebpage {
switch media.content {
case let .Loaded(content):
file = content.file
if file == nil {
image = content.image
}
default:
break
}
}
let request:(ChatListFilterPredicate?)->Signal<[Peer], NoError> = { predicate in
return account.postbox.tailChatListView(groupId: .root, filterPredicate: predicate, count: 25, summaryComponents: .init())
|> take(1) |> map { view in
return view.0.entries.compactMap { entry in
switch entry {
case let .MessageEntry(data):
return data.renderedPeer.peer
default:
return nil
}
}
}
}
let _dialogs: Signal<[Peer], NoError> = request(nil)
let _folders: Signal<[(ChatListFilter, [Peer])], NoError> = chatListFilterPreferences(engine: context.engine) |> mapToSignal { value in
return combineLatest(value.list.filter { !$0.isAllChats }.map { item in
return request(chatListFilterPredicate(for: item)) |> map {
(item, $0)
}
})
}
let _recentUsedPeers: Signal<[Peer], NoError> = context.recentlyUserPeerIds |> mapToSignal { ids in
return account.postbox.transaction { transaction in
let peers = ids.compactMap { transaction.getPeer($0) }
return Array(peers.map { $0 })
}
}
let _favoritePeers: Signal<[Peer], NoError> = context.engine.peers.recentPeers() |> map { recent in
switch recent {
case .disabled:
return []
case let .peers(peers):
return Array(peers.map { $0 })
}
}
let _accountPeer = account.postbox.loadedPeerWithId(context.peerId) |> deliverOnMainQueue
var _resourceData: Signal<MediaResourceData?, NoError> = .single(nil)
var _fileFinderPath: Signal<String?, NoError> = .single(nil)
var _getIsStickerSaved: Signal<Bool?, NoError> = .single(nil)
var _recentMedia: Signal<[RecentMediaItem], NoError> = .single([])
let _savedStickersCount: Signal<Int, NoError> = account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 100) |> take(1) |> map {
$0.orderedItemListsViews[0].items.count
}
let _savedGifsCount: Signal<Int, NoError> = context.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) |> take(1) |> map {
return ($0.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] as! OrderedItemListView).items.count
}
let _updatingMessageMedia = account.pendingUpdateMessageManager.updatingMessageMedia
if let media = file {
_resourceData = account.postbox.mediaBox.resourceData(media.resource) |> map(Optional.init)
if media.isSticker {
_getIsStickerSaved = account.postbox.transaction { transaction -> Bool? in
return getIsStickerSaved(transaction: transaction, fileId: media.fileId)
}
}
if media.isAnimated && media.isVideo {
_recentMedia = account.postbox.transaction { transaction -> [RecentMediaItem] in
transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs).compactMap { $0.contents.get(RecentMediaItem.self) }
}
}
_fileFinderPath = fileFinderPath(media, account.postbox)
} else if let media = image {
if let resource = media.representations.last?.resource {
_resourceData = account.postbox.mediaBox.resourceData(resource) |> map(Optional.init)
}
}
let _groupped: Signal<[Message]?, NoError> = context.account.postbox.transaction { transaction in
return transaction.getMessageGroup(message.id)
}
let cachedData = getCachedDataView(peerId: peerId, postbox: context.account.postbox) |> take(1)
let combined = combineLatest(queue: .mainQueue(), _dialogs, _recentUsedPeers, _favoritePeers, _accountPeer, _resourceData, _fileFinderPath, _getIsStickerSaved, _recentMedia, _updatingMessageMedia, context.reactions.stateValue, context.engine.peers.notificationSoundList(), cachedData, _savedStickersCount, _savedGifsCount, _groupped, _folders)
|> take(1)
return combined |> map { dialogs, recentUsedPeers, favoritePeers, accountPeer, resourceData, fileFinderPath, isStickerSaved, recentMedia, updatingMessageMedia, availableReactions, notifications, cachedData, savedStickersCount, savedGifsCount, groupped, folders in
return .init(chatInteraction: chatInteraction, message: message, accountPeer: accountPeer, resourceData: resourceData, chatState: chatState, chatMode: chatMode, disableSelectAbility: disableSelectAbility, isLogInteraction: isLogInteraction, canPinMessage: canPinMessage, pinnedMessage: pinnedMessage, peer: peer, peerId: peerId, fileFinderPath: fileFinderPath, isStickerSaved: isStickerSaved, dialogs: dialogs, recentUsedPeers: recentUsedPeers, favoritePeers: favoritePeers, recentMedia: recentMedia, updatingMessageMedia: updatingMessageMedia, additionalData: additionalData, file: file, image: image, textLayout: textLayout, availableReactions: availableReactions, notifications: notifications, cachedData: cachedData, savedStickersCount: savedStickersCount, savedGifsCount: savedGifsCount, groupped: groupped, folders: folders, isMediaStory: isMediaStory, isRead: isRead)
}
}
func chatMenuItems(for message: Message, entry: ChatHistoryEntry?, textLayout: (TextViewLayout?, LinkType?)?, chatInteraction: ChatInteraction, useGroupIfNeeded: Bool = true) -> Signal<[ContextMenuItem], NoError> {
return chatMenuItemsData(for: message, textLayout: textLayout, entry: entry, chatInteraction: chatInteraction) |> map { data in
let peer = data.message.peers[data.message.id.peerId]
let isNotFailed = !message.flags.contains(.Failed) && !message.flags.contains(.Unsent) && !data.message.flags.contains(.Sending)
let protected = data.message.containsSecretMedia || data.message.isCopyProtected()
let peerId = data.peerId
let messageId = data.message.id
let appConfiguration = data.chatInteraction.context.appConfiguration
let context = data.chatInteraction.context
let account = context.account
let mode = chatInteraction.mode
var isService = data.message.extendedMedia is TelegramMediaAction || mode.isSavedMode || mode == .preview || chatInteraction.isLogInteraction
if !isService, let story = data.message.media.first as? TelegramMediaStory {
isService = story.isMention
}
var items:[ContextMenuItem] = []
var zeroBlock:[ContextMenuItem] = []
var firstBlock:[ContextMenuItem] = []
var secondBlock:[ContextMenuItem] = []
var add_secondBlock:[ContextMenuItem] = []
var thirdBlock:[ContextMenuItem] = []
var fourthBlock:[ContextMenuItem] = []
var fifthBlock:[ContextMenuItem] = []
var sixBlock:[ContextMenuItem] = []
if let layout = textLayout?.0, !layout.selectedRange.range.isEmpty, mode != .pinned, mode != .scheduled, !mode.isSavedMode, mode.customChatContents == nil {
firstBlock.append(ContextMenuItem(strings().chatMessageContextQuote, handler: {
let quote_length_max = context.appConfiguration.getGeneralValue("quote_length_max", orElse: 1024)
if layout.selectedString.length > quote_length_max {
alert(for: context.window, info: strings().chatMessageContextQuoteLimitExceed)
} else {
let attributed = enititesAttributedStringForText(layout.selectedString).trimmed
let entities = messageTextEntitiesInRange(entities: ChatTextInputState(attributedText: attributed, selectionRange: 0..<0).messageTextEntities(), range: attributed.range, onlyQuoteable: true)
let quote = EngineMessageReplyQuote(text: attributed.string, offset: nil, entities: entities, media: message.media.first)
chatInteraction.setupReplyMessage(message, .init(messageId: message.id, quote: quote))
}
}, itemImage: MenuAnimation.menu_quote.value))
}
if let adAttribute = data.message.adAttribute {
if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil {
let submenu = ContextMenu()
let subItem = ContextMenuItem(strings().chatMessageSponsoredAdvertiser, itemImage: MenuAnimation.menu_channel.value)
if let text = adAttribute.sponsorInfo {
submenu.addItem(ContextMenuItem(text, handler: {
copyToClipboard(text)
showModalText(for: context.window, text: strings().contextAlertCopied)
}, removeTail: false))
}
if let text = adAttribute.additionalInfo {
if !submenu.items.isEmpty {
submenu.addItem(ContextSeparatorItem())
}
submenu.addItem(ContextMenuItem(text, handler: {
copyToClipboard(text)
showModalText(for: context.window, text: strings().contextAlertCopied)
}, removeTail: false))
}
subItem.submenu = submenu
items.append(subItem)
items.append(ContextSeparatorItem())
}
if adAttribute.canReport {
items.append(ContextMenuItem(strings().chatMessageSponsoredAbout, handler: {
showModal(with: FragmentAdsInfoController(context: context), for: context.window)
}, itemImage: MenuAnimation.menu_show_info.value))
items.append(ContextSeparatorItem())
items.append(ContextMenuItem(strings().chatMessageSponsoredReport, handler: {
_ = showModalProgress(signal: context.engine.messages.reportAdMessage(peerId: data.peerId, opaqueId: adAttribute.opaqueId, option: nil), for: context.window).startStandalone(next: { result in
switch result {
case .reported:
showModalText(for: context.window, text: strings().chatMessageSponsoredReportAready)
case .adsHidden:
break
case let .options(title, options):
showComplicatedReport(context: context, title: title, info: strings().chatMessageSponsoredReportLearnMore, data: .init(list: options.map { .init(string: $0.text, id: $0.option) }, title: strings().chatMessageSponsoredReportOptionTitle), report: { report in
return context.engine.messages.reportAdMessage(peerId: data.peerId, opaqueId: adAttribute.opaqueId, option: report.id) |> `catch` { error in
return .single(.reported)
} |> deliverOnMainQueue |> map { result in
switch result {
case let .options(_, options):
return .init(list: options.map { .init(string: $0.text, id: $0.option) }, title: report.string)
case .reported:
showModalText(for: context.window, text: strings().chatMessageSponsoredReportSuccess)
chatInteraction.removeAd(adAttribute.opaqueId)
return nil
case .adsHidden:
return nil
}
}
})
}
}, error: { error in
switch error {
case .premiumRequired:
showModal(with: PremiumBoardingController(context: context, source: .no_ads, openFeatures: true), for: context.window)
case .generic:
break
}
})
}, itemImage: MenuAnimation.menu_restrict.value))
} else {
items.append(ContextMenuItem(strings().chatMessageSponsoredWhat, handler: {
let link = strings().chatMessageAdUrl
verifyAlert_button(for: context.window, information: strings().chatMessageAdText(link), cancel: "", option: strings().chatMessageAdReadMore, successHandler: { result in
switch result {
case .thrid:
let link = inAppLink.external(link: link, false)
execute(inapp: link)
default:
break
}
})
}, itemImage: MenuAnimation.menu_report.value))
}
if !context.premiumIsBlocked {
items.append(ContextMenuItem(strings().chatContextHideAd, handler: {
showModal(with: PremiumBoardingController(context: context, source: .no_ads, openFeatures: true), for: context.window)
}, itemImage: MenuAnimation.menu_clear_history.value))
}
return items
}
if data.disableSelectAbility, data.chatState == .selecting {
return []
}
if messageId.peerId == repliesPeerId, let author = data.message.chatPeer(context.peerId), author.id != context.peerId, !isService {
let text = author.isUser ? strings().chatContextBlockUser : strings().chatContextBlockGroup
firstBlock.append(ContextMenuItem(text, handler: {
let header = author.isUser ? strings().chatContextBlockUserHeader : strings().chatContextBlockGroupHeader
let info = author.isUser ? strings().chatContextBlockUserInfo(author.displayTitle) : strings().chatContextBlockGroupInfo(author.displayTitle)
let third = author.isUser ? strings().chatContextBlockUserThird : strings().chatContextBlockGroupThird
let ok = author.isUser ? strings().chatContextBlockUserOK : strings().chatContextBlockGroupOK
let cancel = author.isUser ? strings().chatContextBlockUserCancel : strings().chatContextBlockGroupCancel
verifyAlert(for: context.window, header: header, information: info, ok: ok, cancel: cancel, option: third, optionIsSelected: true, successHandler: { result in
switch result {
case .thrid:
let block: Signal<Never, NoError> = context.blockedPeersContext.add(peerId: author.id) |> `catch` { _ in return .complete() }
_ = showModalProgress(signal: combineLatest(context.engine.peers.reportPeerMessages(messageIds: [messageId], reason: .spam, message: ""), block), for: context.window).start()
case .basic:
_ = showModalProgress(signal: context.blockedPeersContext.add(peerId: author.id), for: context.window).start()
}
})
}, itemImage: MenuAnimation.menu_restrict.value))
}
if data.message.isScheduledMessage, let peer = data.peer, !isService {
firstBlock.append(ContextMenuItem(strings().chatContextScheduledSendNow, handler: {
_ = context.engine.messages.sendScheduledMessageNowInteractively(messageId: messageId).start()
}, itemImage: MenuAnimation.menu_send_now.value))
firstBlock.append(ContextMenuItem(strings().chatContextScheduledReschedule, handler: {
showModal(with: DateSelectorModalController(context: context, defaultDate: Date(timeIntervalSince1970: TimeInterval(message.timestamp)), mode: .schedule(peer.id), selectedAt: { date in
_ = showModalProgress(signal: context.engine.messages.requestEditMessage(messageId: messageId, text: data.message.text, media: .keep, entities: data.message.textEntities, inlineStickers: data.message.associatedMedia, scheduleTime: Int32(min(date.timeIntervalSince1970, Double(scheduleWhenOnlineTimestamp)))), for: context.window).start()
}), for: context.window)
}, itemImage: MenuAnimation.menu_schedule_message.value))
}
if canReplyMessage(data.message, peerId: data.peerId, mode: data.chatMode, threadData: chatInteraction.presentation.threadInfo) && !data.isLogInteraction {
firstBlock.append(ContextMenuItem(strings().messageContextReply1, handler: {
data.chatInteraction.setupReplyMessage(data.message, .init(messageId: data.message.id, quote: nil))
}, itemImage: MenuAnimation.menu_reply.value, keyEquivalent: .cmdr))
}
if let poll = data.message.anyMedia as? TelegramMediaPoll {
if !poll.isClosed && isNotFailed {
if let _ = poll.results.voters?.first(where: {$0.selected}), poll.kind != .quiz {
let isLoading = data.additionalData.pollStateData.isLoading
add_secondBlock.append(ContextMenuItem(strings().chatPollUnvote, handler: {
if !isLoading, isNotFailed {
data.chatInteraction.vote(messageId, [], true)
}
}, itemImage: MenuAnimation.menu_retract_vote.value))
}
if data.message.forwardInfo == nil {
let canClose: Bool = canEditMessage(data.message, chatInteraction: data.chatInteraction, context: context, ignorePoll: true)
if canClose {
add_secondBlock.append(ContextMenuItem(poll.kind == .quiz ? strings().chatQuizStop : strings().chatPollStop, handler: { [weak chatInteraction] in
verifyAlert_button(for: context.window, header: poll.kind == .quiz ? strings().chatQuizStopConfirmHeader : strings().chatPollStopConfirmHeader, information: poll.kind == .quiz ? strings().chatQuizStopConfirmText : strings().chatPollStopConfirmText, ok: strings().alertConfirmStop, successHandler: { [weak chatInteraction] _ in
chatInteraction?.closePoll(messageId)
})
}, itemImage: MenuAnimation.menu_stop_poll.value))
}
}
}
}
if data.chatMode.threadId == nil, let peer = peer, peer.isSupergroup {
if let attr = data.message.threadAttr, attr.count > 0, mode != .scheduled {
var messageId: MessageId = message.id
var modeIsReplies = true
if let source = message.sourceReference {
messageId = source.messageId
if let peer = message.peers[source.messageId.peerId] {
if peer.isChannel {
modeIsReplies = false
}
}
}
let text = modeIsReplies ? strings().messageContextViewRepliesCountable(Int(attr.count)) : strings().messageContextViewCommentsCountable(Int(attr.count))
secondBlock.append(ContextMenuItem(text, handler: {
data.chatInteraction.openReplyThread(messageId, !modeIsReplies, true, modeIsReplies ? .replies(origin: messageId) : .comments(origin: messageId))
}, itemImage: MenuAnimation.menu_view_replies.value))
}
}
var muteTranslate = false
if let translate = entry?.additionalData.translate {
switch translate {
case .loading:
break
case let .complete(toLang):
if let _ = message.translationAttribute(toLang: toLang) {
muteTranslate = true
}
}
}
if let poll = message.media.first as? TelegramMediaPoll, mode.customChatContents == nil {
var text = poll.text
var entities: [MessageTextEntity] = []
entities.append(contentsOf: poll.textEntities)
text += "\n"
for option in poll.options {
text += "\n🔘 \(option.text)"
for entity in option.entities {
var current = entity
current.range = entity.range.lowerBound + text.length - option.text.length ..< entity.range.upperBound + text.length - option.text.length
entities.append(current)
}
}
let language = Translate.detectLanguage(for: text)
let toLang = context.sharedContext.baseSettings.doNotTranslate.union([appAppearance.languageCode])
if language == nil || !toLang.contains(language!), !muteTranslate, !isService {
thirdBlock.append(ContextMenuItem(strings().chatContextTranslate, handler: {
showModal(with: TranslateModalController(context: context, from: language, toLang: appAppearance.languageCode, text: text, entities: entities, canBreak: false), for: context.window)
data.chatInteraction.enableTranslatePaywall()
}, itemImage: MenuAnimation.menu_translate.value))
}
}
// if !data.message.isCopyProtected() {
if let textLayout = data.textLayout?.0, mode.customChatContents == nil {
if !textLayout.selectedRange.hasSelectText {
let text = message.text
let entities = message.textEntities?.entities ?? []
let language = Translate.detectLanguage(for: text)
let toLang = context.sharedContext.baseSettings.doNotTranslate.union([appAppearance.languageCode])
if language == nil || !toLang.contains(language!), !muteTranslate, !isService {
thirdBlock.append(ContextMenuItem(strings().chatContextTranslate, handler: {
showModal(with: TranslateModalController(context: context, from: language, toLang: appAppearance.languageCode, text: text, entities: entities), for: context.window)
data.chatInteraction.enableTranslatePaywall()
}, itemImage: MenuAnimation.menu_translate.value))
}
if !data.message.isCopyProtected() {
thirdBlock.append(ContextMenuItem(strings().chatContextCopyText, handler: { [weak textLayout] in
if let textLayout = textLayout {
if !globalLinkExecutor.copyAttributedString(textLayout.attributedString) {
copyToClipboard(textLayout.attributedString.string)
}
}
}, itemImage: MenuAnimation.menu_copy.value))
}
} else {
let text: String
if let linkType = data.textLayout?.1, !data.message.isCopyProtected() {
text = copyContextText(from: linkType)
thirdBlock.append(ContextMenuItem(text, handler: { [weak textLayout] in
if let textLayout = textLayout {
let attr = textLayout.attributedString.mutableCopy() as! NSMutableAttributedString
var effectiveRange = textLayout.selectedRange.range
let selectedText = attr.attributedSubstring(from: textLayout.selectedRange.range)
let pb = NSPasteboard.general
pb.clearContents()
pb.declareTypes([.string], owner: textLayout)
let attribute = selectedText.attribute(TextInputAttributes.textUrl, at: 0, effectiveRange: &effectiveRange)
if let attribute = attribute as? TextInputTextUrlAttribute {
pb.setString(attribute.url.isEmpty ? selectedText.string : attribute.url, forType: .string)
} else {
pb.setString(selectedText.string, forType: .string)
}
}
}, itemImage: MenuAnimation.menu_copy.value))
} else if !data.message.isCopyProtected() {
let attr = textLayout.attributedString.mutableCopy() as! NSMutableAttributedString
attr.enumerateAttributes(in: attr.range, options: [], using: { data, range, _ in
if let value = data[TextInputAttributes.embedded] as? InlineStickerItem {
switch value.source {
case let .attribute(value):
attr.replaceCharacters(in: range, with: value.emoji)
default:
break
}
}
})
if let range = attr.range.intersection(textLayout.selectedRange.range) {
let selectedText = attr.attributedSubstring(from: range)
let state = ChatTextInputState(attributedText: selectedText, selectionRange: 0..<0)
let entities = state.messageTextEntities()
let text = state.inputText
let language = Translate.detectLanguage(for: text)
let toLang = context.sharedContext.baseSettings.doNotTranslate.union([appAppearance.languageCode])
if language == nil || !toLang.contains(language!), !muteTranslate, !isService {
thirdBlock.append(ContextMenuItem(strings().chatContextTranslate, handler: {
showModal(with: TranslateModalController(context: context, from: language, toLang: appAppearance.languageCode, text: text, entities: entities), for: context.window)
data.chatInteraction.enableTranslatePaywall()
}, itemImage: MenuAnimation.menu_translate.value))
}
thirdBlock.append(ContextMenuItem(strings().chatCopySelectedText, handler: { [weak textLayout] in
if let textLayout = textLayout {
let attr = textLayout.attributedString
let pb = NSPasteboard.general
pb.clearContents()
pb.declareTypes([.string], owner: textLayout)
var effectiveRange = textLayout.selectedRange.range
let selectedText = attr.attributedSubstring(from: textLayout.selectedRange.range)
let isCopied = globalLinkExecutor.copyAttributedString(selectedText)
if !isCopied {
let attribute = attr.attribute(NSAttributedString.Key.link, at: textLayout.selectedRange.range.location, effectiveRange: &effectiveRange)
if let attribute = attribute as? inAppLink {
pb.setString(attribute.link.isEmpty ? selectedText.string : attribute.link, forType: .string)
} else {
pb.setString(selectedText.string, forType: .string)
}
}
}
}, itemImage: MenuAnimation.menu_copy.value))
}
}
}
}
if let state = data.message.audioTranscription {
if !state.text.isEmpty && !state.isPending {
let text = state.text
let language = Translate.detectLanguage(for: text)
let toLang = context.sharedContext.baseSettings.doNotTranslate.union([appAppearance.languageCode])
if language == nil || !toLang.contains(language!), !isService {
thirdBlock.append(ContextMenuItem(strings().chatContextTranslate, handler: {
showModal(with: TranslateModalController(context: context, from: language, toLang: appAppearance.languageCode, text: text), for: context.window)
data.chatInteraction.enableTranslatePaywall()
}, itemImage: MenuAnimation.menu_translate.value))
}
}
}
if let peer = peer as? TelegramChannel, !isService {
if isNotFailed, !message.isScheduledMessage {
thirdBlock.append(ContextMenuItem(strings().messageContextCopyMessageLink1, handler: {
_ = showModalProgress(signal: context.engine.messages.exportMessageLink(peerId: peer.id, messageId: messageId, isThread: data.chatMode.threadId != nil), for: context.window).start(next: { link in
if let link = link {
copyToClipboard(link)
}
showSuccess(window: context.window)
})
}, itemImage: MenuAnimation.menu_copy_link.value))
}
}
if canEditMessage(data.message, chatInteraction: data.chatInteraction, context: context), !isService {
let edit = ContextMenuItem(strings().messageContextEdit, handler: {
data.chatInteraction.beginEditingMessage(data.message)
}, itemImage: MenuAnimation.menu_edit.value, keyEquivalent: .cmde)
if let groupped = data.groupped, groupped.count > 1, let media = message.media.first as? TelegramMediaFile {
if !media.isVideo {
let menu = ContextMenu()
for message in groupped {
if let fileName = message.file?.fileName {
menu.addItem(ContextMenuItem(fileName, handler: {
data.chatInteraction.beginEditingMessage(message)
}))
}
}
edit.submenu = menu
}
}
secondBlock.append(edit)
}
if !data.message.isScheduledMessage, let peer = peer, !peer.isDeleted, isNotFailed, data.peerId == data.message.id.peerId, !isService, mode.customChatContents == nil {
let needUnpin = data.pinnedMessage?.others.contains(data.message.id) == true
let pinAndOld: Bool
if let pinnedMessage = data.pinnedMessage, let last = pinnedMessage.others.last {
pinAndOld = last > data.message.id
} else {
pinAndOld = false
}
let pinText = data.message.tags.contains(.pinned) ? strings().messageContextUnpin : strings().messageContextPin
let pinImage = data.message.tags.contains(.pinned) ? MenuAnimation.menu_unpin.value : MenuAnimation.menu_pin.value
let canSendMessage = peer.canSendMessage(data.chatMode.isThreadMode || data.chatMode.isTopicMode, media: data.message.media.first, threadData: data.chatInteraction.presentation.threadInfo)
if let peer = peer as? TelegramChannel, peer.hasPermission(.pinMessages) || (peer.isChannel && peer.hasPermission(.editAllMessages)), canSendMessage {
if isNotFailed {
if !data.chatMode.isThreadMode, (needUnpin || data.chatMode != .pinned) {
secondBlock.append(ContextMenuItem(pinText, handler: {
if peer.isSupergroup, !needUnpin {
let info = pinAndOld ? strings().chatConfirmPinOld : strings().messageContextConfirmPin1
verifyAlert(for: context.window, information: info, ok: strings().messageContextPin, option: pinAndOld ? nil : strings().messageContextConfirmNotifyPin, successHandler: { result in
data.chatInteraction.updatePinned(data.message.id, needUnpin, result != .thrid, false)
})
} else {
data.chatInteraction.updatePinned(data.message.id, needUnpin, true, false)
}
}, itemImage: pinImage))
}
}
} else if data.message.id.peerId == context.peerId {
secondBlock.append(ContextMenuItem(pinText, handler: {
data.chatInteraction.updatePinned(data.message.id, needUnpin, true, false)
}, itemImage: pinImage))
} else if let peer = peer as? TelegramGroup, peer.canPinMessage, (needUnpin || data.chatMode != .pinned) {
secondBlock.append(ContextMenuItem(pinText, handler: {
if !needUnpin {
verifyAlert(for: context.window, information: pinAndOld ? strings().chatConfirmPinOld : strings().messageContextConfirmPin1, ok: strings().messageContextPin, option: pinAndOld ? nil : strings().messageContextConfirmNotifyPin, successHandler: { result in
data.chatInteraction.updatePinned(data.message.id, needUnpin, result == .thrid, false)
})
} else {
data.chatInteraction.updatePinned(data.message.id, needUnpin, false, false)
}
}, itemImage: pinImage))
} else if data.canPinMessage, let peer = data.peer, (needUnpin || data.chatMode != .pinned) {
secondBlock.append(ContextMenuItem(pinText, handler: {
if !needUnpin {
verifyAlert(for: context.window, information: pinAndOld ? strings().chatConfirmPinOld : strings().messageContextConfirmPin1, ok: strings().messageContextPin, option: strings().chatConfirmPinFor(peer.displayTitle), optionIsSelected: false, successHandler: { result in
data.chatInteraction.updatePinned(data.message.id, needUnpin, false, result != .thrid)
})
} else {
data.chatInteraction.updatePinned(data.message.id, needUnpin, false, false)
}
}, itemImage: pinImage))
}
}
if canForwardMessage(data.message, chatInteraction: data.chatInteraction), !isService {
let msgs = useGroupIfNeeded ? (data.groupped ?? [data.message]) : [data.message]
let forwardItem = ContextMenuItem(strings().messageContextForward, handler: {
data.chatInteraction.forwardMessages(msgs)
}, itemImage: MenuAnimation.menu_forward.value)
let forwardMenu = ContextMenu()
let forwardObject = ForwardMessagesObject(context, messages: [data.message], album: useGroupIfNeeded, getMessages: chatInteraction.getMessages)
let recent = data.recentUsedPeers.filter {
$0.id != context.peerId && $0.canSendMessage(media: message.media.first) && !$0.isDeleted
}.prefix(5)
let favorite = data.favoritePeers.filter {
!recent.map { $0.id }.contains($0.id)
&& $0.id != context.peerId
&& $0.canSendMessage(media: message.media.first)
&& !$0.isDeleted
}.prefix(5)
let dialogs = data.dialogs.reversed().filter {
!(recent + favorite).map { $0.id }.contains($0.id)
&& $0.id != context.peerId
&& $0.canSendMessage(media: message.media.first)
&& !$0.isDeleted
}.prefix(5)
var items:[ContextMenuItem] = []
func makeItem(_ peer: Peer) -> ContextMenuItem {
let title = peer.id == context.peerId ? strings().peerSavedMessages : peer.displayTitle.prefixWithDots(20)
let item = ReactionPeerMenu(title: title, handler: {
_ = forwardObject.perform(to: [peer.id], threadId: nil).start()
}, peer: peer, context: context, reaction: nil, message: message, destination: .forward(callback: { threadId in
_ = forwardObject.perform(to: [peer.id], threadId: makeThreadIdMessageId(peerId: peer.id, threadId: threadId)).start()
}))
return item
}
items.append(makeItem(data.accountPeer))
if !recent.isEmpty || !dialogs.isEmpty || !favorite.isEmpty {
items.append(ContextSeparatorItem())
}
for peer in recent {
if peer.id.namespace != Namespaces.Peer.SecretChat {
items.append(makeItem(peer))
}
}
if !recent.isEmpty {
items.append(ContextSeparatorItem())
}
for peer in favorite {
items.append(makeItem(peer))
}
if (!favorite.isEmpty || !recent.isEmpty) && !dialogs.isEmpty {
items.append(ContextSeparatorItem())
}
for peer in dialogs {
if peer.id.namespace != Namespaces.Peer.SecretChat {
items.append(makeItem(peer))
}
}
if !items.isEmpty {
if !data.folders.isEmpty {
items.append(ContextSeparatorItem())
let folders = ContextMenuItem(strings().chatContextFolders, itemImage: MenuAnimation.menu_folder.value)
let folderSubmenu = ContextMenu()
for folder in data.folders {
let item = ContextMenuItem(folder.0.title, itemImage: FolderIcon(folder.0).emoticon.drawable.value)
let submenu = ContextMenu()
for peer in folder.1 {
submenu.addItem(makeItem(peer))
}
item.submenu = submenu
folderSubmenu.addItem(item)
}
folders.submenu = folderSubmenu
items.append(folders)
}
let more = ContextMenuItem(strings().chatContextForwardMore, handler: { [unowned chatInteraction] in
chatInteraction.forwardMessages([message])
}, itemImage: MenuAnimation.menu_more.value)
items.append(more)
}
for item in items {
forwardMenu.addItem(item)
}
forwardItem.submenu = forwardMenu
secondBlock.append(forwardItem)
}
/*
else if data.message.id.peerId.namespace == Namespaces.Peer.SecretChat, !data.message.containsSecretMedia {
secondBlock.append(ContextMenuItem(strings().messageContextShare, handler: {
if let data = data {
data.chatInteraction.forwardMessages([data.message.id])
}
}))
}
*/
if data.chatMode.threadId != data.message.id, !isService {
secondBlock.append(ContextMenuItem(strings().messageContextSelect, handler: {
data.chatInteraction.withToggledSelectedMessage({
$0.withToggledSelectedMessage(data.message.id)
})
}, itemImage: MenuAnimation.menu_select_messages.value))
}
if let channel = data.message.peers[message.id.peerId] as? TelegramChannel, channel.isChannel, !isService {
var views: Int = 0
for attribute in message.attributes {
if let attribute = attribute as? ViewCountMessageAttribute {
views = attribute.count
}
}
if let cachedData = data.cachedData as? CachedChannelData, views >= 100 {
if cachedData.flags.contains(.canViewStats) {
thirdBlock.append(ContextMenuItem.init(strings().chatContextViewStatistics, handler: {
context.bindings.rootNavigation().push(MessageStatsController(context, subject: .messageId(messageId)))
}, itemImage: MenuAnimation.menu_statistics.value))
}
}
}
if let resourceData = data.resourceData, !protected {
if let file = data.file {
if file.isVideo && file.isAnimated {
if data.recentMedia.contains(where: {$0.media.id == file.fileId}) {
thirdBlock.append(ContextMenuItem(strings().messageContextRemoveGif, handler: {
showModalText(for: context.window, text: strings().chatContextGifRemoved)
_ = removeSavedGif(postbox: account.postbox, mediaId: file.fileId).start()
}, itemImage: MenuAnimation.menu_remove_gif.value))
} else {
thirdBlock.append(ContextMenuItem(strings().messageContextSaveGif, handler: {
let limit = context.isPremium ? context.premiumLimits.saved_gifs_limit_premium : context.premiumLimits.saved_gifs_limit_default
if limit <= data.savedGifsCount, !context.isPremium, !context.premiumIsBlocked {
showModalText(for: context.window, text: strings().chatContextFavoriteGifsLimitInfo("\(context.premiumLimits.saved_gifs_limit_premium)"), title: strings().chatContextFavoriteGifsLimitTitle, callback: { value in
showPremiumLimit(context: context, type: .savedGifs)
})
} else {
showModalText(for: context.window, text: strings().chatContextGifAdded)
}
_ = addSavedGif(postbox: account.postbox, fileReference: FileMediaReference.message(message: MessageReference(data.message), media: file)).start()
}, itemImage: MenuAnimation.menu_add_gif.value))
}
}
if file.isSticker, let saved = data.isStickerSaved {
if let reference = file.stickerReference {
thirdBlock.append(ContextMenuItem(strings().contextViewStickerSet, handler: {
showModal(with: StickerPackPreviewModalController(context, peerId: peerId, references: [.stickers(reference)]), for: context.window)
}, itemImage: MenuAnimation.menu_view_sticker_set.value))
}
let image = saved ? MenuAnimation.menu_remove_from_favorites.value : MenuAnimation.menu_add_to_favorites.value
thirdBlock.append(ContextMenuItem(!saved ? strings().chatContextAddFavoriteSticker : strings().chatContextRemoveFavoriteSticker, handler: {
if !saved {
let limit = context.isPremium ? context.premiumLimits.stickers_faved_limit_premium : context.premiumLimits.stickers_faved_limit_default
if limit >= data.savedStickersCount, !context.isPremium, !context.premiumIsBlocked {
showModalText(for: context.window, text: strings().chatContextFavoriteStickersLimitInfo("\(context.premiumLimits.stickers_faved_limit_premium)"), title: strings().chatContextFavoriteStickersLimitTitle, callback: { value in
showPremiumLimit(context: context, type: .faveStickers)
})
} else {
showModalText(for: context.window, text: strings().chatContextStickerAddedToFavorites)
}
_ = addSavedSticker(postbox: account.postbox, network: account.network, file: file).start()
} else {
showModalText(for: context.window, text: strings().chatContextStickerRemovedFromFavorites)
_ = removeSavedSticker(postbox: account.postbox, mediaId: file.fileId).start()
}
}, itemImage: image))
}
if resourceData.complete {
if let file = data.file, file.isMusic || file.isVoice, let list = data.notifications {
let settings = NotificationSoundSettings.extract(from: context.appConfiguration)
let size = file.size ?? 0
let contains = list.sounds.contains(where: { $0.file.fileId.id == file.fileId.id })
let duration = file.duration ?? 0
if size < settings.maxSize, Int(duration) < settings.maxDuration, list.sounds.count < settings.maxSavedCount, !contains {
thirdBlock.append(ContextMenuItem(strings().chatContextSaveRingtoneAdd, handler: {
let signal = context.engine.peers.saveNotificationSound(file: .message(message: .init(message), media: file))
_ = showModalProgress(signal: signal, for: context.window).start(error: { error in
alert(for: context.window, info: strings().unknownError)
}, completed: {
showModalText(for: context.window, text: strings().chatContextSaveRingtoneAddSuccess)
})
}, itemImage: MenuAnimation.menu_note_download.value))
} else if contains {
thirdBlock.append(ContextMenuItem(strings().chatContextSaveRingtoneRemove, handler: {
let signal = context.engine.peers.removeNotificationSound(file: .message(message: .init(message), media: file))
_ = showModalProgress(signal: signal, for: context.window).start(error: { error in
alert(for: context.window, info: strings().unknownError)
}, completed: {
showModalText(for: context.window, text: strings().chatContextSaveRingtoneRemoveSuccess)
})
}, itemImage: MenuAnimation.menu_note_slash.value))
}
}
if !data.isMediaStory {
thirdBlock.append(ContextMenuItem(strings().chatContextSaveMedia, handler: {
saveAs(file, account: account)
}, itemImage: MenuAnimation.menu_save_as.value, keyEquivalent: .cmds))
}
if let downloadPath = data.fileFinderPath {
// if !file.isVoice {
// let path: String
// if FileManager.default.fileExists(atPath: downloadPath) {
// path = downloadPath
// } else {
// path = resourceData.path + "." + fileExtenstion(file)
// try? FileManager.default.removeItem(atPath: path)
// try? FileManager.default.linkItem(atPath: resourceData.path, toPath: path)
// }
// let result = ObjcUtils.apps(forFileUrl: path)
// if let result = result, !result.isEmpty {
// let item = ContextMenuItem(strings().messageContextOpenWith, handler: {}, itemImage: MenuAnimation.menu_open_with.value)
// let menu = ContextMenu()
// item.submenu = menu
// for item in result {
// menu.addItem(ContextMenuItem(item.fullname, handler: {
// NSWorkspace.shared.openFile(path, withApplication: item.app.path)
// }, image: item.icon))
// }
// thirdBlock.append(item)
//
// } else {
// try? FileManager.default.removeItem(atPath: path)
// }
// }
}
}
} else if data.image != nil, !data.isMediaStory {
if resourceData.complete {
let text = strings().chatContextCopyMedia
thirdBlock.append(ContextMenuItem(text, handler: {
if let path = link(path: resourceData.path, ext: "jpg") {
let pb = NSPasteboard.general
pb.clearContents()
pb.writeObjects([NSURL(fileURLWithPath: path)])
showModalText(for: context.window, text: strings().contextAlertCopied)
}
}, itemImage: MenuAnimation.menu_copy_media.value, keyEquivalent: .cmdc))
thirdBlock.append(ContextMenuItem(strings().chatContextSaveMedia, handler: {
savePanel(file: resourceData.path, ext: "jpg", for: context.window)
}, itemImage: MenuAnimation.menu_save_as.value, keyEquivalent: .cmds))
}
}
}
if (MessageReadMenuItem.canViewReadStats(message: data.message, chatInteraction: data.chatInteraction, appConfig: appConfiguration)), data.isRead, !data.isLogInteraction {
if data.message.id.peerId.namespace == Namespaces.Peer.CloudUser {
fourthBlock.append(MessageReadMenuItem(context: context, chatInteraction: data.chatInteraction, message: message, availableReactions: data.availableReactions))
} else {
fourthBlock.append(MessageReadMenuItem(context: context, chatInteraction: data.chatInteraction, message: message, availableReactions: data.availableReactions))
}
}
// #if DEBUG
// fourthBlock.append(MessageReadMenuItem(context: context, chatInteraction: data.chatInteraction, message: message))
// #endif
if let peer = peer, peer.isChannel, !peer.isAdmin {
let userCanFactCheck = context.appConfiguration.getBoolValue("can_edit_factcheck", orElse: false)
if canFactCheck(message), userCanFactCheck {
fifthBlock.append(ContextMenuItem(strings().factCheckContextEdit, handler: {
showModal(with: FactCheckController(context: context, message: message), for: context.window)
}, itemImage: MenuAnimation.menu_verification.value))
}
}
if canReportMessage(data.message, context), data.chatMode != .pinned, !data.isLogInteraction {
let report = ContextMenuItem(strings().messageContextReport, itemImage: MenuAnimation.menu_report.value)
let submenu = ContextMenu()
let options:[ReportReason] = [.spam, .violence, .porno, .childAbuse, .copyright, .personalDetails, .illegalDrugs]
let animation:[LocalAnimatedSticker] = [.menu_delete, .menu_violence, .menu_pornography, .menu_restrict, .menu_copyright, .menu_open_profile, .menu_drugs]
for i in 0 ..< options.count {
submenu.addItem(ContextMenuItem(options[i].title, handler: {
_ = showModalProgress(signal: context.engine.peers.reportPeerMessages(messageIds: [messageId], reason: options[i], message: ""), for: context.window).start(completed: {
showModalText(for: context.window, text: strings().messageContextReportAlertOK)
})
}, itemImage: animation[i].value))
}
submenu.addItem(ContextMenuItem(strings().reportReasonOther, handler: {
showModal(with: ReportDetailsController(context: context, reason: .init(reason: .custom, comment: ""), updated: { value in
_ = showModalProgress(signal: context.engine.peers.reportPeerMessages(messageIds: [messageId], reason: .custom, message: value.comment), for: context.window).start(completed: {
showModalText(for: context.window, text: strings().messageContextReportAlertOK)
})
}), for: context.window)
}, itemImage: MenuAnimation.menu_read.value))
report.submenu = submenu
fifthBlock.append(report)
}
if let peer = data.peer as? TelegramChannel, peer.isSupergroup, data.chatMode == .history {
if peer.groupAccess.canEditMembers, let author = data.message.author {
if author.id != context.peerId, data.message.flags.contains(.Incoming), author.isUser || author.isBot {
fifthBlock.append(ContextMenuItem(strings().chatContextRestrict, handler: {
_ = showModalProgress(signal: context.engine.peers.fetchChannelParticipant(peerId: chatInteraction.peerId, participantId: author.id), for: context.window).start(next: { participant in
if let participant = participant {
switch participant {
case let .member(memberId, _, _, _, _):
showModal(with: RestrictedModalViewController(context, peerId: peerId, memberId: memberId, initialParticipant: participant, updated: { updatedRights in
_ = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: author.id, bannedRights: updatedRights).startStandalone()
_ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 3.0).startStandalone()
}), for: context.window)
default:
break
}
}
})
}, itemImage: MenuAnimation.menu_restrict.value))
if data.isLogInteraction {
fifthBlock.append(ContextMenuItem(strings().chatContextBan, handler: {
_ = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: author.id, bannedRights: TelegramChatBannedRights(flags: .banReadMessages, untilDate: .max)).startStandalone()
_ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 3.0).startStandalone()
}, itemMode: .destruct, itemImage: MenuAnimation.menu_ban.value))
}
}
}
}
if data.updatingMessageMedia[messageId] != nil {
fifthBlock.append(ContextMenuItem(strings().chatContextCancelEditing, handler: {
account.pendingUpdateMessageManager.cancel(messageId: messageId)
}, itemImage: MenuAnimation.menu_clear_history.value))
}
if canDeleteMessage(data.message, account: account, mode: data.chatMode), !data.isLogInteraction {
fifthBlock.append(ContextMenuItem(strings().messageContextDelete, handler: {
data.chatInteraction.deleteMessages([data.message.id])
}, itemMode: .destruct, itemImage: MenuAnimation.menu_delete.value))
}
if data.isLogInteraction {
if let adminLog = entry?.additionalData.eventLog {
let config = AntiSpamBotConfiguration.with(appConfiguration: context.appConfiguration)
if adminLog.peerId == config.antiSpamBotId {
firstBlock.append(ContextMenuItem(strings().chatContextReportFalsePositive, handler: {
_ = context.engine.peers.reportAntiSpamFalsePositive(peerId: message.id.peerId, messageId: message.id).start()
showModalText(for: context.window, text: strings().chatContextReportFalsePositiveThanks)
}, itemImage: MenuAnimation.menu_report_false_positive.value))
}
}
}
#if BETA || DEBUG
if let mediaId = message.media.first?.id, !data.isLogInteraction {
fifthBlock.append(ContextSeparatorItem())
fifthBlock.append(ContextMenuItem("Copy Media Id (dev)", handler: {
copyToClipboard("\(mediaId.id)")
showModalText(for: context.window, text: "Copied")
}, itemMode: .normal, itemImage: MenuAnimation.menu_copy.value))
}
#endif
if let attr = message.textEntities, !isService {
var references: [StickerPackReference] = attr.entities.compactMap({ value in
if case let .CustomEmoji(reference, _) = value.type {
return reference
} else {
return nil
}
})
references += message.associatedMedia.compactMap {
($0.value as? TelegramMediaFile)?.emojiReference
}
references = references.uniqueElements
let sources: [StickerPackPreviewSource] = references.map {
.emoji($0)
}
if !sources.isEmpty {
let text = strings().chatContextMessageContainsEmojiCountable(sources.count)
let item = MessageContainsPacksMenuItem(title: text, handler: {
showModal(with: StickerPackPreviewModalController(context, peerId: peerId, references: sources), for: context.window)
}, packs: references, context: context)
sixBlock.append(item)
// sixBlock.append(ContextMenuItem(strings().chatContextViewEmojiSetNewCountable(sources.count), handler: {
// showModal(with: StickerPackPreviewModalController(context, peerId: peerId, references: sources), for: context.window)
// }, itemImage: MenuAnimation.menu_smile.value))
}
}
let blocks:[[ContextMenuItem]] = [zeroBlock, firstBlock,
add_secondBlock,
thirdBlock,
secondBlock,
fourthBlock,
fifthBlock,
sixBlock].filter { !$0.isEmpty }
for (i, block) in blocks.enumerated() {
if i == 0 {
items.append(contentsOf: block)
} else {
items.append(ContextSeparatorItem())
items.append(contentsOf: block)
}
}
for item in items {
item.contextObject = data
}
return items
}
}