import Foundation import Display import TelegramCore import SwiftSignalKit import Postbox enum MediaAccessoryPanelVisibility { case none case specific(size: ContainerViewLayoutSizeClass) case always } enum LocationBroadcastPanelSource { case none case summary case peer(PeerId) } private func presentLiveLocationController(account: Account, peerId: PeerId, controller: ViewController) { let presentImpl: (Message?) -> Void = { [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, 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: { _ in }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in}) } } if let id = account.telegramApplicationContext.liveLocationManager?.internalMessageForPeerId(peerId) { let _ = (account.postbox.transaction { transaction -> Message? in return transaction.getMessage(id) } |> deliverOnMainQueue).start(next: presentImpl) } else if let liveLocationManager = account.telegramApplicationContext.liveLocationManager { let _ = (liveLocationManager.summaryManager.peersBroadcastingTo(peerId: peerId) |> take(1) |> map { peersAndMessages -> Message? in return peersAndMessages?.first?.1 } |> deliverOnMainQueue).start(next: presentImpl) } } public class TelegramController: ViewController { private let account: Account let mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility let locationBroadcastPanelSource: LocationBroadcastPanelSource private var mediaStatusDisposable: Disposable? private var locationBroadcastDisposable: Disposable? private(set) var playlistStateAndType: (SharedMediaPlaylistItem, MusicPlaybackSettingsOrder, MediaManagerPlayerType)? var tempVoicePlaylistEnded: (() -> Void)? var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? private var locationBroadcastPeers: [Peer]? private var locationBroadcastMessages: [MessageId: Message]? private var locationBroadcastAccessoryPanel: LocationBroadcastNavigationAccessoryPanel? private var dismissingPanel: ASDisplayNode? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var playlistPreloadDisposable: Disposable? override public var navigationHeight: CGFloat { return super.navigationHeight + self.additionalHeight } override public var navigationInsetHeight: CGFloat { return super.navigationInsetHeight + self.additionalHeight } private var additionalHeight: CGFloat { var height: CGFloat = 0.0 if let _ = self.mediaAccessoryPanel { height += MediaNavigationAccessoryHeaderNode.minimizedHeight } if let _ = self.locationBroadcastAccessoryPanel { height += MediaNavigationAccessoryHeaderNode.minimizedHeight } return height } public var primaryNavigationHeight: CGFloat { return super.navigationHeight } init(account: Account, navigationBarPresentationData: NavigationBarPresentationData?, mediaAccessoryPanelVisibility: MediaAccessoryPanelVisibility, locationBroadcastPanelSource: LocationBroadcastPanelSource) { self.account = account self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } self.mediaAccessoryPanelVisibility = mediaAccessoryPanelVisibility self.locationBroadcastPanelSource = locationBroadcastPanelSource super.init(navigationBarPresentationData: navigationBarPresentationData) if case .none = mediaAccessoryPanelVisibility { } else if let mediaManager = account.telegramApplicationContext.mediaManager { self.mediaStatusDisposable = (mediaManager.globalMediaPlayerState |> mapToSignal { playlistStateAndType -> Signal<(SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in if let (state, type) = playlistStateAndType { switch state { case let .state(state): return .single((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?.0.item) || strongSelf.playlistStateAndType?.1 != playlistStateAndType?.0.order || strongSelf.playlistStateAndType?.2 != playlistStateAndType?.1 { var previousVoiceItem: SharedMediaPlaylistItem? if let playlistStateAndType = strongSelf.playlistStateAndType, playlistStateAndType.2 == .voice { previousVoiceItem = playlistStateAndType.0 } var updatedVoiceItem: SharedMediaPlaylistItem? if let playlistStateAndType = playlistStateAndType, playlistStateAndType.1 == .voice { updatedVoiceItem = playlistStateAndType.0.item } strongSelf.tempVoicePlaylistItemChanged?(previousVoiceItem, updatedVoiceItem) if let playlistStateAndType = playlistStateAndType { strongSelf.playlistStateAndType = (playlistStateAndType.0.item, playlistStateAndType.0.order, playlistStateAndType.1) } else { var voiceEnded = false if strongSelf.playlistStateAndType?.2 == .voice { voiceEnded = true } strongSelf.playlistStateAndType = nil if voiceEnded { strongSelf.tempVoicePlaylistEnded?() } } 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]?, [MessageId: Message]?), 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, nil) } default: self.locationBroadcastMode = .summary signal = liveLocationManager.summaryManager.broadcastingToMessages() |> map { messages -> ([Peer]?, [MessageId: Message]?) in if messages.isEmpty { return (nil, nil) } else { var peers: [Peer] = [] for message in messages.values.sorted(by: { MessageIndex($0) < MessageIndex($1) }) { if let peer = message.peers[message.id.peerId] { peers.append(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 = !arePeerArraysEqual(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 { strongSelf.locationBroadcastAccessoryPanel?.update(peers: peers, mode: locationBroadcastMode) } } } }) } } self.presentationDataDisposable = (account.telegramApplicationContext.presentationData |> 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) } } }) } deinit { self.mediaStatusDisposable?.dispose() self.locationBroadcastDisposable?.dispose() self.presentationDataDisposable?.dispose() self.playlistPreloadDisposable?.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) var navigationHeight = super.navigationHeight if !self.displayNavigationBar { navigationHeight = 0.0 } 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, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) } else { let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } locationBroadcastAccessoryPanel = LocationBroadcastNavigationAccessoryPanel(accountPeerId: self.account.peerId, theme: presentationData.theme, strings: presentationData.strings, 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: { MessageIndex($0) > MessageIndex($1) }) if messages.count == 1 { presentLiveLocationController(account: strongSelf.account, peerId: messages[0].id.peerId, 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 !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(account: strongSelf.account, peer: peer, title: peer.displayTitle, beginTimestamp: beginTimeAndTimeout.0, timeout: beginTimeAndTimeout.1, strings: presentationData.strings, 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() if let locationBroadcastPeers = strongSelf.locationBroadcastPeers { 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, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, 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() } } let mediaAccessoryPanelHidden: Bool switch self.mediaAccessoryPanelVisibility { case .always: mediaAccessoryPanelHidden = false case .none: mediaAccessoryPanelHidden = true case let .specific(size): mediaAccessoryPanelHidden = size != layout.metrics.widthClass } if let (item, _, type) = self.playlistStateAndType, !mediaAccessoryPanelHidden { 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, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) mediaAccessoryPanel.containerNode.headerNode.playbackItem = item if let mediaManager = self.account.telegramApplicationContext.mediaManager { let delayedStatus = mediaManager.globalMediaPlayerState |> mapToSignal { value -> Signal<(SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in guard let value = value else { return .single(nil) } switch value.0 { 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?.0, 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) } } } } 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.toggleRate = { [weak self] in guard let strongSelf = self else { return } let _ = (strongSelf.account.postbox.transaction { transaction -> AudioPlaybackRate in let settings = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.musicPlaybackSettings) as? MusicPlaybackSettings ?? MusicPlaybackSettings.defaultSettings let nextRate: AudioPlaybackRate switch settings.voicePlaybackRate { case .x1: nextRate = .x2 case .x2: nextRate = .x1 } transaction.setPreferencesEntry(key: ApplicationSpecificPreferencesKeys.musicPlaybackSettings, value: settings.withUpdatedVoicePlaybackRate(nextRate)) return nextRate } |> deliverOnMainQueue).start(next: { baseRate in guard let strongSelf = self, let (_, _, type) = strongSelf.playlistStateAndType else { return } strongSelf.account.telegramApplicationContext.mediaManager?.playlistControl(.setBaseRate(baseRate), 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 guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController, let (state, order, type) = strongSelf.playlistStateAndType else { return } if let id = state.id as? PeerMessagesMediaPlaylistItemId { if type == .music { let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(id.messageId), count: 60), account: strongSelf.account, chatLocation: .peer(id.messageId.peerId), fixedCombinedReadStates: nil, tagMask: MessageTags.music, additionalData: []) let signal = historyView |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in switch historyView { case .Loading: return .single((nil, true)) case let .HistoryView(view, _, _, _, _): for entry in view.entries { if case let .MessageEntry(message, _, _, _) = entry { if message.id == id.messageId { return .single((MessageIndex(message), false)) } } } return .single((nil, false)) } } |> take(until: { index in return SignalTakeAction(passthrough: true, complete: !index.1) }) var cancelImpl: (() -> Void)? let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, 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 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 if index.1 { if !progressStarted { progressStarted = true progressDisposable.set(progressSignal.start()) } } }, completed: { }) cancelImpl = { self?.playlistPreloadDisposable?.dispose() } } 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, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate) mediaAccessoryPanel.containerNode.headerNode.playbackItem = item if let mediaManager = self.account.telegramApplicationContext.mediaManager { mediaAccessoryPanel.containerNode.headerNode.playbackStatus = mediaManager.globalMediaPlayerState |> map { state -> MediaPlayerStatus in if let stateOrLoading = state?.0, 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) } } } 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 } }) } } }