import Foundation import Display import TelegramCore import SwiftSignalKit import Postbox enum LocationBroadcastPanelSource { case none case summary case peer(PeerId) } private func presentLiveLocationController(account: Account, peerId: PeerId, controller: ViewController) { if let id = account.telegramApplicationContext.liveLocationManager?.internalMessageForPeerId(peerId) { let _ = (account.postbox.modify { modifier -> Message? in return modifier.getMessage(id) } |> deliverOnMainQueue).start(next: { [weak controller] message in if let message = message, let strongController = controller { let _ = openChatMessage(account: account, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, dismissInput: { controller?.view.endEditing(true) }, present: { c, a in controller?.present(c, in: .window(.root), with: a) }, transitionNode: { _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { peer, navigation in }, callPeer: { _ in }, sendSticker: { _ in }, setupTemporaryHiddenMedia: { _, _, _ in }) } }) } } public class TelegramController: ViewController { private let account: Account let enableMediaAccessoryPanel: Bool let locationBroadcastPanelSource: LocationBroadcastPanelSource private var mediaStatusDisposable: Disposable? private var locationBroadcastDisposable: Disposable? private(set) var playlistStateAndType: (SharedMediaPlaylistItem, MusicPlaybackSettingsOrder, MediaManagerPlayerType)? private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? private var locationBroadcastPeers: [Peer]? private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? private var dismissingPanel: ASDisplayNode? override public var navigationHeight: CGFloat { var height = super.navigationHeight if let _ = self.mediaAccessoryPanel { height += 36.0 } if let _ = self.locationBroadcastAccessoryPanel { height += 36.0 } return height } init(account: Account, navigationBarTheme: NavigationBarTheme?, enableMediaAccessoryPanel: Bool, locationBroadcastPanelSource: LocationBroadcastPanelSource) { self.account = account self.enableMediaAccessoryPanel = enableMediaAccessoryPanel self.locationBroadcastPanelSource = locationBroadcastPanelSource super.init(navigationBarTheme: navigationBarTheme) if enableMediaAccessoryPanel { self.mediaStatusDisposable = (account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in if let strongSelf = self, strongSelf.enableMediaAccessoryPanel { if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.0.item) || strongSelf.playlistStateAndType?.1 != playlistStateAndType?.0.order || strongSelf.playlistStateAndType?.2 != playlistStateAndType?.1 { if let playlistStateAndType = playlistStateAndType { strongSelf.playlistStateAndType = (playlistStateAndType.0.item, playlistStateAndType.0.order, playlistStateAndType.1) } else { strongSelf.playlistStateAndType = nil } strongSelf.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } }) } if let liveLocationManager = account.telegramApplicationContext.liveLocationManager { switch locationBroadcastPanelSource { case .none: self.locationBroadcastMode = nil case .summary, .peer: let signal: Signal<[Peer]?, NoError> switch locationBroadcastPanelSource { case let .peer(peerId): self.locationBroadcastMode = .peer signal = liveLocationManager.summaryManager.peersBroadcastingTo(peerId: peerId) default: self.locationBroadcastMode = .summary signal = liveLocationManager.summaryManager.broadcastingToPeers() |> map { $0.isEmpty ? nil : $0 } } self.locationBroadcastDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peers in if let strongSelf = self { var updated = false if let current = strongSelf.locationBroadcastPeers, let peers = peers { updated = !arePeerArraysEqual(current, peers) } else if (strongSelf.locationBroadcastPeers != nil) != (peers != nil) { updated = true } 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 { strongSelf.locationBroadcastAccessoryPanel?.update(peers: peers, mode: locationBroadcastMode) } } } }) } } } deinit { self.mediaStatusDisposable?.dispose() self.locationBroadcastDisposable?.dispose() } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) let navigationHeight = super.navigationHeight var additionalHeight: CGFloat = 0.0 if let locationBroadcastPeers = self.locationBroadcastPeers, let locationBroadcastMode = self.locationBroadcastMode { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight let locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel if let current = self.locationBroadcastAccessoryPanel { locationBroadcastAccessoryPanel = current transition.updateFrame(node: locationBroadcastAccessoryPanel, frame: panelFrame) locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) } else { let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } locationBroadcastAccessoryPanel = LocationBroadcastNavigationAccessoryPanel(theme: presentationData.theme, strings: presentationData.strings, tapAction: { [weak self] in if let strongSelf = self { switch strongSelf.locationBroadcastPanelSource { case .none: break case .summary: if let locationBroadcastPeers = strongSelf.locationBroadcastPeers { if locationBroadcastPeers.count == 1 { presentLiveLocationController(account: strongSelf.account, peerId: locationBroadcastPeers[0].id, controller: strongSelf) } else { let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var items: [ActionSheetItem] = [] if !locationBroadcastPeers.isEmpty { items.append(ActionSheetTextItem(title: presentationData.strings.LiveLocation_MenuChatsCount(Int32(locationBroadcastPeers.count)))) for peer in locationBroadcastPeers { items.append(ActionSheetButtonItem(title: peer.displayTitle, action: { dismissAction() if let strongSelf = self { presentLiveLocationController(account: strongSelf.account, peerId: peer.id, controller: strongSelf) } })) } items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { dismissAction() for peer in locationBroadcastPeers { self?.account.telegramApplicationContext.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(account: strongSelf.account, peerId: peerId, controller: strongSelf) } } }, close: { [weak self] in if let strongSelf = self { var closePeers: [Peer]? var closePeerId: PeerId? 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.account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) 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, action: { dismissAction() if let strongSelf = self { presentLiveLocationController(account: strongSelf.account, peerId: peer.id, controller: strongSelf) } })) } items.append(ActionSheetButtonItem(title: presentationData.strings.LiveLocation_MenuStopAll, color: .destructive, action: { dismissAction() for peer in closePeers { self?.account.telegramApplicationContext.liveLocationManager?.cancelLiveLocation(peerId: peer.id) } })) } else if let closePeerId = closePeerId { items.append(ActionSheetButtonItem(title: presentationData.strings.Map_StopLiveLocation, color: .destructive, action: { dismissAction() self?.account.telegramApplicationContext.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 navigationBar = self.navigationBar { self.displayNode.insertSubnode(locationBroadcastAccessoryPanel, aboveSubnode: navigationBar) } else { self.displayNode.addSubnode(locationBroadcastAccessoryPanel) } self.locationBroadcastAccessoryPanel = locationBroadcastAccessoryPanel locationBroadcastAccessoryPanel.frame = panelFrame locationBroadcastAccessoryPanel.update(peers: locationBroadcastPeers, mode: locationBroadcastMode) locationBroadcastAccessoryPanel.updateLayout(size: panelFrame.size, 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() } } if let (item, _, type) = self.playlistStateAndType { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight.isZero ? -panelHeight : (navigationHeight + additionalHeight + UIScreenPixel)), size: CGSize(width: layout.size.width, height: panelHeight)) if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type { transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame) mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) mediaAccessoryPanel.containerNode.headerNode.playbackItem = item mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> map { state in return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) } } else { if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { self.mediaAccessoryPanel = nil self.dismissingPanel = mediaAccessoryPanel 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(account: self.account) mediaAccessoryPanel.containerNode.headerNode.displayScrubber = type != .voice mediaAccessoryPanel.close = { [weak self] in if let strongSelf = self, let (_, _, type) = strongSelf.playlistStateAndType { strongSelf.account.telegramApplicationContext.mediaManager.setPlaylist(nil, type: type) } } mediaAccessoryPanel.togglePlayPause = { [weak self] in if let strongSelf = self, let (_, _, type) = strongSelf.playlistStateAndType { strongSelf.account.telegramApplicationContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type) } } mediaAccessoryPanel.tapAction = { [weak self] in if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController, let (state, order, type) = strongSelf.playlistStateAndType { if let id = state.id as? PeerMessagesMediaPlaylistItemId { if type == .music { let controller = OverlayPlayerController(account: strongSelf.account, peerId: id.messageId.peerId, type: type, initialMessageId: id.messageId, initialOrder: order, parentNavigationController: strongSelf.navigationController as? NavigationController) strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) } else { navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(id.messageId.peerId), messageId: id.messageId) } } } } mediaAccessoryPanel.frame = panelFrame if let dismissingPanel = self.dismissingPanel { self.displayNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel) } else if let navigationBar = self.navigationBar { self.displayNode.insertSubnode(mediaAccessoryPanel, aboveSubnode: navigationBar) } else { self.displayNode.addSubnode(mediaAccessoryPanel) } self.mediaAccessoryPanel = (mediaAccessoryPanel, type) mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: .immediate) mediaAccessoryPanel.containerNode.headerNode.playbackItem = item mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.account.telegramApplicationContext.mediaManager.globalMediaPlayerState |> map { state in return state?.0.status ?? MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, timestamp: 0.0, seekId: 0, status: .paused) } mediaAccessoryPanel.animateIn(transition: transition) } } else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel { self.mediaAccessoryPanel = nil self.dismissingPanel = mediaAccessoryPanel mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in mediaAccessoryPanel?.removeFromSupernode() if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel { strongSelf.dismissingPanel = nil } }) } } }