Consolidates 137 wave commits + 31 supporting commits (CLAUDE.md bump, typealias additions, AnyObject→EngineMedia restoration) into one squashed commit. Migrates dozens of consumer-side public APIs, struct fields, protocol methods, and enum payloads from Postbox protocols/structs to TelegramEngine engine wrappers and typealiases. Drops `import Postbox` from many files. Adds new TelegramCore typealiases and one TelegramEngineUnauthorized facade. Notable changes by category: **TelegramCore typealias additions** (rule 2 — narrow utility typealiases): - EngineChatListIndex, EngineTempBoxFile, EngineItemCollectionItemIndex, EngineItemCollectionViewEntryIndex, EngineValueBoxEncryptionParameters, EngineMessageAndThreadId, EnginePeerStoryStats, EngineMessageHistoryAnchorIndex, EngineChatListTotalUnreadStateCategory, EngineChatListTotalUnreadStateStats, EnginePeerSummaryCounterTags, EngineChatListTotalUnreadState, EngineItemCacheEntryId, EngineHashFunctions, EngineCachedMediaResourceRepresentationResult, EngineMediaResourceDataFetchResult, EngineMediaResourceDataFetchError, EngineMediaResourceStatus, EngineCachedPeerData **TelegramCore engine extensions/forwarders**: - EngineMessage.engineMedia, EngineMessage.enginePeers, EngineMessage.adAttribute, EngineMessage.effectivelyIncoming - engineFileSize forwarder - TelegramEngine.Resources.clearCachedMediaResources(mediaResourceIds: Set<EngineMediaResource.Id>) - TelegramEngine.Resources.fetchStatus(id:resourceSize:) - TelegramEngineUnauthorized.UnauthorizedResources facade with storeResourceData **Public API/struct migrations to engine types**: - ChatAvailableMessageActions.banAuthor/banAuthors → EnginePeer?/[EnginePeer] - WebSessionsContextState.peers → [EnginePeer.Id: EnginePeer] - CacheUsageStats.peers → [EnginePeer.Id: EnginePeer] - PeerCommand.peer → EnginePeer - PeerInfoControllerMode.calls(messages:) → [EngineMessage] - CallControllerNodeProtocol.updatePeer → EnginePeer params - ChatHistoryListNode.messageInCurrentHistoryView (and 4 variants) → EngineMessage? - ChatHistorySearchContainerNode.messageForGallery → EngineMessage? - PeerInfoPaneNode.findLoadedMessage / ensureMessageIsVisible / transitionNodeForGallery → engine-typed - GalleryHiddenMediaTarget.getTransitionInfo / GalleryHiddenMediaManager.findTarget → engine-typed - ChatPanelInterfaceInteraction.presentReactionDeletionOptions / presentBan*MessageOptions → EnginePeer - DrawingMessageRenderer.messages → [EngineMessage] - ChatVideoGalleryItemScrubberView.setFetchStatusSignal → EngineMediaResource.FetchStatus - ChannelDiscussionGroupActionSheetItem.peer, VoiceChatPeerEntry.peer, VoiceChatFullscreenParticipantItem.peer, MediaStreamComponent.chatPeer, MediaStreamVideoComponent.callPeer, ChatMessageContactBubbleContentNode.contactPeer, ChatMessageForwardInfoNode.peer, ChatMessageCommentFooterContentNode.replyPeers, ChatReportPeerTitlePanelNode.peer, ChatMessageActionUrlAuthController.bot, PeerMediaCollectionInterfaceState.peer, ChatMessageCallBubbleContentNode.peopleAvatars, ChatLoadingNode.renderedPeer (→ EngineRenderedPeer) — all to engine types **Wave-71-shadow stored-field migrations** (Postbox Peer/Message → Engine wrapper): - LegacyCallControllerNode.peer - CallStatusBarNode.currentPeer **Dead-code / dead-field removals**: - CallController.peer, CallControllerNodeV2.account, ContactMultiselectionController PeerNameIndex fields, preparedChatListNodeViewTransition account: Account param, FetchResource.swift entirely (unused function) **Module-level Postbox import drops**: 30+ files including TelegramRootController, EditStories, GiftViewScreen, AnimatedStickerUtils, FetchPhotoLibraryImageResource, PeerInfoGiftsPaneNode, PeerInfoPaneContainerNode, PresentAddMembers, PeerInfoProfileItems, ChatControllerAdminBanUsers, PresentationData typealiases, DefaultDayPresentationTheme, ChatListViewTransition, GalleryHiddenMediaManager, RecentSessionsController, GifContext, AuthorizationSequenceController, PeerInfoHeaderEditingContentNode, PeerInfoHeaderNode, PeerAllowedReactionListController, CallControllerNodeV2, and 6 PeerInfo pane files. **AnyObject restoration**: rule 8 added (never substitute Postbox protocols with Any/AnyObject) — undid previous AnyObject substitutions in waves 141/143 back to EngineMedia. Doc maintenance: CLAUDE.md updated to reflect new typealiases and forwarders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
567 lines
25 KiB
Swift
567 lines
25 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TelegramAudio
|
|
import AccountContext
|
|
import LocalizedPeerData
|
|
import PhotoResources
|
|
import CallsEmoji
|
|
|
|
final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
|
|
private let sharedContext: SharedAccountContext
|
|
private let account: Account
|
|
|
|
private let statusBar: StatusBar
|
|
|
|
private var presentationData: PresentationData
|
|
private var peer: EnginePeer?
|
|
private let debugInfo: Signal<(String, String), NoError>
|
|
private var forceReportRating = false
|
|
private let easyDebugAccess: Bool
|
|
private let call: PresentationCall
|
|
|
|
private let containerNode: ASDisplayNode
|
|
|
|
private let imageNode: TransformImageNode
|
|
private let dimNode: ASDisplayNode
|
|
private let backButtonArrowNode: ASImageNode
|
|
private let backButtonNode: HighlightableButtonNode
|
|
private let statusNode: LegacyCallControllerStatusNode
|
|
private let buttonsNode: LegacyCallControllerButtonsNode
|
|
private var keyPreviewNode: CallControllerKeyPreviewNode?
|
|
|
|
private var debugNode: CallDebugNode?
|
|
|
|
private var keyTextData: (Data, String)?
|
|
private let keyButtonNode: HighlightableButtonNode
|
|
|
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
var isMuted: Bool = false {
|
|
didSet {
|
|
self.buttonsNode.isMuted = self.isMuted
|
|
}
|
|
}
|
|
|
|
private var shouldStayHiddenUntilConnection: Bool = false
|
|
|
|
private var audioOutputState: ([AudioSessionOutput], currentOutput: AudioSessionOutput?)?
|
|
private var callState: PresentationCallState?
|
|
|
|
var toggleMute: (() -> Void)?
|
|
var setCurrentAudioOutput: ((AudioSessionOutput) -> Void)?
|
|
var beginAudioOuputSelection: ((Bool) -> Void)?
|
|
var acceptCall: (() -> Void)?
|
|
var endCall: (() -> Void)?
|
|
var setIsVideoPaused: ((Bool) -> Void)?
|
|
var back: (() -> Void)?
|
|
var presentCallRating: ((CallId, Bool) -> Void)?
|
|
var callEnded: ((Bool) -> Void)?
|
|
var willBeDismissedInteractively: (() -> Void)?
|
|
var dismissedInteractively: (() -> Void)?
|
|
var present: ((ViewController) -> Void)?
|
|
var dismissAllTooltips: (() -> Void)?
|
|
|
|
init(sharedContext: SharedAccountContext, account: Account, presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall) {
|
|
self.sharedContext = sharedContext
|
|
self.account = account
|
|
self.presentationData = presentationData
|
|
self.statusBar = statusBar
|
|
self.debugInfo = debugInfo
|
|
self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection
|
|
self.easyDebugAccess = easyDebugAccess
|
|
self.call = call
|
|
|
|
self.containerNode = ASDisplayNode()
|
|
if self.shouldStayHiddenUntilConnection {
|
|
self.containerNode.alpha = 0.0
|
|
}
|
|
|
|
self.imageNode = TransformImageNode()
|
|
self.imageNode.contentAnimations = [.subsequentUpdates]
|
|
self.dimNode = ASDisplayNode()
|
|
self.dimNode.isUserInteractionEnabled = false
|
|
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
|
|
|
self.backButtonArrowNode = ASImageNode()
|
|
self.backButtonArrowNode.displayWithoutProcessing = true
|
|
self.backButtonArrowNode.displaysAsynchronously = false
|
|
self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white)
|
|
self.backButtonNode = HighlightableButtonNode()
|
|
|
|
self.statusNode = LegacyCallControllerStatusNode()
|
|
|
|
self.buttonsNode = LegacyCallControllerButtonsNode(strings: self.presentationData.strings)
|
|
self.keyButtonNode = HighlightableButtonNode()
|
|
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
return UITracingLayerView()
|
|
})
|
|
|
|
self.containerNode.backgroundColor = .black
|
|
|
|
self.addSubnode(self.containerNode)
|
|
|
|
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
|
|
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
|
|
self.backButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.backButtonNode.alpha = 0.4
|
|
strongSelf.backButtonArrowNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.backButtonNode.alpha = 1.0
|
|
strongSelf.backButtonArrowNode.alpha = 1.0
|
|
strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.containerNode.addSubnode(self.imageNode)
|
|
self.containerNode.addSubnode(self.dimNode)
|
|
self.containerNode.addSubnode(self.statusNode)
|
|
self.containerNode.addSubnode(self.buttonsNode)
|
|
self.containerNode.addSubnode(self.keyButtonNode)
|
|
self.containerNode.addSubnode(self.backButtonArrowNode)
|
|
self.containerNode.addSubnode(self.backButtonNode)
|
|
|
|
self.buttonsNode.mute = { [weak self] in
|
|
self?.toggleMute?()
|
|
}
|
|
|
|
self.buttonsNode.speaker = { [weak self] in
|
|
self?.beginAudioOuputSelection?(false)
|
|
}
|
|
|
|
self.buttonsNode.end = { [weak self] in
|
|
self?.endCall?()
|
|
}
|
|
|
|
self.buttonsNode.accept = { [weak self] in
|
|
self?.acceptCall?()
|
|
}
|
|
|
|
self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
self.view.addGestureRecognizer(panRecognizer)
|
|
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
self.view.addGestureRecognizer(tapRecognizer)
|
|
}
|
|
|
|
func updatePeer(accountPeer: EnginePeer, peer: EnginePeer, hasOther: Bool) {
|
|
if self.peer != peer {
|
|
self.peer = peer
|
|
if let peerReference = PeerReference(peer._asPeer()), !peer.profileImageRepresentations.isEmpty {
|
|
let representations: [ImageRepresentationWithReference] = peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: .avatar(peer: peerReference, resource: $0.resource)) })
|
|
self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.account, representations: representations, immediateThumbnailData: nil, autoFetchFullSize: true))
|
|
self.dimNode.isHidden = false
|
|
} else {
|
|
self.imageNode.setSignal(callDefaultBackground())
|
|
self.dimNode.isHidden = true
|
|
}
|
|
|
|
self.statusNode.title = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
|
|
if hasOther {
|
|
self.statusNode.subtitle = self.presentationData.strings.Call_AnsweringWithAccount(accountPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).string
|
|
|
|
if let callState = callState {
|
|
self.updateCallState(callState)
|
|
}
|
|
}
|
|
|
|
if let (layout, navigationBarHeight) = self.validLayout {
|
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) {
|
|
if self.audioOutputState?.0 != availableOutputs || self.audioOutputState?.1 != currentOutput {
|
|
self.audioOutputState = (availableOutputs, currentOutput)
|
|
self.updateButtonsMode()
|
|
}
|
|
}
|
|
|
|
func updateCallState(_ callState: PresentationCallState) {
|
|
self.callState = callState
|
|
|
|
let statusValue: LegacyCallControllerStatusValue
|
|
var statusReception: Int32?
|
|
|
|
switch callState.state {
|
|
case .waiting, .connecting:
|
|
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
|
|
case let .requesting(ringing):
|
|
if ringing {
|
|
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
|
|
} else {
|
|
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
|
|
}
|
|
case .terminating:
|
|
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
|
case let .terminated(_, reason, _):
|
|
if let reason = reason {
|
|
switch reason {
|
|
case let .ended(type):
|
|
switch type {
|
|
case .busy:
|
|
statusValue = .text(self.presentationData.strings.Call_StatusBusy)
|
|
case .hungUp, .missed, .switchedToConference:
|
|
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
|
}
|
|
case .error:
|
|
statusValue = .text(self.presentationData.strings.Call_StatusFailed)
|
|
}
|
|
} else {
|
|
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
|
|
}
|
|
case .ringing:
|
|
var text = self.presentationData.strings.Call_StatusIncoming
|
|
if !self.statusNode.subtitle.isEmpty {
|
|
text += "\n\(self.statusNode.subtitle)"
|
|
}
|
|
statusValue = .text(text)
|
|
case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash):
|
|
let strings = self.presentationData.strings
|
|
var isReconnecting = false
|
|
if case .reconnecting = callState.state {
|
|
isReconnecting = true
|
|
}
|
|
statusValue = .timer({ value in
|
|
if isReconnecting {
|
|
return strings.Call_StatusConnecting
|
|
} else {
|
|
return strings.Call_StatusOngoing(value).string
|
|
}
|
|
}, timestamp)
|
|
if self.keyTextData?.0 != keyVisualHash {
|
|
let text = stringForEmojiHashOfData(keyVisualHash, 4)!.joined(separator: "")
|
|
self.keyTextData = (keyVisualHash, text)
|
|
|
|
self.keyButtonNode.setAttributedTitle(NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: Font.regular(22.0), NSAttributedString.Key.kern: 2.5 as NSNumber]), for: [])
|
|
|
|
let keyTextSize = self.keyButtonNode.measure(CGSize(width: 200.0, height: 200.0))
|
|
self.keyButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
self.keyButtonNode.frame = CGRect(origin: self.keyButtonNode.frame.origin, size: keyTextSize)
|
|
|
|
if let (layout, navigationBarHeight) = self.validLayout {
|
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
|
}
|
|
}
|
|
statusReception = reception
|
|
}
|
|
switch callState.state {
|
|
case .terminated, .terminating:
|
|
if !self.statusNode.alpha.isEqual(to: 0.5) {
|
|
self.statusNode.alpha = 0.5
|
|
self.buttonsNode.alpha = 0.5
|
|
self.keyButtonNode.alpha = 0.5
|
|
self.backButtonArrowNode.alpha = 0.5
|
|
self.backButtonNode.alpha = 0.5
|
|
|
|
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
|
self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
|
self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
|
|
}
|
|
default:
|
|
if !self.statusNode.alpha.isEqual(to: 1.0) {
|
|
self.statusNode.alpha = 1.0
|
|
self.buttonsNode.alpha = 1.0
|
|
self.keyButtonNode.alpha = 1.0
|
|
self.backButtonArrowNode.alpha = 1.0
|
|
self.backButtonNode.alpha = 1.0
|
|
}
|
|
}
|
|
if self.shouldStayHiddenUntilConnection {
|
|
switch callState.state {
|
|
case .connecting, .active:
|
|
self.containerNode.alpha = 1.0
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
self.statusNode.status = statusValue
|
|
self.statusNode.reception = statusReception
|
|
|
|
self.updateButtonsMode()
|
|
|
|
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
|
|
let presentRating = reportRating || self.forceReportRating
|
|
if presentRating {
|
|
self.presentCallRating?(callId, false)
|
|
}
|
|
self.callEnded?(presentRating)
|
|
}
|
|
}
|
|
|
|
private func updateButtonsMode() {
|
|
guard let callState = self.callState else {
|
|
return
|
|
}
|
|
|
|
switch callState.state {
|
|
case .ringing:
|
|
self.buttonsNode.updateMode(.incoming)
|
|
default:
|
|
var mode: LegacyCallControllerButtonsSpeakerMode = .none
|
|
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
|
|
switch currentOutput {
|
|
case .builtin:
|
|
mode = .builtin
|
|
case .speaker:
|
|
mode = .speaker
|
|
case .headphones:
|
|
mode = .headphones
|
|
case .port:
|
|
mode = .bluetooth
|
|
}
|
|
if availableOutputs.count <= 1 {
|
|
mode = .none
|
|
}
|
|
}
|
|
self.buttonsNode.updateMode(.active(speakerMode: mode))
|
|
}
|
|
}
|
|
|
|
func animateIn() {
|
|
var bounds = self.bounds
|
|
bounds.origin = CGPoint()
|
|
self.bounds = bounds
|
|
self.layer.removeAnimation(forKey: "bounds")
|
|
self.statusBar.layer.removeAnimation(forKey: "opacity")
|
|
self.containerNode.layer.removeAnimation(forKey: "opacity")
|
|
self.containerNode.layer.removeAnimation(forKey: "scale")
|
|
self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
if !self.shouldStayHiddenUntilConnection {
|
|
self.containerNode.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3)
|
|
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
func animateOut(completion: @escaping () -> Void) {
|
|
self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
if !self.shouldStayHiddenUntilConnection || self.containerNode.alpha > 0.0 {
|
|
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
|
completion()
|
|
})
|
|
} else {
|
|
completion()
|
|
}
|
|
}
|
|
|
|
func expandFromPipIfPossible() {
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = (layout, navigationBarHeight)
|
|
|
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
|
|
if let keyPreviewNode = self.keyPreviewNode {
|
|
transition.updateFrame(node: keyPreviewNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
keyPreviewNode.updateLayout(size: layout.size, transition: .immediate)
|
|
}
|
|
|
|
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640.0, height: 640.0).aspectFilled(layout.size), boundingSize: layout.size, intrinsicInsets: UIEdgeInsets())
|
|
let apply = self.imageNode.asyncLayout()(arguments)
|
|
apply()
|
|
|
|
let navigationOffset: CGFloat = max(20.0, layout.safeInsets.top)
|
|
|
|
let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0))
|
|
if let image = self.backButtonArrowNode.image {
|
|
transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 10.0, y: navigationOffset + 11.0), size: image.size))
|
|
}
|
|
transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: navigationOffset + 11.0), size: backSize))
|
|
|
|
var statusOffset: CGFloat
|
|
if layout.metrics.widthClass == .regular && layout.metrics.heightClass == .regular {
|
|
if layout.size.height.isEqual(to: 1366.0) {
|
|
statusOffset = 160.0
|
|
} else {
|
|
statusOffset = 120.0
|
|
}
|
|
} else {
|
|
if layout.size.height.isEqual(to: 736.0) {
|
|
statusOffset = 80.0
|
|
} else if layout.size.width.isEqual(to: 320.0) {
|
|
statusOffset = 60.0
|
|
} else {
|
|
statusOffset = 64.0
|
|
}
|
|
}
|
|
|
|
statusOffset += layout.safeInsets.top
|
|
|
|
let buttonsHeight: CGFloat = 75.0
|
|
let buttonsOffset: CGFloat
|
|
if layout.size.width.isEqual(to: 320.0) {
|
|
if layout.size.height.isEqual(to: 480.0) {
|
|
buttonsOffset = 60.0
|
|
} else {
|
|
buttonsOffset = 73.0
|
|
}
|
|
} else {
|
|
buttonsOffset = 83.0
|
|
}
|
|
|
|
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
|
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
|
|
|
|
self.buttonsNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
|
|
let buttonsOriginY: CGFloat = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
|
|
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
|
|
|
|
let keyTextSize = self.keyButtonNode.frame.size
|
|
transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: navigationOffset + 8.0), size: keyTextSize))
|
|
|
|
if let debugNode = self.debugNode {
|
|
transition.updateFrame(node: debugNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
}
|
|
}
|
|
|
|
@objc func keyPressed() {
|
|
if self.keyPreviewNode == nil, let keyText = self.keyTextData?.1, let peer = self.peer {
|
|
let keyPreviewNode = CallControllerKeyPreviewNode(keyText: keyText, infoText: self.presentationData.strings.Call_EmojiDescription(peer.compactDisplayTitle).string.replacingOccurrences(of: "%%", with: "%"), dismiss: { [weak self] in
|
|
if let _ = self?.keyPreviewNode {
|
|
self?.backPressed()
|
|
}
|
|
})
|
|
|
|
self.containerNode.insertSubnode(keyPreviewNode, belowSubnode: self.statusNode)
|
|
self.keyPreviewNode = keyPreviewNode
|
|
|
|
if let (validLayout, _) = self.validLayout {
|
|
keyPreviewNode.updateLayout(size: validLayout.size, transition: .immediate)
|
|
|
|
self.keyButtonNode.isHidden = true
|
|
keyPreviewNode.animateIn(from: self.keyButtonNode.frame, fromNode: self.keyButtonNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func backPressed() {
|
|
if let keyPreviewNode = self.keyPreviewNode {
|
|
self.keyPreviewNode = nil
|
|
keyPreviewNode.animateOut(to: self.keyButtonNode.frame, toNode: self.keyButtonNode, completion: { [weak self, weak keyPreviewNode] in
|
|
self?.keyButtonNode.isHidden = false
|
|
keyPreviewNode?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
self.back?()
|
|
}
|
|
}
|
|
|
|
private var debugTapCounter: (Double, Int) = (0.0, 0)
|
|
|
|
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if let _ = self.keyPreviewNode {
|
|
self.backPressed()
|
|
} else {
|
|
let point = recognizer.location(in: recognizer.view)
|
|
if self.statusNode.frame.contains(point) {
|
|
if self.easyDebugAccess {
|
|
self.presentDebugNode()
|
|
} else {
|
|
let timestamp = CACurrentMediaTime()
|
|
if self.debugTapCounter.0 < timestamp - 0.75 {
|
|
self.debugTapCounter.0 = timestamp
|
|
self.debugTapCounter.1 = 0
|
|
}
|
|
|
|
if self.debugTapCounter.0 >= timestamp - 0.75 {
|
|
self.debugTapCounter.0 = timestamp
|
|
self.debugTapCounter.1 += 1
|
|
}
|
|
|
|
if self.debugTapCounter.1 >= 10 {
|
|
self.debugTapCounter.1 = 0
|
|
|
|
self.presentDebugNode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func presentDebugNode() {
|
|
guard self.debugNode == nil else {
|
|
return
|
|
}
|
|
|
|
self.forceReportRating = true
|
|
|
|
let debugNode = CallDebugNode(signal: self.debugInfo)
|
|
debugNode.dismiss = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.debugNode?.removeFromSupernode()
|
|
strongSelf.debugNode = nil
|
|
}
|
|
}
|
|
self.addSubnode(debugNode)
|
|
self.debugNode = debugNode
|
|
|
|
if let (layout, navigationBarHeight) = self.validLayout {
|
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .changed:
|
|
let offset = recognizer.translation(in: self.view).y
|
|
var bounds = self.bounds
|
|
bounds.origin.y = -offset
|
|
self.bounds = bounds
|
|
case .ended:
|
|
let velocity = recognizer.velocity(in: self.view).y
|
|
if abs(velocity) < 100.0 {
|
|
var bounds = self.bounds
|
|
let previous = bounds
|
|
bounds.origin = CGPoint()
|
|
self.bounds = bounds
|
|
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
} else {
|
|
var bounds = self.bounds
|
|
let previous = bounds
|
|
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
|
|
self.bounds = bounds
|
|
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
|
self?.dismissedInteractively?()
|
|
})
|
|
}
|
|
case .cancelled:
|
|
var bounds = self.bounds
|
|
let previous = bounds
|
|
bounds.origin = CGPoint()
|
|
self.bounds = bounds
|
|
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|