import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData import TelegramUIPreferences import UniversalMediaPlayer import AccountContext import OverlayStatusController import PresentationDataUtils import TelegramCallsUI import UndoUI public enum MediaAccessoryPanelVisibility { case none case specific(size: ContainerViewLayoutSizeClass) case always } public enum LocationBroadcastPanelSource { case none case summary case peer(PeerId) } private func presentLiveLocationController(context: AccountContext, peerId: PeerId, controller: ViewController) { let presentImpl: (EngineMessage?) -> Void = { [weak controller] message in if let message = message, let strongController = controller { let _ = context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message._asMessage(), standalone: false, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, modal: true, dismissInput: { controller?.view.endEditing(true) }, present: { c, a, _ in controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { peer, navigation in }, callPeer: { _, _ in }, enqueueMessage: { message in let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in })) } } if let id = context.liveLocationManager?.internalMessageForPeerId(peerId) { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: id)) |> deliverOnMainQueue).start(next: presentImpl) } else if let liveLocationManager = context.liveLocationManager { let _ = (liveLocationManager.summaryManager.peersBroadcastingTo(peerId: peerId) |> take(1) |> map { peersAndMessages -> EngineMessage? in return peersAndMessages?.first?.1 } |> deliverOnMainQueue).start(next: presentImpl) } } open class TelegramBaseController: ViewController, KeyShortcutResponder { private let context: AccountContext public var accessoryPanelContainer: ASDisplayNode? public private(set) var accessoryPanelContainerHeight: CGFloat = 0.0 public let mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility public var tempHideAccessoryPanels: Bool = false public let locationBroadcastPanelSource: LocationBroadcastPanelSource public let groupCallPanelSource: GroupCallPanelSource private var mediaStatusDisposable: Disposable? private var locationBroadcastDisposable: Disposable? private var currentGroupCallDisposable: Disposable? public private(set) var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)? private var playlistLocation: SharedMediaPlaylistLocation? public var tempVoicePlaylistEnded: (() -> Void)? public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? public var tempVoicePlaylistCurrentItem: SharedMediaPlaylistItem? public private(set) var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? private var locationBroadcastPeers: [EnginePeer]? private var locationBroadcastMessages: [EngineMessage.Id: EngineMessage]? private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? private var groupCallPanelData: GroupCallPanelData? public private(set) var groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel? private var dismissingPanel: ASDisplayNode? private weak var audioRateTooltipController: UndoOverlayController? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var playlistPreloadDisposable: Disposable? override open var additionalNavigationBarHeight: CGFloat { var height: CGFloat = 0.0 if self.accessoryPanelContainer == nil { if let _ = self.groupCallAccessoryPanel { height += 50.0 } if let _ = self.mediaAccessoryPanel { height += MediaNavigationAccessoryHeaderNode.minimizedHeight } if let _ = self.locationBroadcastAccessoryPanel { height += MediaNavigationAccessoryHeaderNode.minimizedHeight } } return height } public init(context: AccountContext, navigationBarPresentationData: NavigationBarPresentationData?, mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility, locationBroadcastPanelSource: LocationBroadcastPanelSource, groupCallPanelSource: GroupCallPanelSource) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.mediaAccessoryPanelVisibility = mediaAccessoryPanelVisibility self.locationBroadcastPanelSource = locationBroadcastPanelSource self.groupCallPanelSource = groupCallPanelSource super.init(navigationBarPresentationData: navigationBarPresentationData) if case .none = mediaAccessoryPanelVisibility { } else { self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState |> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in if let (account, state, type) = playlistStateAndType { switch state { case let .state(state): return .single((account, state, type)) case .loading: return .single(nil) |> delay(0.2, queue: .mainQueue()) } } else { return .single(nil) } } |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in guard let strongSelf = self else { return } if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) || !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) || !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) || strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 { var previousVoiceItem: SharedMediaPlaylistItem? if let playlistStateAndType = strongSelf.playlistStateAndType, playlistStateAndType.4 == .voice { previousVoiceItem = playlistStateAndType.0 } var updatedVoiceItem: SharedMediaPlaylistItem? if let playlistStateAndType = playlistStateAndType, playlistStateAndType.2 == .voice { updatedVoiceItem = playlistStateAndType.1.item } strongSelf.tempVoicePlaylistCurrentItem = updatedVoiceItem strongSelf.tempVoicePlaylistItemChanged?(previousVoiceItem, updatedVoiceItem) if let playlistStateAndType = playlistStateAndType { strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0) } else { var voiceEnded = false if strongSelf.playlistStateAndType?.4 == .voice { voiceEnded = true } strongSelf.playlistStateAndType = nil if voiceEnded { strongSelf.tempVoicePlaylistEnded?() } } strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } strongSelf.playlistLocation = playlistStateAndType?.1.playlistLocation }) } if let liveLocationManager = context.liveLocationManager { switch locationBroadcastPanelSource { case .none: self.locationBroadcastMode = nil case .summary, .peer: let signal: Signal<([EnginePeer]?, [EngineMessage.Id: EngineMessage]?), NoError> switch locationBroadcastPanelSource { case let .peer(peerId): self.locationBroadcastMode = .peer signal = combineLatest(liveLocationManager.summaryManager.peersBroadcastingTo(peerId: peerId), liveLocationManager.summaryManager.broadcastingToMessages()) |> map { peersAndMessages, outgoingMessages in var peers = peersAndMessages?.map { $0.0 } for message in outgoingMessages.values { if message.id.peerId == peerId, let author = message.author { if peers == nil { peers = [] } peers?.append(author) } } return (peers, outgoingMessages) } default: self.locationBroadcastMode = .summary signal = liveLocationManager.summaryManager.broadcastingToMessages() |> map { messages -> ([EnginePeer]?, [EngineMessage.Id: EngineMessage]?) in if messages.isEmpty { return (nil, nil) } else { var peers: [EnginePeer] = [] for message in messages.values.sorted(by: { $0.index < $1.index }) { if let peer = message.peers[message.id.peerId] { peers.append(EnginePeer(peer)) } } return (peers, messages) } } } self.locationBroadcastDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peers, messages in if let strongSelf = self { var updated = false if let current = strongSelf.locationBroadcastPeers, let peers = peers { updated = current != peers } else if (strongSelf.locationBroadcastPeers != nil) != (peers != nil) { updated = true } strongSelf.locationBroadcastMessages = messages if updated { let wasEmpty = strongSelf.locationBroadcastPeers == nil strongSelf.locationBroadcastPeers = peers if wasEmpty != (peers == nil) { strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } else if let peers = peers, let locationBroadcastMode = strongSelf.locationBroadcastMode { var canClose = true if case let .peer(peerId) = strongSelf.locationBroadcastPanelSource, let messages = messages { canClose = false for messageId in messages.keys { if messageId.peerId == peerId { canClose = true } } } strongSelf.locationBroadcastAccessoryPanel?.update(peers: peers, mode: locationBroadcastMode, canClose: canClose) } } } }) } } if let callManager = context.sharedContext.callManager { switch groupCallPanelSource { case .none, .all: break case let .peer(peerId): let currentGroupCall: Signal = callManager.currentGroupCallSignal |> distinctUntilChanged(isEqual: { lhs, rhs in return lhs?.internalId == rhs?.internalId }) |> map { call -> PresentationGroupCall? in guard let call = call, call.peerId == peerId && call.account.peerId == context.account.peerId else { return nil } return call } let availableGroupCall: Signal if case let .peer(peerId) = groupCallPanelSource { availableGroupCall = context.account.viewTracker.peerView(peerId) |> map { peerView -> (CachedChannelData.ActiveCall?, EnginePeer?) in let peer = peerView.peers[peerId].flatMap(EnginePeer.init) if let cachedData = peerView.cachedData as? CachedChannelData { return (cachedData.activeCall, peer) } else if let cachedData = peerView.cachedData as? CachedGroupData { return (cachedData.activeCall, peer) } else { return (nil, peer) } } |> distinctUntilChanged(isEqual: { lhs, rhs in if lhs.0 != rhs.0 { return false } return true }) |> mapToSignal { activeCall, peer -> Signal in guard let activeCall = activeCall else { return .single(nil) } var isChannel = false if let peer = peer, case let .channel(channel) = peer, case .broadcast = channel.info { isChannel = true } return Signal { [weak context] subscriber in guard let context = context, let callContextCache = context.cachedGroupCallContexts as? AccountGroupCallContextCacheImpl else { return EmptyDisposable } let disposable = MetaDisposable() callContextCache.impl.syncWith { impl in let callContext = impl.get(account: context.account, engine: context.engine, peerId: peerId, isChannel: isChannel, call: EngineGroupCallDescription(activeCall)) disposable.set((callContext.context.panelData |> deliverOnMainQueue).start(next: { panelData in callContext.keep() var updatedPanelData = panelData if let panelData { var updatedInfo = panelData.info updatedInfo.subscribedToScheduled = activeCall.subscribedToScheduled updatedPanelData = panelData.withInfo(updatedInfo) } subscriber.putNext(updatedPanelData) })) } return ActionDisposable { disposable.dispose() } } |> runOn(.mainQueue()) } } else { availableGroupCall = .single(nil) } let previousCurrentGroupCall = Atomic(value: nil) self.currentGroupCallDisposable = combineLatest(queue: .mainQueue(), availableGroupCall, currentGroupCall).start(next: { [weak self] availableState, currentGroupCall in guard let strongSelf = self else { return } let previousCurrentGroupCall = previousCurrentGroupCall.swap(currentGroupCall) let panelData: GroupCallPanelData? if previousCurrentGroupCall != nil && currentGroupCall == nil && availableState?.participantCount == 1 { panelData = nil } else { panelData = currentGroupCall != nil || (availableState?.participantCount == 0 && availableState?.info.scheduleTimestamp == nil && availableState?.info.isStream == false) ? nil : availableState } let wasEmpty = strongSelf.groupCallPanelData == nil strongSelf.groupCallPanelData = panelData let isEmpty = strongSelf.groupCallPanelData == nil if wasEmpty != isEmpty { strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } else if let groupCallPanelData = strongSelf.groupCallPanelData { strongSelf.groupCallAccessoryPanel?.update(data: groupCallPanelData) } }) } } self.presentationDataDisposable = (self.updatedPresentationData.1 |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings strongSelf.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.mediaAccessoryPanel?.0.containerNode.updatePresentationData(presentationData) strongSelf.locationBroadcastAccessoryPanel?.updatePresentationData(presentationData) strongSelf.groupCallAccessoryPanel?.updatePresentationData(presentationData) } } }) } open var updatedPresentationData: (PresentationData, Signal) { return (self.presentationData, self.context.sharedContext.presentationData) } deinit { self.mediaStatusDisposable?.dispose() self.locationBroadcastDisposable?.dispose() self.currentGroupCallDisposable?.dispose() self.presentationDataDisposable?.dispose() self.playlistPreloadDisposable?.dispose() } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } private var suspendNavigationBarLayout: Bool = false private var suspendedNavigationBarLayout: ContainerViewLayout? private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 override open func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { if self.suspendNavigationBarLayout { self.suspendedNavigationBarLayout = layout return } self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.suspendNavigationBarLayout = true super.containerLayoutUpdated(layout, transition: transition) let navigationHeight = super.navigationLayout(layout: layout).navigationFrame.height - self.additionalNavigationBarHeight let mediaAccessoryPanelHidden: Bool if self.tempHideAccessoryPanels { mediaAccessoryPanelHidden = true } else { switch self.mediaAccessoryPanelVisibility { case .always: mediaAccessoryPanelHidden = false case .none: mediaAccessoryPanelHidden = true case let .specific(size): mediaAccessoryPanelHidden = size != layout.metrics.widthClass } } var additionalHeight: CGFloat = 0.0 var panelStartY: CGFloat = 0.0 if self.accessoryPanelContainer == nil { var negativeHeight: CGFloat = 0.0 if let _ = self.groupCallPanelData { negativeHeight += 50.0 } if let _ = self.locationBroadcastPeers, let _ = self.locationBroadcastMode { negativeHeight += MediaNavigationAccessoryHeaderNode.minimizedHeight } if let _ = self.playlistStateAndType, !mediaAccessoryPanelHidden { negativeHeight += MediaNavigationAccessoryHeaderNode.minimizedHeight } panelStartY = navigationHeight.isZero ? (-negativeHeight) : (navigationHeight + additionalHeight + UIScreenPixel) } if let groupCallPanelData = self.groupCallPanelData { let panelHeight: CGFloat = 50.0 let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight panelStartY += panelHeight let groupCallAccessoryPanel: GroupCallNavigationAccessoryPanel if let current = self.groupCallAccessoryPanel { groupCallAccessoryPanel = current transition.updateFrame(node: groupCallAccessoryPanel, frame: panelFrame) groupCallAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: transition) } else { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } groupCallAccessoryPanel = GroupCallNavigationAccessoryPanel(context: self.context, presentationData: presentationData, tapAction: { [weak self] in guard let strongSelf = self else { return } strongSelf.joinGroupCall( peerId: groupCallPanelData.peerId, invite: nil, activeCall: EngineGroupCallDescription(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, title: groupCallPanelData.info.title, scheduleTimestamp: groupCallPanelData.info.scheduleTimestamp, subscribedToScheduled: groupCallPanelData.info.subscribedToScheduled, isStream: groupCallPanelData.info.isStream) ) }, notifyScheduledTapAction: { [weak self] in guard let self, let groupCallPanelData = self.groupCallPanelData else { return } if groupCallPanelData.info.scheduleTimestamp != nil && !groupCallPanelData.info.subscribedToScheduled { let _ = self.context.engine.calls.toggleScheduledGroupCallSubscription(peerId: groupCallPanelData.peerId, callId: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash, subscribe: true).startStandalone() //TODO:localize let controller = UndoOverlayController( presentationData: presentationData, content: .universal( animation: "anim_set_notification", scale: 0.06, colors: [ "Middle.Group 1.Fill 1": UIColor.white, "Top.Group 1.Fill 1": UIColor.white, "Bottom.Group 1.Fill 1": UIColor.white, "EXAMPLE.Group 1.Fill 1": UIColor.white, "Line.Group 1.Stroke 1": UIColor.white ], title: nil, text: "You will be notified when the liver stream starts.", customUndoText: nil, timeout: nil ), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return true } ) self.audioRateTooltipController = controller self.present(controller, in: .current) } }) if let accessoryPanelContainer = self.accessoryPanelContainer { accessoryPanelContainer.addSubnode(groupCallAccessoryPanel) } else { self.navigationBar?.additionalContentNode.addSubnode(groupCallAccessoryPanel) } self.groupCallAccessoryPanel = groupCallAccessoryPanel groupCallAccessoryPanel.frame = panelFrame groupCallAccessoryPanel.update(data: groupCallPanelData) groupCallAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: .immediate) if transition.isAnimated { groupCallAccessoryPanel.animateIn(transition) } } } else if let groupCallAccessoryPanel = self.groupCallAccessoryPanel { self.groupCallAccessoryPanel = nil if transition.isAnimated { groupCallAccessoryPanel.animateOut(transition, completion: { [weak groupCallAccessoryPanel] in groupCallAccessoryPanel?.removeFromSupernode() }) } else { groupCallAccessoryPanel.removeFromSupernode() } } if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight panelStartY += panelHeight let locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel if let current = self.locationBroadcastAccessoryPanel { locationBroadcastAccessoryPanel = current transition.updateFrame(node: locationBroadcastAccessoryPanel, frame: panelFrame) locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: transition) } else { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } locationBroadcastAccessoryPanel = LocationBroadcastNavigationAccessoryPanel(accountPeerId: self.context.account.peerId, theme: presentationData.theme, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, tapAction: { [weak self] in if let strongSelf = self { switch strongSelf.locationBroadcastPanelSource { case .none: break case .summary: if let locationBroadcastMessages = strongSelf.locationBroadcastMessages { let messages = locationBroadcastMessages.values.sorted(by: { $0.index > $1.index }) if messages.count == 1 { presentLiveLocationController(context: strongSelf.context, peerId: messages[0].id.peerId, controller: strongSelf) } else { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var items: [ActionSheetItem] = [] if !messages.isEmpty { items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(messages.count)))) for message in messages { if let peer = message.peers[message.id.peerId] { var beginTimeAndTimeout: (Double, Double)? for media in message.media { if let media = media as? TelegramMediaMap, let timeout = media.liveBroadcastingTimeout { beginTimeAndTimeout = (Double(message.timestamp), Double(timeout)) } } if let beginTimeAndTimeout = beginTimeAndTimeout { items.append(LocationBroadcastActionSheetItem(context: strongSelf.context, peer: peer, title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), beginTimestamp: beginTimeAndTimeout.0, timeout: beginTimeAndTimeout.1, strings: presentationData.strings, action: { dismissAction() if let strongSelf = self { presentLiveLocationController(context: strongSelf.context, peerId: peer.id, controller: strongSelf) } })) } } } items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { dismissAction() if let locationBroadcastPeers = strongSelf.locationBroadcastPeers { for peer in locationBroadcastPeers { self?.context.liveLocationManager?.cancelLiveLocation(peerId: peer.id) } } })) } controller.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) strongSelf.view.endEditing(true) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } case let .peer(peerId): presentLiveLocationController(context: strongSelf.context, peerId: peerId, controller: strongSelf) } } }, close: { [weak self] in if let strongSelf = self { var closePeers: [EnginePeer]? var closePeerId: EnginePeer.Id? switch strongSelf.locationBroadcastPanelSource { case .none: break case .summary: if let locationBroadcastPeers = strongSelf.locationBroadcastPeers { if locationBroadcastPeers.count > 1 { closePeers = locationBroadcastPeers } else { closePeerId = locationBroadcastPeers.first?.id } } case let .peer(peerId): closePeerId = peerId } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var items: [ActionSheetItem] = [] if let closePeers = closePeers, !closePeers.isEmpty { items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(closePeers.count)))) for peer in closePeers { items.append(ActionSheetButtonItem(title: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), action: { dismissAction() if let strongSelf = self { presentLiveLocationController(context: strongSelf.context, peerId: peer.id, controller: strongSelf) } })) } items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { dismissAction() for peer in closePeers { self?.context.liveLocationManager?.cancelLiveLocation(peerId: peer.id) } })) } else if let closePeerId = closePeerId { items.append(ActionSheetButtonItem(title: presentationData.strings.Map_StopLiveLocation, color: .destructive, action: { dismissAction() self?.context.liveLocationManager?.cancelLiveLocation(peerId: closePeerId) })) } controller.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) strongSelf.view.endEditing(true) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) if let accessoryPanelContainer = self.accessoryPanelContainer { accessoryPanelContainer.addSubnode(locationBroadcastAccessoryPanel) } else { self.navigationBar?.additionalContentNode.addSubnode(locationBroadcastAccessoryPanel) } self.locationBroadcastAccessoryPanel = locationBroadcastAccessoryPanel locationBroadcastAccessoryPanel.frame = panelFrame var canClose = true if case let .peer(peerId) = self.locationBroadcastPanelSource, let messages = self.locationBroadcastMessages { canClose = false for messageId in messages.keys { if messageId.peerId == peerId { canClose = true } } } locationBroadcastAccessoryPanel.update(peers: locationBroadcastPeers, mode: locationBroadcastMode, canClose: canClose) locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: .immediate) if transition.isAnimated { locationBroadcastAccessoryPanel.animateIn(transition) } } } else if let locationBroadcastAccessoryPanel = self.locationBroadcastAccessoryPanel { self.locationBroadcastAccessoryPanel = nil if transition.isAnimated { locationBroadcastAccessoryPanel.animateOut(transition, completion: { [weak locationBroadcastAccessoryPanel] in locationBroadcastAccessoryPanel?.removeFromSupernode() }) } else { locationBroadcastAccessoryPanel.removeFromSupernode() } } var isViewOnceMessage = false if let (item, _, _, _, _, _) = self.playlistStateAndType, let source = item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce { isViewOnceMessage = true } if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType, !mediaAccessoryPanelHidden && !isViewOnceMessage { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight panelStartY += panelHeight if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: transition) switch order { case .regular: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) case .reversed: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) case .random: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) } let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState |> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in guard let value = value else { return .single(nil) } switch value.1 { case .state: return .single(value) case .loading: return .single(value) |> delay(0.1, queue: .mainQueue()) } } mediaAccessoryPanel.containerNode.headerNode.playbackStatus = delayedStatus |> map { state -> MediaPlayerStatus in if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { return state.status } else { return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) } } } else { if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { self.mediaAccessoryPanel = nil self.dismissingPanel = mediaAccessoryPanel self.audioRateTooltipController?.dismissWithCommitAction() mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in mediaAccessoryPanel?.removeFromSupernode() if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { strongSelf.dismissingPanel = nil } }) } let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context, presentationData: self.updatedPresentationData.0) mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo mediaAccessoryPanel.getController = { [weak self] in return self } mediaAccessoryPanel.presentInGlobalOverlay = { [weak self] c in self?.presentInGlobalOverlay(c) } mediaAccessoryPanel.close = { [weak self] in if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) } } mediaAccessoryPanel.setRate = { [weak self] rate, changeType in guard let strongSelf = self else { return } let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings)?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in return PreferencesEntry(settings.withUpdatedVoicePlaybackRate(rate)) }) return rate } |> deliverOnMainQueue).start(next: { baseRate in guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else { return } strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type) var hasTooltip = false strongSelf.forEachController({ controller in if let controller = controller as? UndoOverlayController { hasTooltip = true controller.dismissWithCommitAction() } return true }) let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let text: String? let rate: CGFloat? if case let .sliderCommit(previousValue, newValue) = changeType { let value = String(format: "%0.1f", baseRate.doubleValue) if baseRate == .x1 { text = presentationData.strings.Conversation_AudioRateTooltipNormal } else { text = presentationData.strings.Conversation_AudioRateTooltipCustom(value).string } if newValue > previousValue { rate = .infinity } else if newValue < previousValue { rate = -.infinity } else { rate = nil } } else if baseRate == .x1 { text = presentationData.strings.Conversation_AudioRateTooltipNormal rate = 1.0 } else if baseRate == .x1_5 { text = presentationData.strings.Conversation_AudioRateTooltip15X rate = 1.5 } else if baseRate == .x2 { text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp rate = 2.0 } else { text = nil rate = nil } var showTooltip = true if case .sliderChange = changeType { showTooltip = false } if let rate, let text, showTooltip { let controller = UndoOverlayController( presentationData: presentationData, content: .audioRate( rate: rate, text: text ), elevatedLayout: false, animateInAsReplacement: hasTooltip, action: { action in return true } ) strongSelf.audioRateTooltipController = controller strongSelf.present(controller, in: .current) } }) } mediaAccessoryPanel.togglePlayPause = { [weak self] in if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { strongSelf.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) } } mediaAccessoryPanel.playPrevious = { [weak self] in if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { strongSelf.context.sharedContext.mediaManager.playlistControl(.next, type: type) } } mediaAccessoryPanel.playNext = { [weak self] in if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType { strongSelf.context.sharedContext.mediaManager.playlistControl(.previous, type: type) } } mediaAccessoryPanel.tapAction = { [weak self] in guard let strongSelf = self, let _ = strongSelf.navigationController as? NavigationController, let (state, _, _, order, type, account) = strongSelf.playlistStateAndType else { return } if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation { if type == .music { if case .custom = playlistLocation { let controllerContext: AccountContext if account.id == strongSelf.context.account.id { controllerContext = strongSelf.context } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: playlistLocation, parentNavigationController: strongSelf.navigationController as? NavigationController) strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if case let .messages(chatLocation, _, _) = playlistLocation { let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) self?.present(controller, in: .window(.root)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = MetaDisposable() var progressStarted = false strongSelf.playlistPreloadDisposable?.dispose() strongSelf.playlistPreloadDisposable = (signal |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } |> deliverOnMainQueue).start(next: { index in guard let strongSelf = self else { return } if let _ = index.0 { let controllerContext: AccountContext if account.id == strongSelf.context.account.id { controllerContext = strongSelf.context } else { controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account) } let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: chatLocation, type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: nil, parentNavigationController: strongSelf.navigationController as? NavigationController) strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else if index.1 { if !progressStarted { progressStarted = true progressDisposable.set(progressSignal.start()) } } }, completed: { }) cancelImpl = { self?.playlistPreloadDisposable?.dispose() } } } else { strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) } } } mediaAccessoryPanel.frame = panelFrame if let dismissingPanel = self.dismissingPanel { if let accessoryPanelContainer = self.accessoryPanelContainer { accessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) } else { self.navigationBar?.additionalContentNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) } } else { if let accessoryPanelContainer = self.accessoryPanelContainer { accessoryPanelContainer.addSubnode(mediaAccessoryPanel) } else { self.navigationBar?.additionalContentNode.addSubnode(mediaAccessoryPanel) } } self.mediaAccessoryPanel = (mediaAccessoryPanel, type) mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, isHidden: !self.displayNavigationBar, transition: .immediate) switch order { case .regular: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem) case .reversed: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem) case .random: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) } mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState |> map { state -> MediaPlayerStatus in if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { return state.status } else { return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) } } mediaAccessoryPanel.animateIn(transition: transition) } } else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { self.mediaAccessoryPanel = nil self.dismissingPanel = mediaAccessoryPanel self.audioRateTooltipController?.dismissWithCommitAction() mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in mediaAccessoryPanel?.removeFromSupernode() if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { strongSelf.dismissingPanel = nil } }) } self.suspendNavigationBarLayout = false if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { self.suspendedNavigationBarLayout = suspendedNavigationBarLayout self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) } self.accessoryPanelContainerHeight = additionalHeight } open var keyShortcuts: [KeyShortcut] { return [KeyShortcut(input: UIKeyCommand.inputEscape, action: { [weak self] in if !(self?.navigationController?.topViewController is TabBarController) { _ = self?.navigationBar?.executeBack() } })] } open func joinGroupCall(peerId: PeerId, invite: String?, activeCall: EngineGroupCallDescription) { let context = self.context let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.view.endEditing(true) self.context.joinGroupCall(peerId: peerId, invite: invite, requestJoinAsPeerId: { completion in let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] } let _ = (combineLatest( currentAccountPeer, context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.CallJoinAsPeerId(id: peerId)) ) |> map { currentAccountPeer, availablePeers, callJoinAsPeerId -> ([FoundPeer], EnginePeer.Id?) in var result = currentAccountPeer result.append(contentsOf: availablePeers) return (result, callJoinAsPeerId) } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peers, callJoinAsPeerId in guard let strongSelf = self else { return } let defaultJoinAsPeerId: PeerId? = callJoinAsPeerId if peers.count == 1, let peer = peers.first { completion(peer.peer.id) } else { if let defaultJoinAsPeerId = defaultJoinAsPeerId { completion(defaultJoinAsPeerId) } else { let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var items: [ActionSheetItem] = [] var isGroup = false for peer in peers { if peer.peer is TelegramGroup { isGroup = true break } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { isGroup = true break } } items.append(VoiceChatAccountHeaderActionSheetItem(title: presentationData.strings.VoiceChat_SelectAccount, text: isGroup ? presentationData.strings.VoiceChat_DisplayAsInfoGroup : presentationData.strings.VoiceChat_DisplayAsInfo)) for peer in peers { var subtitle: String? if peer.peer.id.namespace == Namespaces.Peer.CloudUser { subtitle = presentationData.strings.VoiceChat_PersonalAccount } else if let subscribers = peer.subscribers { if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) } else { subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) } } items.append(VoiceChatPeerActionSheetItem(context: context, peer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: { dismissAction() completion(peer.peer.id) })) } controller.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) strongSelf.present(controller, in: .window(.root)) } } }) }, activeCall: activeCall) } }