Swiftgram/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift
2025-08-28 04:16:09 +04:00

1293 lines
65 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import DirectionalPanGesture
import ChatPresentationInterfaceState
import ChatControllerInteraction
import ContextUI
import UndoUI
import ChatHistoryEntry
final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestureRecognizerDelegate {
let ready = Promise<Bool>()
private let context: AccountContext
private let source: ChatHistoryListSource
private let chatLocation: ChatLocation
private var presentationData: PresentationData
private let type: MediaManagerPlayerType
private let requestDismiss: () -> Void
private let requestShare: (ShareControllerSubject) -> Void
private let requestSearchByArtist: (String) -> Void
private let playlistLocation: SharedMediaPlaylistLocation?
private let isGlobalSearch: Bool
private let controllerInteraction: ChatControllerInteraction
private var currentIsReversed: Bool
private let dimNode: ASDisplayNode
private let contentNode: ASDisplayNode
private let controlsNode: OverlayPlayerControlsNode
private let historyBackgroundNode: ASDisplayNode
private let historyBackgroundContentNode: ASDisplayNode
private var floatingHeaderOffset: CGFloat?
private var historyNode: ChatHistoryListNodeImpl
private var replacementHistoryNode: ChatHistoryListNodeImpl?
private var replacementHistoryNodeFloatingOffset: CGFloat?
private var saveMediaDisposable: MetaDisposable?
private var validLayout: ContainerViewLayout?
private var presentationDataDisposable: Disposable?
private let replacementHistoryNodeReadyDisposable = MetaDisposable()
private let getParentController: () -> ViewController?
private var savedIdsDisposable: Disposable?
private var savedIdsPromise = Promise<Set<Int64>?>()
private var savedIds: Set<Int64>?
private var copyProtectionEnabled = false
init(
context: AccountContext,
chatLocation: ChatLocation,
type: MediaManagerPlayerType,
initialMessageId: MessageId,
initialOrder: MusicPlaybackSettingsOrder,
playlistLocation: SharedMediaPlaylistLocation?,
requestDismiss: @escaping () -> Void,
requestShare: @escaping (ShareControllerSubject) -> Void,
requestSearchByArtist: @escaping (String) -> Void,
getParentController: @escaping () -> ViewController?
) {
self.context = context
self.chatLocation = chatLocation
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.type = type
self.requestDismiss = requestDismiss
self.requestShare = requestShare
self.requestSearchByArtist = requestSearchByArtist
self.playlistLocation = playlistLocation
self.getParentController = getParentController
if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, canReorder, at, loadMore) = playlistLocation.effectiveLocation(context: context) {
self.source = .custom(messages: messages, messageId: at, quote: nil, isSavedMusic: true, canReorder: canReorder, loadMore: loadMore)
self.isGlobalSearch = false
} else {
self.source = .default
self.isGlobalSearch = false
}
if case .regular = initialOrder {
self.currentIsReversed = false
} else {
self.currentIsReversed = true
}
var openMessageImpl: ((MessageId) -> Bool)?
var openMessageContextMenuImpl: ((Message, ASDisplayNode, CGRect, Any?) -> Void)?
self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in
if let openMessageImpl = openMessageImpl {
return openMessageImpl(message.id)
} else {
return false
}
}, openPeer: { _, _, _, _ in
}, openPeerMention: { _, _ in
}, openMessageContextMenu: { message, _, node, rect, gesture, _ in
openMessageContextMenuImpl?(message, node, rect, gesture)
}, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _, _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in
}, sendCurrentMessage: { _, _ in
}, sendMessage: { _ in
}, sendSticker: { _, _, _, _, _, _, _, _, _ in
return false
}, sendEmoji: { _, _, _ in
}, sendGif: { _, _, _, _, _ in
return false
}, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, sendBotCommand: { _, _ in
}, openInstantPage: { _, _ in
}, openWallpaper: { _ in
}, openTheme: {_ in
}, openHashtag: { _, _ in
}, updateInputState: { _ in
}, updateInputMode: { _ in
}, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, navigationController: {
return nil
}, chatControllerNode: {
return nil
}, presentGlobalOverlayController: { _, _ in
}, callPeer: { _, _ in
}, openConferenceCall: { _ in
}, longTap: { _, _ in
}, todoItemLongTap: { _, _ in
}, openCheckoutOrReceipt: { _, _ in
}, openSearch: {
}, setupReply: { _ in
}, canSetupReply: { _ in
return .none
}, canSendMessages: {
return false
}, navigateToFirstDateMessage: { _, _ in
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: { _ in
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _, _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, displayEmojiPackTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, copyText: { _ in
}, displayUndo: { _ in
}, isAnimatingMessage: { _ in
return false
}, getMessageTransitionNode: {
return nil
}, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _, _, _ in
}, adContextAction: { _, _, _ in
}, removeAd: { _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {
}, openAdsInfo: {
}, displayGiveawayParticipationStatus: { _ in
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, openUniqueGift: { _ in
}, openMessageFeeException: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
}, scrollToMessageId: { _ in
}, navigateToStory: { _, _ in
}, attemptedNavigationToPrivateQuote: { _ in
}, forceUpdateWarpContents: {
}, playShakeAnimation: {
}, displayQuickShare: { _, _ ,_ in
}, updateChatLocationThread: { _, _ in
}, requestToggleTodoMessageItem: { _, _, _ in
}, displayTodoToggleUnavailable: { _ in
}, openStarsPurchase: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentNode = ASDisplayNode()
self.controlsNode = OverlayPlayerControlsNode(account: context.account, engine: context.engine, accountManager: context.sharedContext.accountManager, presentationData: self.presentationData, status: context.sharedContext.mediaManager.musicMediaPlayerState, chatLocation: self.chatLocation, source: self.source)
self.controlsNode.getParentController = getParentController
self.historyBackgroundNode = ASDisplayNode()
self.historyBackgroundNode.isLayerBacked = true
self.historyBackgroundContentNode = ASDisplayNode()
self.historyBackgroundContentNode.isLayerBacked = true
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
let tagMask: MessageTags
switch type {
case .music:
tagMask = .music
case .voice:
tagMask = .voiceOrInstantVideo
case .file:
tagMask = .file
}
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
self.historyNode = ChatHistoryListNodeImpl(context: context, updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil, tag: .tag(tagMask), source: self.source, subject: .message(id: .id(initialMessageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil })
self.historyNode.clipsToBounds = true
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.historyNode.preloadPages = true
self.historyNode.stackFromBottom = true
self.historyNode.areContentAnimationsEnabled = true
self.historyNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self {
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
}
}
self.historyNode.endedInteractiveDragging = { [weak self] _ in
guard let self else {
return
}
switch self.historyNode.visibleContentOffset() {
case let .known(value):
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
} else {
if value <= -10.0 {
self.requestDismiss()
}
}
default:
break
}
}
self.controlsNode.updateIsExpanded = { [weak self] in
if let strongSelf = self, let validLayout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.5, curve: .spring))
}
}
self.controlsNode.requestCollapse = { [weak self] in
self?.requestDismiss()
}
self.controlsNode.requestShare = { [weak self] subject in
self?.requestShare(subject)
}
self.controlsNode.requestSearchByArtist = { [weak self] artist in
self?.requestSearchByArtist(artist)
}
self.controlsNode.requestLayout = { [weak self] transition in
if let self, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
}
self.controlsNode.updateOrder = { [weak self] order in
if let strongSelf = self {
let reversed: Bool
if case .regular = order {
reversed = false
} else {
reversed = true
}
if reversed != strongSelf.currentIsReversed {
strongSelf.currentIsReversed = reversed
if let itemId = strongSelf.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId {
strongSelf.transitionToUpdatedHistoryNode(atMessage: itemId.messageId)
}
}
}
}
self.controlsNode.control = { [weak self] action in
if let strongSelf = self {
strongSelf.context.sharedContext.mediaManager.playlistControl(action, type: strongSelf.type)
}
}
self.controlsNode.requestSaveToProfile = { [weak self] file in
if let self {
self.addToSavedMusic(file: file)
}
}
self.controlsNode.requestRemoveFromProfile = { [weak self] file in
if let self {
self.removeFromSavedMusic(file: file)
}
}
self.addSubnode(self.dimNode)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.historyBackgroundNode)
self.contentNode.addSubnode(self.historyNode)
self.contentNode.addSubnode(self.controlsNode)
self.historyNode.beganInteractiveDragging = { [weak self] _ in
self?.controlsNode.collapse()
}
openMessageImpl = { [weak self] id in
if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.historyNode.messageInCurrentHistoryView(id) {
var playlistLocation: PeerMessagesPlaylistLocation?
if let location = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation {
if case let .custom(messages, canReorder, _, loadMore) = location {
playlistLocation = .custom(messages: messages, canReorder: canReorder, at: id, loadMore: loadMore)
} else if case let .savedMusic(context, _, canReorder) = location {
playlistLocation = .savedMusic(context: context, at: id.id, canReorder: canReorder)
}
}
return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in
}, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: playlistLocation))
}
return false
}
openMessageContextMenuImpl = { [weak self] message, node, rect, gesture in
guard let self else {
return
}
self.openMessageContextMenu(message: message, node: node, frame: rect, gesture: gesture as? ContextGesture)
}
self.presentationDataDisposable = context.sharedContext.presentationData.startStrict(next: { [weak self] presentationData in
if let strongSelf = self {
if strongSelf.presentationData.theme !== presentationData.theme || strongSelf.presentationData.strings !== presentationData.strings {
strongSelf.updatePresentationData(presentationData)
}
}
})
let copyProtectionEnabled: Signal<Bool, NoError>
if case let .peer(peerId) = self.chatLocation {
copyProtectionEnabled = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CopyProtectionEnabled(id: peerId))
} else {
copyProtectionEnabled = .single(false)
}
self.savedIdsDisposable = combineLatest(
queue: Queue.mainQueue(),
context.engine.peers.savedMusicIds(),
copyProtectionEnabled
).start(next: { [weak self] savedIds, copyProtectionEnabled in
guard let self else {
return
}
let isFirstTime = self.savedIds == nil
self.savedIds = savedIds
self.savedIdsPromise.set(.single(savedIds))
self.copyProtectionEnabled = copyProtectionEnabled
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : .animated(duration: 0.5, curve: .spring)
self.updateFloatingHeaderOffset(offset: self.floatingHeaderOffset ?? 0.0, transition: transition)
if let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
})
self.ready.set(
combineLatest(
self.historyNode.historyState.get()
|> take(1),
self.savedIdsPromise.get()
|> filter {
$0 != nil
}
|> take(1)
)
|> map { _, _ -> Bool in
return true
}
)
self.setupReordering()
}
deinit {
self.presentationDataDisposable?.dispose()
self.replacementHistoryNodeReadyDisposable.dispose()
self.savedIdsDisposable?.dispose()
self.saveMediaDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
panRecognizer.shouldBegin = { [weak self] point in
guard let self else {
return false
}
if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) {
if self.controlsNode.frame.maxY <= self.historyNode.frame.minY {
return true
}
}
return false
}
self.view.addGestureRecognizer(panRecognizer)
}
private func setupReordering() {
guard let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(savedMusicContext, _, canReorder) = playlistLocation, canReorder else {
return
}
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: savedMusicContext.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer = peer.flatMap({ PeerReference($0._asPeer()) }) else {
return
}
self.historyNode.reorderItem = { fromIndex, toIndex, transactionOpaqueState -> Signal<Bool, NoError> in
guard let filteredEntries = (transactionOpaqueState as? ChatHistoryTransactionOpaqueState)?.historyView.filteredEntries, !filteredEntries.isEmpty else {
return .single(false)
}
func mapIndex(_ uiIndex: Int) -> Int {
return filteredEntries.count - 1 - uiIndex
}
let mappedFromIndex = mapIndex(fromIndex)
guard filteredEntries.indices.contains(mappedFromIndex), case let .MessageEntry(fromMessage, _, _, _, _, _) = filteredEntries[mappedFromIndex], let fromFile = fromMessage.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile else {
return .single(false)
}
var afterFile: TelegramMediaFile?
if toIndex > 0 {
let afterMappedIndex = mapIndex(toIndex - 1)
if filteredEntries.indices.contains(afterMappedIndex), case let .MessageEntry(afterMessage, _, _, _, _, _) = filteredEntries[afterMappedIndex], let file = afterMessage.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
afterFile = file
}
} else {
afterFile = nil
}
let _ = savedMusicContext.addMusic(
file: .savedMusic(peer: peer, media: fromFile),
afterFile: afterFile.flatMap { .savedMusic(peer: peer, media: $0) }
).start()
return .single(true)
}
})
self.historyNode.autoScrollWhenReordering = false
self.historyNode.didEndScrollingWithOverscroll = { [weak self] in
guard let self else {
return
}
self.requestDismiss()
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.controlsNode.updatePresentationData(self.presentationData)
}
private func dismissAllTooltips() {
guard let controller = self.getParentController() else {
return
}
controller.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
controller.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
}
func forwardToSavedMessages(file: FileMediaReference) {
self.dismissAllTooltips()
let _ = self.context.engine.messages.enqueueOutgoingMessage(to: self.context.account.peerId, replyTo: nil, content: .file(file)).start()
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .forward(savedMessages: true, text: "Audio forwarded to Saved Messages."),
action: { _ in
return true
}
)
self.getParentController()?.present(controller, in: .window(.root))
}
private func updateMusicSaved(file: FileMediaReference, isSaved: Bool) {
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(savedMusicContext, _, _) = playlistLocation, savedMusicContext.peerId == self.context.account.peerId {
if isSaved {
let _ = savedMusicContext.addMusic(file: file).start()
} else {
let _ = savedMusicContext.removeMusic(file: file).start()
}
} else {
if isSaved {
let _ = self.context.engine.peers.addSavedMusic(file: file).start()
} else {
let _ = self.context.engine.peers.removeSavedMusic(file: file).start()
}
}
}
func addToSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips()
var actionText: String? = self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile_View
if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId {
actionText = nil
}
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil,
title: nil,
text: self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile,
customUndoText: actionText,
timeout: 3.0
),
action: { [weak self] action in
if let self, case .undo = action {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
if let controller = self.context.sharedContext.makePeerInfoController(
context: self.context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .myProfile,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
if let navigationController = (self.getParentController() as? OverlayAudioPlayerControllerImpl)?.parentNavigationController {
self.requestDismiss()
navigationController.pushViewController(controller)
}
}
})
}
return true
}
)
self.getParentController()?.present(controller, in: .window(.root))
self.updateMusicSaved(file: file, isSaved: true)
}
func removeFromSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips()
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil,
title: nil,
text: self.presentationData.strings.MediaPlayer_SavedMusic_RemovedFromProfile,
customUndoText: nil,
timeout: 3.0
),
action: { _ in
return true
}
)
if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId, self.historyNode.originalHistoryView?.entries.count == 1 {
if let navigationController = (self.getParentController() as? OverlayAudioPlayerControllerImpl)?.parentNavigationController {
self.requestDismiss()
navigationController.presentOverlay(controller: controller)
self.context.sharedContext.mediaManager.setPlaylist(nil, type: self.type, control: .playback(.pause))
}
} else {
self.getParentController()?.present(controller, in: .window(.root))
}
self.updateMusicSaved(file: file, isSaved: false)
}
private var isSaved: Bool? {
if self .copyProtectionEnabled {
return nil
}
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
return nil
}
guard let fileReference = self.controlsNode.currentFileReference else {
return nil
}
return self.savedIds?.contains(fileReference.media.fileId.id)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
var insets = UIEdgeInsets()
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
insets.bottom = layout.intrinsicInsets.bottom
if layout.size.width > layout.size.height && self.controlsNode.isExpanded {
self.controlsNode.isExpanded = false
}
let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5)
let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved)
let listTopInset = layoutTopInset + controlsHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5))
var itemOffsetInsets = insets
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
itemOffsetInsets.top = 0.0
itemOffsetInsets.bottom = 0.0
insets = itemOffsetInsets
}
transition.updateFrame(node: self.historyNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: duration, curve: curve)
self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
if let replacementHistoryNode = self.replacementHistoryNode {
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil))
replacementHistoryNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
}
}
func animateIn() {
self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.dimNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.bounds.size.height), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
}
func animateOut(completion: (() -> Void)?) {
self.dismissAllTooltips()
self.layer.animateBoundsOriginYAdditive(from: self.bounds.origin.y, to: -self.bounds.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion?()
})
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.dimNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -self.bounds.size.height), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) {
let controlsHitTest = self.controlsNode.view.hitTest(self.view.convert(point, to: self.controlsNode.view), with: event)
if controlsHitTest == nil {
if self.controlsNode.frame.maxY > self.historyNode.frame.minY {
return self.historyNode.view
}
}
}
let result = super.hitTest(point, with: event)
if !self.bounds.contains(point) {
return nil
}
if point.y < self.controlsNode.frame.minY {
return self.dimNode.view
}
return result
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.requestDismiss()
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let recognizer = gestureRecognizer as? UIPanGestureRecognizer {
let location = recognizer.location(in: self.view)
if let view = super.hitTest(location, with: nil) {
if let gestureRecognizers = view.gestureRecognizers, view != self.view {
for gestureRecognizer in gestureRecognizers {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, gestureRecognizer.isEnabled {
if panGestureRecognizer.state != .began {
panGestureRecognizer.isEnabled = false
panGestureRecognizer.isEnabled = true
}
}
}
}
}
}
return true
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.dismissAllTooltips()
case .changed:
let translation = recognizer.translation(in: self.contentNode.view)
var bounds = self.contentNode.bounds
bounds.origin.y = -translation.y
bounds.origin.y = min(0.0, bounds.origin.y)
if bounds.origin.y < 0.0 {
//let delta = -bounds.origin.y
//bounds.origin.y = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0)
}
self.contentNode.bounds = bounds
case .ended:
let translation = recognizer.translation(in: self.contentNode.view)
var bounds = self.contentNode.bounds
bounds.origin.y = -translation.y
if bounds.origin.y < 0.0 {
//let delta = -bounds.origin.y
//bounds.origin.y = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0)
}
let velocity = recognizer.velocity(in: self.contentNode.view)
if (bounds.minY < -60.0 || velocity.y > 300.0) {
self.requestDismiss()
} else {
let previousBounds = self.bounds
var bounds = self.bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
case .cancelled:
let previousBounds = self.contentNode.bounds
var bounds = self.contentNode.bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
default:
break
}
}
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let validLayout = self.validLayout else {
return
}
self.floatingHeaderOffset = offset
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
let maxHeight = validLayout.size.height - layoutTopInset - floor(56.0 * 0.5)
let controlsHeight = self.controlsNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, hasSectionHeader: true, savedMusic: self.isSaved, transition: transition)
let listTopInset = layoutTopInset + controlsHeight
let rawControlsOffset = offset + listTopInset - controlsHeight
let controlsOffset = max(layoutTopInset, rawControlsOffset)
let isOverscrolling = rawControlsOffset <= layoutTopInset
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight))
let previousFrame = self.controlsNode.frame
if !controlsFrame.equalTo(previousFrame) {
self.controlsNode.frame = controlsFrame
let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY)
transition.animateOffsetAdditive(node: self.controlsNode, offset: positionDelta.y)
}
transition.updateAlpha(node: self.controlsNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let previousBackgroundFrame = self.historyBackgroundNode.frame
if !backgroundFrame.equalTo(previousBackgroundFrame) {
self.historyBackgroundNode.frame = backgroundFrame
self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY)
transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y)
}
}
private func transitionToUpdatedHistoryNode(atMessage messageId: MessageId) {
let tagMask: MessageTags
switch self.type {
case .music:
tagMask = .music
case .voice:
tagMask = .voiceOrInstantVideo
case .file:
tagMask = .file
}
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
let historyNode = ChatHistoryListNodeImpl(context: self.context, updatedPresentationData: (self.context.sharedContext.currentPresentationData.with({ $0 }), self.context.sharedContext.presentationData), chatLocation: self.chatLocation, chatLocationContextHolder: chatLocationContextHolder, adMessagesContext: nil, tag: .tag(tagMask), source: self.source, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, reverseGroups: !self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch), isChatPreview: false, messageTransitionNode: { return nil })
historyNode.clipsToBounds = true
historyNode.preloadPages = true
historyNode.stackFromBottom = true
historyNode.areContentAnimationsEnabled = true
historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in
self?.replacementHistoryNodeFloatingOffset = offset
}
self.replacementHistoryNode = historyNode
if let layout = self.validLayout {
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
var insets = UIEdgeInsets()
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
insets.bottom = layout.intrinsicInsets.bottom
let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5)
let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved)
let listTopInset = layoutTopInset + controlsHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5))
historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil))
historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets)
}
self.replacementHistoryNodeReadyDisposable.set((historyNode.historyState.get() |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.replaceWithReadyUpdatedHistoryNode()
}
}))
}
private func replaceWithReadyUpdatedHistoryNode() {
if let replacementHistoryNode = self.replacementHistoryNode {
self.replacementHistoryNode = nil
let previousHistoryNode = self.historyNode
previousHistoryNode.disconnect()
self.contentNode.insertSubnode(replacementHistoryNode, belowSubnode: self.historyNode)
self.historyNode = replacementHistoryNode
self.setupReordering()
if let validLayout = self.validLayout, let offset = self.replacementHistoryNodeFloatingOffset, let previousOffset = self.floatingHeaderOffset {
let offsetDelta = offset - previousOffset
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
let maxHeight = validLayout.size.height - layoutTopInset - floor(56.0 * 0.5)
let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved)
let listTopInset = layoutTopInset + controlsHeight
let controlsBottomOffset = max(layoutTopInset, offset + listTopInset)
let previousBackgroundNode = ASDisplayNode()
previousBackgroundNode.isLayerBacked = true
previousBackgroundNode.backgroundColor = self.historyBackgroundContentNode.backgroundColor
self.contentNode.insertSubnode(previousBackgroundNode, belowSubnode: previousHistoryNode)
previousBackgroundNode.frame = self.historyBackgroundNode.frame
previousBackgroundNode.layer.animateFrame(from: previousBackgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: controlsBottomOffset), size: validLayout.size), duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.updateFloatingHeaderOffset(offset: offset, transition: .animated(duration: 0.4, curve: .spring))
previousHistoryNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousHistoryNode] _ in
previousHistoryNode?.removeFromSupernode()
})
previousHistoryNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
previousBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousBackgroundNode] _ in
previousBackgroundNode?.removeFromSupernode()
})
self.historyNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offsetDelta), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
} else {
previousHistoryNode.removeFromSupernode()
}
self.historyNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self {
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
}
}
self.historyNode.endedInteractiveDragging = { [weak self] _ in
guard let strongSelf = self else {
return
}
switch strongSelf.historyNode.visibleContentOffset() {
case let .known(value):
if value <= -10.0 {
strongSelf.requestDismiss()
}
default:
break
}
}
self.historyNode.beganInteractiveDragging = { [weak self] _ in
self?.controlsNode.collapse()
}
if let layout = self.validLayout {
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
var insets = UIEdgeInsets()
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
insets.bottom = layout.intrinsicInsets.bottom
let maxHeight = layout.size.height - layoutTopInset - floor(56.0 * 0.5)
let controlsHeight = OverlayPlayerControlsNode.heightForLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: maxHeight, isExpanded: self.controlsNode.isExpanded, hasSectionHeader: true, savedMusic: self.isSaved)
let listTopInset = layoutTopInset + controlsHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5))
var itemOffsetInsets = insets
if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder {
itemOffsetInsets.top = 0.0
itemOffsetInsets.bottom = 0.0
insets = itemOffsetInsets
}
self.historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: 0.0, curve: .Default(duration: nil))
self.historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets)
self.historyNode.recursivelyEnsureDisplaySynchronously(true)
}
}
}
private func openMessageContextMenu(message: Message, node: ASDisplayNode, frame: CGRect, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, location: CGPoint? = nil) {
guard let node = node as? ContextExtractedContentContainingNode, let peer = message.peers[message.id.peerId].flatMap({ PeerReference($0) }), let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile else {
return
}
let context = self.context
let presentationData = self.presentationData
let source: ContextContentSource = .extracted(OverlayAudioPlayerContextExtractedContentSource(contentNode: node))
let fileReference: FileMediaReference = message.id.namespace == Namespaces.Message.Local ? .savedMusic(peer: peer, media: file) : .message(message: MessageReference(message), media: file)
let canSaveToProfile = !(self.savedIds?.contains(file.fileId.id) == true)
let canSaveToSavedMessages = message.id.peerId != self.context.account.peerId || message.id.namespace == Namespaces.Message.Local
let _ = (context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: [message.id], keepUpdated: false)
|> deliverOnMainQueue).startStandalone(next: { [weak self] actions in
guard let self else {
return
}
var items: [ContextMenuItem] = []
if canSaveToProfile || canSaveToSavedMessages {
items.append(
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
if let self {
var subActions: [ContextMenuItem] = []
subActions.append(
.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { c, _ in
c?.popItems()
}))
)
subActions.append(.separator)
if canSaveToProfile {
subActions.append(
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Profile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
if let self {
self.addToSavedMusic(file: fileReference)
}
}))
)
}
if canSaveToSavedMessages {
subActions.append(
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_SavedMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
if let self {
self.forwardToSavedMessages(file: fileReference)
}
}))
)
}
subActions.append(
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Files, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
if let self {
let disposable: MetaDisposable
if let current = self.saveMediaDisposable {
disposable = current
} else {
disposable = MetaDisposable()
self.saveMediaDisposable = disposable
}
disposable.set(
saveMediaToFiles(context: context, fileReference: fileReference, present: { [weak self] c, a in
if let self, let controller = (self.getParentController() as? OverlayAudioPlayerControllerImpl) {
controller.present(c, in: .window(.root), with: a)
}
})
)
}
}))
)
let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil
subActions.append(
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction))
)
c?.pushItems(items: .single(ContextController.Items(content: .list(subActions))))
}
}))
)
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveToFiles, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.default)
if let self {
let disposable: MetaDisposable
if let current = self.saveMediaDisposable {
disposable = current
} else {
disposable = MetaDisposable()
self.saveMediaDisposable = disposable
}
disposable.set(
saveMediaToFiles(context: context, fileReference: fileReference, present: { [weak self] c, a in
if let self, let controller = (self.getParentController() as? OverlayAudioPlayerControllerImpl) {
controller.present(c, in: .window(.root), with: a)
}
})
)
}
})))
}
var addedSeparator = false
if message.id.namespace == Namespaces.Message.Cloud {
if !addedSeparator {
items.append(.separator)
addedSeparator = true
}
items.append(
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_ShowInChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self else {
return
}
context.sharedContext.navigateToChat(accountId: context.account.id, peerId: message.id.peerId, messageId: message.id)
self.requestDismiss()
}))
)
}
// items.append(
// .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_Forward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default)
//
// if let _ = self {
//
// }
// }))
// )
var canDelete = false
if case .custom = self.source {
if self.savedIds?.contains(file.fileId.id) == true {
canDelete = true
}
} else if let peer = message.peers[message.id.peerId] {
if peer is TelegramUser || peer is TelegramSecretChat {
canDelete = true
} else if let _ = peer as? TelegramGroup {
canDelete = true
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
} else {
canDelete = true
}
} else {
canDelete = false
}
} else {
canDelete = false
}
if canDelete {
if !addedSeparator {
items.append(.separator)
addedSeparator = true
}
var actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Delete
if case .custom = self.source {
actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Remove
}
items.append(
.action(ContextMenuActionItem(text: actionTitle, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in
guard let self else {
return
}
if message.id.namespace == Namespaces.Message.Local {
f(.default)
self.removeFromSavedMusic(file: fileReference)
} else {
c?.setItems(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId))
|> map { peer -> ContextController.Items in
var items: [ContextMenuItem] = []
let messageIds = [message.id]
if let peer {
var personalPeerName: String?
var isChannel = false
if case let .user(user) = peer {
personalPeerName = EnginePeer(user).compactDisplayTitle
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
isChannel = true
}
if actions.options.contains(.deleteGlobally) {
let globalTitle: String
if isChannel {
globalTitle = presentationData.strings.Conversation_DeleteMessagesForEveryone
} else if let personalPeerName = personalPeerName {
globalTitle = presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string
} else {
globalTitle = presentationData.strings.Conversation_DeleteMessagesForEveryone
}
items.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { c, f in
c?.dismiss(completion: {
let _ = context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
})
})))
}
if actions.options.contains(.deleteLocally) {
var localOptionText = presentationData.strings.Conversation_DeleteMessagesForMe
if context.account.peerId == message.id.peerId {
if messageIds.count == 1 {
localOptionText = presentationData.strings.Conversation_Moderate_Delete
} else {
localOptionText = presentationData.strings.Conversation_DeleteManyMessages
}
}
items.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { c, f in
c?.dismiss(completion: {
let _ = context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forLocalPeer).startStandalone()
})
})))
}
}
return ContextController.Items(content: .list(items))
}, minHeight: nil, animated: true)
}
}))
)
}
guard !items.isEmpty else {
return
}
let contextController = ContextController(presentationData: presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture)
self.getParentController()?.presentInGlobalOverlay(contextController)
})
}
}
private final class OverlayAudioPlayerContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = false
let blurBackground: Bool = true
let additionalInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 80.0, right: 0.0)
private let contentNode: ContextExtractedContentContainingNode
init(contentNode: ContextExtractedContentContainingNode) {
self.contentNode = contentNode
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}