Swiftgram/submodules/TelegramBaseController/Sources/TelegramBaseController.swift
Isaac 0c7c73a3e9 Fix call panel
(cherry picked from commit ebdd7b37ed92b0e36a58ed8633c1f3ca1851f1ad)
2025-04-18 01:11:36 +04:00

1069 lines
63 KiB
Swift

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
}, openConferenceCall: { _ 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<PresentationGroupCall?, NoError> = callManager.currentGroupCallSignal
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs == rhs
})
|> map { call -> PresentationGroupCall? in
guard case let .group(call) = call else {
return nil
}
guard call.peerId == peerId && call.account.peerId == context.account.peerId else {
return nil
}
return call
}
let availableGroupCall: Signal<GroupCallPanelData?, NoError>
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
return lhs.0 == rhs.0
})
|> mapToSignal { activeCall, peer -> Signal<GroupCallPanelData?, NoError> 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<PresentationGroupCall?>(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<PresentationData, NoError>) {
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, let groupCallPanelData = strongSelf.groupCallPanelData 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, reference: .id(id: groupCallPanelData.info.id, accessHash: groupCallPanelData.info.accessHash), subscribe: true).startStandalone()
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: presentationData.strings.Chat_ToastSubscribedToScheduledLiveStream_Text,
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<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { 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)
}
}