From 3b1fd7cc6703462941de8399d036b2309896ec54 Mon Sep 17 00:00:00 2001 From: overtake <> Date: Mon, 6 Jul 2020 14:50:13 +0300 Subject: [PATCH 1/4] no message --- submodules/TgVoipWebrtc/Impl/CodecsApple.mm | 14 ++++++++++---- submodules/TgVoipWebrtc/Impl/MediaManager.cpp | 2 +- submodules/TgVoipWebrtc/Impl/TgVoip.mm | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/submodules/TgVoipWebrtc/Impl/CodecsApple.mm b/submodules/TgVoipWebrtc/Impl/CodecsApple.mm index e3fad7c4e8..4a91a491bb 100644 --- a/submodules/TgVoipWebrtc/Impl/CodecsApple.mm +++ b/submodules/TgVoipWebrtc/Impl/CodecsApple.mm @@ -25,7 +25,9 @@ #include "api/video_track_source_proxy.h" #include "sdk/objc/api/RTCVideoRendererAdapter.h" #include "sdk/objc/native/api/video_frame.h" +#if defined(WEBRTC_IOS) #include "sdk/objc/components/audio/RTCAudioSession.h" +#endif #include "api/media_types.h" #import "VideoCameraCapturer.h" @@ -47,6 +49,9 @@ _videoCapturer = [[VideoCameraCapturer alloc] initWithSource:source isActiveUpdated:isActiveUpdated]; + AVCaptureDevice *selectedCamera = nil; + +#if TARGET_OS_IOS AVCaptureDevice *frontCamera = nil; AVCaptureDevice *backCamera = nil; for (AVCaptureDevice *device in [VideoCameraCapturer captureDevices]) { @@ -56,14 +61,15 @@ backCamera = device; } } - - AVCaptureDevice *selectedCamera = nil; if (useFrontCamera && frontCamera != nil) { selectedCamera = frontCamera; } else { selectedCamera = backCamera; } - +#else + selectedCamera = [VideoCameraCapturer captureDevices].firstObject; +#endif + // NSLog(@"%@", selectedCamera); if (selectedCamera == nil) { return nil; } @@ -74,7 +80,7 @@ return width1 < width2 ? NSOrderedAscending : NSOrderedDescending; }]; - AVCaptureDeviceFormat *bestFormat = nil; + AVCaptureDeviceFormat *bestFormat = sortedFormats.firstObject; for (AVCaptureDeviceFormat *format in sortedFormats) { CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription); if (dimensions.width >= 1000 || dimensions.height >= 1000) { diff --git a/submodules/TgVoipWebrtc/Impl/MediaManager.cpp b/submodules/TgVoipWebrtc/Impl/MediaManager.cpp index 5c91342c7f..9e20dc00bf 100644 --- a/submodules/TgVoipWebrtc/Impl/MediaManager.cpp +++ b/submodules/TgVoipWebrtc/Impl/MediaManager.cpp @@ -22,7 +22,7 @@ #include "TgVoip.h" #include "VideoCaptureInterfaceImpl.h" -#if TARGET_OS_IPHONE +#if TARGET_OS_IPHONE || TARGET_OS_OSX #include "CodecsApple.h" diff --git a/submodules/TgVoipWebrtc/Impl/TgVoip.mm b/submodules/TgVoipWebrtc/Impl/TgVoip.mm index 5cfb5d75a4..3e1fd2bb15 100644 --- a/submodules/TgVoipWebrtc/Impl/TgVoip.mm +++ b/submodules/TgVoipWebrtc/Impl/TgVoip.mm @@ -12,7 +12,7 @@ #include "VideoCaptureInterfaceImpl.h" -#if TARGET_OS_IPHONE +#if TARGET_OS_IPHONE || TARGET_OS_OSX #include "CodecsApple.h" From 401e4798fa56f0e4d7977e44d803ecffaaf0b0ee Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Jul 2020 16:35:35 +0400 Subject: [PATCH 2/4] Support autoarchive for groups and channels --- .../Sources/Node/ChatListItemStrings.swift | 8 ++++++- .../Sources/PeerContactSettings.swift | 23 ++++++++++++++----- .../ChatReportPeerTitlePanelNode.swift | 3 +++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index bb90be8f37..6fdca48f9d 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -54,7 +54,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: peer = chatPeer.chatMainPeer } - messageText = messages[0].text + messageText = "" + for message in messages { + if !message.text.isEmpty { + messageText = message.text + break + } + } var textIsReady = false if messages.count > 1 { diff --git a/submodules/TelegramCore/Sources/PeerContactSettings.swift b/submodules/TelegramCore/Sources/PeerContactSettings.swift index 4ee06b509c..787d4cc976 100644 --- a/submodules/TelegramCore/Sources/PeerContactSettings.swift +++ b/submodules/TelegramCore/Sources/PeerContactSettings.swift @@ -39,14 +39,25 @@ public func unarchiveAutomaticallyArchivedPeer(account: Account, peerId: PeerId) let _ = (account.postbox.transaction { transaction -> Void in updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root) transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in - guard let currentData = current as? CachedUserData, let currentStatusSettings = currentData.peerStatusSettings else { + if let currentData = current as? CachedUserData, let currentStatusSettings = currentData.peerStatusSettings { + var statusSettings = currentStatusSettings + statusSettings.flags.remove(.canBlock) + statusSettings.flags.remove(.canReport) + statusSettings.flags.remove(.autoArchived) + return currentData.withUpdatedPeerStatusSettings(statusSettings) + } else if let currentData = current as? CachedGroupData, let currentStatusSettings = currentData.peerStatusSettings { + var statusSettings = currentStatusSettings + statusSettings.flags.remove(.canReport) + statusSettings.flags.remove(.autoArchived) + return currentData.withUpdatedPeerStatusSettings(statusSettings) + } else if let currentData = current as? CachedChannelData, let currentStatusSettings = currentData.peerStatusSettings { + var statusSettings = currentStatusSettings + statusSettings.flags.remove(.canReport) + statusSettings.flags.remove(.autoArchived) + return currentData.withUpdatedPeerStatusSettings(statusSettings) + }else { return current } - var statusSettings = currentStatusSettings - statusSettings.flags.remove(.canBlock) - statusSettings.flags.remove(.canReport) - statusSettings.flags.remove(.autoArchived) - return currentData.withUpdatedPeerStatusSettings(statusSettings) }) } |> deliverOnMainQueue).start() diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index fb4aa5603a..82eee24a16 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -87,6 +87,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport } else if let _ = state.renderedPeer?.chatMainPeer { if let contactStatus = state.contactStatus, contactStatus.canReportIrrelevantLocation, let peerStatusSettings = contactStatus.peerStatusSettings, peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { buttons.append(.reportIrrelevantGeoLocation) + } else if let contactStatus = state.contactStatus, let peerStatusSettings = contactStatus.peerStatusSettings, peerStatusSettings.contains(.autoArchived) { + buttons.append(.reportUserSpam) + buttons.append(.unarchive) } else { buttons.append(.reportSpam) } From f5ca2503da36575eda027fdf9a98517cbbd5ef96 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 6 Jul 2020 18:38:31 +0400 Subject: [PATCH 3/4] Use legacy call screen when performing audio calls --- .../Sources/PresentationCallManager.swift | 1 + .../Sources/CallController.swift | 34 +- .../Sources/CallControllerNode.swift | 2 +- .../Sources/LegacyCallControllerButton.swift | 249 ++++++ .../LegacyCallControllerButtonsNode.swift | 261 ++++++ .../Sources/LegacyCallControllerNode.swift | 787 ++++++++++++++++++ .../Sources/PresentationCall.swift | 2 +- .../Contents.json | 22 + ...42dc3fd7b381de80e554286a94ccd5b8d02154.png | Bin 0 -> 1108 bytes ...42dc3fd7b381de80e554286a94ccd5b8d02154.png | Bin 0 -> 1780 bytes .../Contents.json | 22 + ...42dc3fd7b381de80e554286a94ccd5b8d02154.png | Bin 0 -> 545 bytes ...42dc3fd7b381de80e554286a94ccd5b8d02154.png | Bin 0 -> 844 bytes .../Contents.json | 22 + ...42dc3fd7b381de80e554286a94ccd5b8d02154.png | Bin 0 -> 655 bytes ...c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png | Bin 0 -> 1244 bytes .../Contents.json | 22 + ...42dc3fd7b381de80e554286a94ccd5b8d02154.png | Bin 0 -> 1197 bytes ...c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png | Bin 0 -> 1916 bytes 19 files changed, 1419 insertions(+), 5 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/LegacyCallControllerButton.swift create mode 100644 submodules/TelegramCallsUI/Sources/LegacyCallControllerButtonsNode.swift create mode 100644 submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png create mode 100644 submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index e1e1f40397..5e4dbbc8ea 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -73,6 +73,7 @@ public protocol PresentationCall: class { var internalId: CallSessionInternalId { get } var peerId: PeerId { get } var isOutgoing: Bool { get } + var isVideo: Bool { get } var peer: Peer? { get } var state: Signal { get } diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 52089bd548..3054b55c85 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -14,9 +14,33 @@ import AccountContext import TelegramNotices import AppBundle +protocol CallControllerNodeProtocol: class { + var isMuted: Bool { get set } + + var toggleMute: (() -> Void)? { get set } + var setCurrentAudioOutput: ((AudioSessionOutput) -> Void)? { get set } + var beginAudioOuputSelection: (() -> Void)? { get set } + var acceptCall: (() -> Void)? { get set } + var endCall: (() -> Void)? { get set } + var setIsVideoPaused: ((Bool) -> Void)? { get set } + var back: (() -> Void)? { get set } + var presentCallRating: ((CallId) -> Void)? { get set } + var callEnded: ((Bool) -> Void)? { get set } + var dismissedInteractively: (() -> Void)? { get set } + + func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) + func updateCallState(_ callState: PresentationCallState) + func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) + + func animateIn() + func animateOut(completion: @escaping () -> Void) + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) +} + public final class CallController: ViewController { - private var controllerNode: CallControllerNode { - return self.displayNode as! CallControllerNode + private var controllerNode: CallControllerNodeProtocol { + return self.displayNode as! CallControllerNodeProtocol } private let _ready = Promise(false) @@ -109,7 +133,11 @@ public final class CallController: ViewController { } override public func loadDisplayNode() { - self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) + if self.call.isVideo { + self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) + } else { + self.displayNode = LegacyCallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) + } self.displayNodeDidLoad() self.controllerNode.toggleMute = { [weak self] in diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index e204fea8d9..578cd12511 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -133,7 +133,7 @@ private final class OutgoingVideoNode: ASDisplayNode { } } -final class CallControllerNode: ASDisplayNode { +final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol { private enum VideoNodeCorner { case topLeft case topRight diff --git a/submodules/TelegramCallsUI/Sources/LegacyCallControllerButton.swift b/submodules/TelegramCallsUI/Sources/LegacyCallControllerButton.swift new file mode 100644 index 0000000000..79c82f11bb --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LegacyCallControllerButton.swift @@ -0,0 +1,249 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import AppBundle + +enum LegacyCallControllerButtonType { + case mute + case end + case accept + case speaker + case bluetooth + case switchCamera +} + +private let buttonSize = CGSize(width: 75.0, height: 75.0) + +private func generateEmptyButtonImage(icon: UIImage?, strokeColor: UIColor?, fillColor: UIColor, knockout: Bool = false, angle: CGFloat = 0.0) -> UIImage? { + return generateImage(buttonSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + if let strokeColor = strokeColor { + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.5, y: 1.5), size: CGSize(width: size.width - 3.0, height: size.height - 3.0))) + } else { + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + } + + if let icon = icon { + if !angle.isZero { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: angle) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let imageSize = icon.size + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.width - imageSize.height) / 2.0)), size: imageSize) + if knockout { + context.setBlendMode(.copy) + context.clip(to: imageRect, mask: icon.cgImage!) + context.setFillColor(UIColor.clear.cgColor) + context.fill(imageRect) + } else { + context.setBlendMode(.normal) + context.draw(icon.cgImage!, in: imageRect) + } + } + }) +} + +private func generateFilledButtonImage(color: UIColor, icon: UIImage?, angle: CGFloat = 0.0) -> UIImage? { + return generateImage(buttonSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let icon = icon { + if !angle.isZero { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: angle) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)) + } + }) +} + +private let emptyStroke = UIColor(white: 1.0, alpha: 0.8) +private let emptyHighlightedFill = UIColor(white: 1.0, alpha: 0.3) +private let invertedFill = UIColor(white: 1.0, alpha: 1.0) + +private let labelFont = Font.regular(14.5) + +final class LegacyCallControllerButtonNode: HighlightTrackingButtonNode { + private var type: LegacyCallControllerButtonType + + private var regularImage: UIImage? + private var highlightedImage: UIImage? + private var filledImage: UIImage? + + private let backgroundNode: ASImageNode + private let labelNode: ASTextNode? + + init(type: LegacyCallControllerButtonType, label: String?) { + self.type = type + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = false + self.backgroundNode.displaysAsynchronously = false + + if let label = label { + let labelNode = ASTextNode() + labelNode.attributedText = NSAttributedString(string: label, font: labelFont, textColor: .white) + self.labelNode = labelNode + } else { + self.labelNode = nil + } + + var regularImage: UIImage? + var highlightedImage: UIImage? + var filledImage: UIImage? + + switch type { + case .mute: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .accept: + regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0) + highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0) + case .end: + regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton")) + highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton")) + case .speaker: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .bluetooth: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .switchCamera: + let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white) + regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true) + } + + self.regularImage = regularImage + self.highlightedImage = highlightedImage + self.filledImage = filledImage + + super.init() + + self.addSubnode(self.backgroundNode) + + if let labelNode = self.labelNode { + self.addSubnode(labelNode) + } + + self.backgroundNode.image = regularImage + self.currentImage = regularImage + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + strongSelf.internalHighlighted = highlighted + strongSelf.updateState(highlighted: highlighted, selected: strongSelf.isSelected) + } + } + } + + private var internalHighlighted = false + + override var isSelected: Bool { + didSet { + self.updateState(highlighted: self.internalHighlighted, selected: self.isSelected) + } + } + + private var currentImage: UIImage? + + private func updateState(highlighted: Bool, selected: Bool) { + let image: UIImage? + if selected { + image = self.filledImage + } else if highlighted { + image = self.highlightedImage + } else { + image = self.regularImage + } + + if self.currentImage !== image { + let currentContents = self.backgroundNode.layer.contents + self.backgroundNode.layer.removeAnimation(forKey: "contents") + if let currentContents = currentContents, let image = image { + self.backgroundNode.image = image + self.backgroundNode.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: image === self.currentImage || image === self.filledImage ? 0.25 : 0.15) + } else { + self.backgroundNode.image = image + } + self.currentImage = image + } + } + + func updateType(_ type: LegacyCallControllerButtonType) { + if self.type == type { + return + } + self.type = type + var regularImage: UIImage? + var highlightedImage: UIImage? + var filledImage: UIImage? + + switch type { + case .mute: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .accept: + regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0) + highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0) + case .end: + regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton")) + highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/LegacyCallPhoneButton")) + case .speaker: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/LegacyCallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .bluetooth: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .switchCamera: + let patternImage = generateTintedImage(image: UIImage(bundleImageName: "Call/CallSwitchCameraButton"), color: .white) + regularImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: patternImage, strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: patternImage, strokeColor: nil, fillColor: invertedFill, knockout: true) + } + + self.regularImage = regularImage + self.highlightedImage = highlightedImage + self.filledImage = filledImage + + self.updateState(highlighted: self.isHighlighted, selected: self.isSelected) + } + + func animateRollTransition() { + self.backgroundNode.layer.animate(from: 0.0 as NSNumber, to: (-CGFloat.pi * 5 / 4) as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3, removeOnCompletion: false) + self.labelNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)) + + if let labelNode = self.labelNode { + let labelSize = labelNode.measure(CGSize(width: 200.0, height: 100.0)) + labelNode.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 81.0), size: labelSize) + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/LegacyCallControllerButtonsNode.swift b/submodules/TelegramCallsUI/Sources/LegacyCallControllerButtonsNode.swift new file mode 100644 index 0000000000..59e14b0f1a --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LegacyCallControllerButtonsNode.swift @@ -0,0 +1,261 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import MediaPlayer +import TelegramPresentationData + +enum LegacyCallControllerButtonsSpeakerMode { + case none + case builtin + case speaker + case headphones + case bluetooth +} + +enum LegacyCallControllerButtonsMode: Equatable { + enum VideoState: Equatable { + case notAvailable + case available(Bool) + case active + } + + case active(speakerMode: LegacyCallControllerButtonsSpeakerMode, videoState: VideoState) + case incoming +} + +final class LegacyCallControllerButtonsNode: ASDisplayNode { + private let acceptButton: LegacyCallControllerButtonNode + private let declineButton: LegacyCallControllerButtonNode + + private let muteButton: LegacyCallControllerButtonNode + private let endButton: LegacyCallControllerButtonNode + private let speakerButton: LegacyCallControllerButtonNode + private let swichCameraButton: LegacyCallControllerButtonNode + + private var mode: LegacyCallControllerButtonsMode? + + private var validLayout: CGFloat? + + var isMuted = false { + didSet { + self.muteButton.isSelected = self.isMuted + } + } + + var accept: (() -> Void)? + var mute: (() -> Void)? + var end: (() -> Void)? + var speaker: (() -> Void)? + var toggleVideo: (() -> Void)? + var rotateCamera: (() -> Void)? + + init(strings: PresentationStrings) { + self.acceptButton = LegacyCallControllerButtonNode(type: .accept, label: strings.Call_Accept) + self.acceptButton.alpha = 0.0 + self.declineButton = LegacyCallControllerButtonNode(type: .end, label: strings.Call_Decline) + self.declineButton.alpha = 0.0 + + self.muteButton = LegacyCallControllerButtonNode(type: .mute, label: nil) + self.muteButton.alpha = 0.0 + self.endButton = LegacyCallControllerButtonNode(type: .end, label: nil) + self.endButton.alpha = 0.0 + self.speakerButton = LegacyCallControllerButtonNode(type: .speaker, label: nil) + self.speakerButton.alpha = 0.0 + self.swichCameraButton = LegacyCallControllerButtonNode(type: .switchCamera, label: nil) + self.swichCameraButton.alpha = 0.0 + + super.init() + + self.addSubnode(self.acceptButton) + self.addSubnode(self.declineButton) + self.addSubnode(self.muteButton) + self.addSubnode(self.endButton) + self.addSubnode(self.speakerButton) + self.addSubnode(self.swichCameraButton) + + self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.swichCameraButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + } + + func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) { + let previousLayout = self.validLayout + self.validLayout = constrainedWidth + + if let mode = self.mode, previousLayout != self.validLayout { + self.updateButtonsLayout(mode: mode, width: constrainedWidth, animated: false) + } + } + + func updateMode(_ mode: LegacyCallControllerButtonsMode) { + if self.mode != mode { + let previousMode = self.mode + self.mode = mode + if let validLayout = self.validLayout { + self.updateButtonsLayout(mode: mode, width: validLayout, animated: previousMode != nil) + } + } + } + + private func updateButtonsLayout(mode: LegacyCallControllerButtonsMode, width: CGFloat, animated: Bool) { + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + let threeButtonSpacing: CGFloat = 28.0 + let twoButtonSpacing: CGFloat = 105.0 + let buttonSize = CGSize(width: 75.0, height: 75.0) + + let threeButtonsWidth = 3.0 * buttonSize.width + 2.0 * threeButtonSpacing + let twoButtonsWidth = 2.0 * buttonSize.width + 1.0 * twoButtonSpacing + + var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0) + + for button in [self.muteButton, self.endButton, self.speakerButton] { + transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize)) + if button === self.speakerButton { + transition.updateFrame(node: self.swichCameraButton, frame: CGRect(origin: origin, size: buttonSize)) + } + + origin.x += buttonSize.width + threeButtonSpacing + } + + origin = CGPoint(x: floor((width - twoButtonsWidth) / 2.0), y: 0.0) + for button in [self.declineButton, self.acceptButton] { + transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize)) + origin.x += buttonSize.width + twoButtonSpacing + } + + switch mode { + case .incoming: + for button in [self.declineButton, self.acceptButton] { + button.alpha = 1.0 + } + for button in [self.muteButton, self.endButton, self.speakerButton, self.swichCameraButton] { + button.alpha = 0.0 + } + case let .active(speakerMode, videoState): + for button in [self.muteButton] { + if animated && button.alpha.isZero { + button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + button.alpha = 1.0 + } + switch videoState { + case .active, .available: + for button in [self.speakerButton] { + if animated && !button.alpha.isZero { + button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + button.alpha = 0.0 + } + for button in [self.swichCameraButton] { + if animated && button.alpha.isZero { + button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + button.alpha = 1.0 + } + case .notAvailable: + for button in [self.swichCameraButton] { + if animated && !button.alpha.isZero { + button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + button.alpha = 0.0 + } + for button in [self.speakerButton] { + if animated && button.alpha.isZero { + button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + button.alpha = 1.0 + } + } + var animatingAcceptButton = false + if self.endButton.alpha.isZero { + if animated { + if !self.acceptButton.alpha.isZero { + animatingAcceptButton = true + self.endButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.acceptButton.animateRollTransition() + self.endButton.layer.animate(from: (CGFloat.pi * 5 / 4) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3) + self.acceptButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.acceptButton.alpha = 0.0 + strongSelf.acceptButton.layer.removeAnimation(forKey: "position") + strongSelf.acceptButton.layer.removeAnimation(forKey: "transform.rotation.z") + } + }) + } + self.endButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + self.endButton.alpha = 1.0 + } + + if !self.declineButton.alpha.isZero { + if animated { + self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + self.declineButton.alpha = 0.0 + } + + if self.acceptButton.alpha.isZero && !animatingAcceptButton { + self.acceptButton.alpha = 0.0 + } + + self.speakerButton.isSelected = speakerMode == .speaker + self.speakerButton.isHidden = speakerMode == .none + let speakerButtonType: LegacyCallControllerButtonType + switch speakerMode { + case .none, .builtin, .speaker: + speakerButtonType = .speaker + case .headphones: + speakerButtonType = .bluetooth + case .bluetooth: + speakerButtonType = .bluetooth + } + self.speakerButton.updateType(speakerButtonType) + } + } + + @objc func buttonPressed(_ button: LegacyCallControllerButtonNode) { + if button === self.muteButton { + self.mute?() + } else if button === self.endButton || button === self.declineButton { + self.end?() + } else if button === self.speakerButton { + self.speaker?() + } else if button === self.acceptButton { + self.accept?() + } else if button === self.swichCameraButton { + self.rotateCamera?() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let buttons = [ + self.acceptButton, + self.declineButton, + self.muteButton, + self.endButton, + self.speakerButton, + self.swichCameraButton + ] + for button in buttons { + if button.isHidden || button.alpha.isZero { + continue + } + if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } +} diff --git a/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift b/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift new file mode 100644 index 0000000000..e742741dd2 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/LegacyCallControllerNode.swift @@ -0,0 +1,787 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import TelegramAudio +import AccountContext +import LocalizedPeerData +import PhotoResources +import CallsEmoji + +private final class IncomingVideoNode: ASDisplayNode { + private let videoView: UIView + private var effectView: UIVisualEffectView? + private var isBlurred: Bool = false + + init(videoView: UIView) { + self.videoView = videoView + + super.init() + + self.view.addSubview(self.videoView) + } + + func updateLayout(size: CGSize) { + self.videoView.frame = CGRect(origin: CGPoint(), size: size) + } + + func updateIsBlurred(isBlurred: Bool) { + if self.isBlurred == isBlurred { + return + } + self.isBlurred = isBlurred + + if isBlurred { + if self.effectView == nil { + let effectView = UIVisualEffectView() + self.effectView = effectView + effectView.frame = self.videoView.frame + self.view.addSubview(effectView) + } + UIView.animate(withDuration: 0.3, animations: { + self.effectView?.effect = UIBlurEffect(style: .dark) + }) + } else if let effectView = self.effectView { + UIView.animate(withDuration: 0.3, animations: { + effectView.effect = nil + }) + } + } +} + +private final class OutgoingVideoNode: ASDisplayNode { + private let videoView: UIView + private let switchCameraButton: HighlightableButtonNode + private let switchCamera: () -> Void + + init(videoView: UIView, switchCamera: @escaping () -> Void) { + self.videoView = videoView + self.switchCameraButton = HighlightableButtonNode() + self.switchCamera = switchCamera + + super.init() + + self.view.addSubview(self.videoView) + self.addSubnode(self.switchCameraButton) + self.switchCameraButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.switchCamera() + } + + func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: self.videoView, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateCornerRadius(layer: self.videoView.layer, cornerRadius: isExpanded ? 0.0 : 16.0) + self.switchCameraButton.frame = CGRect(origin: CGPoint(), size: size) + } +} + +final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol { + private let sharedContext: SharedAccountContext + private let account: Account + + private let statusBar: StatusBar + + private var presentationData: PresentationData + private var peer: Peer? + 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 var incomingVideoNode: IncomingVideoNode? + private var incomingVideoViewRequested: Bool = false + private var outgoingVideoNode: OutgoingVideoNode? + private var outgoingVideoViewRequested: Bool = false + private let backButtonArrowNode: ASImageNode + private let backButtonNode: HighlightableButtonNode + private let statusNode: CallControllerStatusNode + private let videoPausedNode: ImmediateTextNode + 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: (() -> Void)? + var acceptCall: (() -> Void)? + var endCall: (() -> Void)? + var toggleVideo: (() -> Void)? + var back: (() -> Void)? + var presentCallRating: ((CallId) -> Void)? + var callEnded: ((Bool) -> Void)? + var dismissedInteractively: (() -> Void)? + var setIsVideoPaused: ((Bool) -> 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 = CallControllerStatusNode() + + self.videoPausedNode = ImmediateTextNode() + self.videoPausedNode.alpha = 0.0 + + 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.videoPausedNode) + 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?() + } + + self.buttonsNode.end = { [weak self] in + self?.endCall?() + } + + self.buttonsNode.accept = { [weak self] in + self?.acceptCall?() + } + + self.buttonsNode.toggleVideo = { [weak self] in + self?.toggleVideo?() + } + + self.buttonsNode.rotateCamera = { [weak self] in + self?.call.switchVideoCamera() + } + + 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: Peer, peer: Peer, hasOther: Bool) { + if !arePeersEqual(self.peer, peer) { + self.peer = peer + if let peerReference = PeerReference(peer), !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, 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)).0 + + if let callState = callState { + self.updateCallState(callState) + } + } + + self.videoPausedNode.attributedText = NSAttributedString(string: self.presentationData.strings.Call_RemoteVideoPaused(peer.compactDisplayTitle).0, font: Font.regular(17.0), textColor: .white) + + 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: CallControllerStatusValue + var statusReception: Int32? + + switch callState.videoState { + case .active: + if !self.incomingVideoViewRequested { + self.incomingVideoViewRequested = true + self.call.makeIncomingVideoView(completion: { [weak self] incomingVideoView in + guard let strongSelf = self else { + return + } + if let incomingVideoView = incomingVideoView { + strongSelf.setCurrentAudioOutput?(.speaker) + let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView) + strongSelf.incomingVideoNode = incomingVideoNode + strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode) + strongSelf.statusNode.isHidden = true + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + }) + } + if !self.outgoingVideoViewRequested { + self.outgoingVideoViewRequested = true + self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in + guard let strongSelf = self else { + return + } + if let outgoingVideoView = outgoingVideoView { + outgoingVideoView.backgroundColor = .black + outgoingVideoView.clipsToBounds = true + strongSelf.setCurrentAudioOutput?(.speaker) + let outgoingVideoNode = OutgoingVideoNode(videoView: outgoingVideoView, switchCamera: { + guard let strongSelf = self else { + return + } + strongSelf.call.switchVideoCamera() + }) + strongSelf.outgoingVideoNode = outgoingVideoNode + if let incomingVideoNode = strongSelf.incomingVideoNode { + strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: incomingVideoNode) + } else { + strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode) + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + }) + } + case .activeOutgoing: + if !self.outgoingVideoViewRequested { + self.outgoingVideoViewRequested = true + self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in + guard let strongSelf = self else { + return + } + if let outgoingVideoView = outgoingVideoView { + outgoingVideoView.backgroundColor = .black + outgoingVideoView.clipsToBounds = true + outgoingVideoView.layer.cornerRadius = 16.0 + strongSelf.setCurrentAudioOutput?(.speaker) + let outgoingVideoNode = OutgoingVideoNode(videoView: outgoingVideoView, switchCamera: { + guard let strongSelf = self else { + return + } + strongSelf.call.switchVideoCamera() + }) + strongSelf.outgoingVideoNode = outgoingVideoNode + if let incomingVideoNode = strongSelf.incomingVideoNode { + strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: incomingVideoNode) + } else { + strongSelf.containerNode.insertSubnode(outgoingVideoNode, aboveSubnode: strongSelf.dimNode) + } + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + }) + } + default: + break + } + + if let incomingVideoNode = self.incomingVideoNode { + let isActive: Bool + switch callState.remoteVideoState { + case .inactive: + isActive = false + case .active: + isActive = true + } + incomingVideoNode.updateIsBlurred(isBlurred: !isActive) + if isActive != self.videoPausedNode.alpha.isZero { + if isActive { + self.videoPausedNode.alpha = 0.0 + self.videoPausedNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } else { + self.videoPausedNode.alpha = 1.0 + self.videoPausedNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + } + + 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: + 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).0 + } + }, timestamp) + if self.keyTextData?.0 != keyVisualHash { + let text = stringForEmojiHashOfData(keyVisualHash, 4)! + 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) + } + 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 + } + } + let mappedVideoState: LegacyCallControllerButtonsMode.VideoState + switch callState.videoState { + case .notAvailable: + mappedVideoState = .notAvailable + case .available: + mappedVideoState = .available(true) + case .active: + mappedVideoState = .active + case .activeOutgoing: + mappedVideoState = .active + } + self.buttonsNode.updateMode(.active(speakerMode: mode, videoState: mappedVideoState)) + } + } + + 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 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))) + + let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: 100.0)) + transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - videoPausedSize.width) / 2.0), y: floor((layout.size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize)) + + 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))) + + var outgoingVideoTransition = transition + if let incomingVideoNode = self.incomingVideoNode { + if incomingVideoNode.frame.width.isZero, let outgoingVideoNode = self.outgoingVideoNode, !outgoingVideoNode.frame.width.isZero, !transition.isAnimated { + outgoingVideoTransition = .animated(duration: 0.3, curve: .easeInOut) + } + incomingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size) + incomingVideoNode.updateLayout(size: layout.size) + } + if let outgoingVideoNode = self.outgoingVideoNode { + if self.incomingVideoNode == nil { + outgoingVideoNode.frame = CGRect(origin: CGPoint(), size: layout.size) + outgoingVideoNode.updateLayout(size: layout.size, isExpanded: true, transition: transition) + } else { + let outgoingSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0)) + let outgoingFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - outgoingSize.width, y: buttonsOriginY - 32.0 - outgoingSize.height), size: outgoingSize) + outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: outgoingFrame) + outgoingVideoNode.updateLayout(size: outgoingFrame.size, isExpanded: false, transition: outgoingVideoTransition) + } + } + + 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).0.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 + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 53fdce80a0..3c95060bb6 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -167,7 +167,7 @@ public final class PresentationCallImpl: PresentationCall { public let internalId: CallSessionInternalId public let peerId: PeerId public let isOutgoing: Bool - private var isVideo: Bool + public var isVideo: Bool public let peer: Peer? private let serializedData: String? diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/Contents.json new file mode 100644 index 0000000000..96ef62f88c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png new file mode 100644 index 0000000000000000000000000000000000000000..07403f47ecd49cbb454aa5471d91618e438ef1be GIT binary patch literal 1108 zcmV-a1grarP)=llKVIp?1H<~--#d%mB~ z2|_kEs>y4mkh!0qV@J%e1|7N$7&dG`w+;=Z!8hT$`l2C=bQv{1Ms-<~93K3RZ^sST z;)b{wvL!iusbQm5<@LO3%w#d9H7Pv!2uJf~J!+~Ln{4)l&Dk#C>1eHQND@QwYd^l@ zSqzvdct_IcN(!%V3?B*_jU=vwW&xl6Bu5{tUmIyw@5y4CYX~H=u znCmj06l_Zu&L+W&IBUA#Xu9yf)u?n5?+I2F?KJ-jPP(4dO;U{64bkv zCY(Kjg>GS;pgmo9M!Ua@lPcBtCpml=wFRx>I4YR#EMAI?&UjH&o8QX8ub?j58i?6L zwh5lVWpf1wi{xA#t`x(2$+Ilov8r&U;0QhwEQ$VMPKd5XZ!!GmXa~$>N9^XhE7I=w ztPt!TC!t%gzZl+z&OGNCSCDhs{cvP-^2L8A+KS;1WVV)6I2d%tNa%+KD_guaQ(+>S`T<*Z_y3(aZww+jkePoc%nzckAr zHs|%U#^PHe?S9jO?C8xHG%Kz^Z%2RrWm{qvC#T(i$WE;v+vBT$l@r*P=wRUmL4(^^ zD+u=CgsRD;#WF*9zvxKhO@B;yB(f97RjPK{kY!WpmS!Up;Nvo*-JcgUX-*=Fo-u-b z1#2IlTXD&p}mFP8r}8jz&hO4DgJ0AGWkS@QijpST1lvdnImiQ^&9&o;a=~kCEPYu@ a%>Muw{R##@!nmmb00009LK-&ZJL`nGjXDmg(MTfOcb1kLV~2LRij1=YgyTQgqzWexkalr(z0A8Eh@6o z71c5`6&DJ|6u~f;mMEgg5H;~-zD%c|{y5LQJLkThd(S+_e)q4Pd+)Q)ea`co-}m|b zzEVVRqppXbA61-Vs_V~ZQ%E@#ETn>R3d!z1lw|q$)lZ0GQ6Smv13Jc{sqY8i5~C%F#{+uM8JH~qtdk^qurqCxMcz@yaRq=PNn&c+DAnDe z?Un8<^@S2pNjxI;t-uaqnxzP(Yy&j8(B8d=`#wfkRHPXO3U zZ=2}!83o>uB!+MbfccU{o)MisqnsyCl6Vn-Qw*_*PM=ZWJ4vE9dja^=6wM9=fdbPc ziJ1UgVS-I``i`=To|42*0Bm)L&hA99zLO9Io{=O*nQGiaMs#+jiW2L8rHXQb97&=U zfFty`iB6wUpx)GM+W>gWDm)v}=`%_vV09+*BCOUmafo4+_+Xldo4$*QGY!jV6qrfUj zq7VBZN_6^+g4H=r9+d`=wZtm~4K^berhxiusOFv#fSTd=yK8I9dagyFl4*b&mKlhUko0 zwnax*xl6jL!G0Sjp!U? z6P+b~O4raC_al2h~MzwVIxN<5`Y_#gcH2o|8n9k8R zcF}1@m}nGUvD@OtTGNcmjxlGIE_TxbN9SmsEs0pCgSGDKEcxc3wL0dzc}cxa^(Hqj zblR0z4ZvyZ!0#5O)C~K!7YA6lbF|d_bu>ENx`Fv9xtoA&rr3OOQ)1lE`wZfku;{E$ z$E(qlcxF(-_22HR|4R;TYM2zQPw(b*&2Vfuz=W?%Hyc0J0nNV`(HmYRA4 zbA{!RPvGR924hz`(Iq;);goXP;GCi+GEpYzPS%|5)QkPcbE)HjD&C( z+bQWvdVFgao!tXeKyB0^G&1&58!3#!cBQ%M-XNDU>S&@he7tC*l_u&aizlAVfy0Q- zYZ6In0=BJf>`IFRN0E5IZdVEv#c5Xx8YN^`njJWb#K(5KQm`l?yV8QdQ6!$X+m(Vv z33ZM>4;&@Zt`sPW#1D46Qm`l?yVApfql6FfZO+ncFestU(K~^oge!l>t`sOr*siqZ zdO&g5mE3)$D&fnDgrA8!a)aHjR{GfOU* zr987zw$;3{eYDn+$MXOE|NlJ>|Ia6)Xi`vOx(Zb)RjN{9x{{bEO4M3qr)Fnd*WtBZ zgZ?zA*DH@))2`VLtJK6;GQ|qJoOVy2T=luD%}#ZuMB!Lwzsq_RQjbfTEQ$Q6cTA_T z^h~R|NGD^T$Hq&Cy~-n7=4sVqynWK5I{rtwqduD`{f?LsqqD_(6YZ@H@h>vYnJn7P zjNe(PQ||hG(B+By?zrQDC%SylFL%$>#xLf3C(R2d?Y2gpYGpABWvZ>vXpd96q+Rr$dgVf>ces=PDP%akz#SkNU=HcWV`GQnh_CR%GOtz$dfsK$ky{1 zqFuJyq5zzit+p6qy=;7QR(TYF*)AK9jkPhvG#`y_K!+37$wUFEwBB(K{V=+2r7=va zzkPOFi$=3z44G-QX1DbFTXT$1>8%lOoY!Qf(ilfdHE43dJ0o716~9>EphGsQi3z6G jCWjp`KZ=TqipIlF%r|exLzJJq00000NkvXXu0mjfK5__K literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png new file mode 100644 index 0000000000000000000000000000000000000000..8237f34688c33f8d6e31e8ca8bc6db055bb6aabc GIT binary patch literal 844 zcmV-S1GD^zP)bEGeX-P4GrY%EHu(utHs8TPo6u z3^kH2N>L)z3KX=%MRZV!E;^}Y>eIy#tES@ae6t6c=X&PlefOXJ=Kub0yu5UDbaci5 zKSGHk2?Ob5l0!a4lwih8DW#Y(Q%n*0QrW;VLh$vRfIci>6Gthef+ji{z!3)M zppknNaD(hD z%3EB?Yl=xBQ0u6{9H5e3+=U+Qv!9vT)Y}v)_=7vr$6dAx)^_Gnz-Qc@F7gOdA2ya7 zZo00cueFuMj)#q-22Z4#rS^vn!NMS($^d5O*i)dg>BiIfLDqx{8PPN|S$M|6@xywP zq0#oxRdka+-VUmQ$w)Z?qs5)hQ}KUM8&4?X5@#@xhJj=fSxGE0L=#O6v8*DIWDKNX z;w+abrM?&JisU(Bd%u>0DUBfXR&?n@oYDAOCX%X2$E)p!drJYTY z(Caa4sBDzbFS$>Xmc=OI?HBWGl2#GP>zZ|G77>3Xwi64PSJ5KP0!NBG*5C10ML|CBDG`skir7ODvE`Zuju-J*p@=mKNPX4zFclxZ3An6R#9j(W zRcd~8SR+!o(xO}WgNwf^{ng?Dl;PRFtm8V+hi}vFX`~`LkIf~RC)k0RM z3;?QC``oDrMW_}^a!O$J3-j-si#O_uJ>E_UJcU=Pe74%8)aGxTPI4Mk3v6 z7$3%NuFx`p_Azbvje&HIabYiYoM)T5Gb+u4&tMC=)QvgHeb3a9x1@}ibe~XP|X7Aj)vwI(>+kc1o?*8}8oO2e^k3|enBJ;S4sIW9^ z%qT+Kr;shCmnuRW&HDH3L5K;uvj*Kli0kQYx{(pKTFP=J5drJl+h`wOV!nNc_{ADa zJA)9n{SjS^AQZFW9zwjQXlM)hi3nKh2kGMtYJpkC4}>^vspk=5*axzgiCKtZ*9{=V zl$Be(%pj_C`I+U$6q`OjL5Rzi_6Q+%`I+Uy3=weFQZ^AFezU~T4kN_1Xh_*G%Y~Uj z1nji5F@!j7C?)EM@6l!jA&8ARh7jLaU?sf}FRgWN5n`2(SuRY0rwB1DduPLH8o zL5TfCY}pC}8wd~;=7-MD!O+{e&^hM+yVzzZr$X7b86ifKV2&WfMMK$+5MzeYjS#Pr zU{)inFc;~_%y$9Fc(=EX(1^9HKX+gCj>C1*Q$ z4k0f2b)9qOMMuM&4@sKaIrF09V`Qs-r@e`w%%|AFV~8zj3?A*qO@z3SEo65dLt#%o pBW`10e~|USp3~nCi`mf(`3JSRpV?ng&gB3A002ovPDHLkV1i)&EjR!G literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png new file mode 100644 index 0000000000000000000000000000000000000000..0026e6063dbcb732c02d6b26c4aad56e141f66fd GIT binary patch literal 1244 zcmV<21S9*2P)_x9Ke6q&0*WBsU@6FNIN2Caio(93|2liIRz! zlu#mTVHvGPRuC1nN;AUxFy_mAX};t(@4pZCf6r~(|2aG7-g~^~L(jhL_dL(}?VRW3 z|NNi9|HUj7?4*Nbaa3E3AP5h{Pi>B>bx|8PwFJ#7_Z`FHCO3+9m0J_Pk9sq`2tqHD z<0sds@9lev;-*%{ewBN)FalLfaI(8pt%K5YFE)=Jga#+yp>j{=>w%Z>3RQt>wjc;e zW;poe^dJZaD9eY~y>ueX3{)5Lg3aSQ|(y1-xJ8HbId`7lfLwJU6Pb|0^+^;PTa)HV3P`*7ou`zu1AFXg| z33h3l2FD-$2ZGQOaBB4_bsXs@7Df$fbRHgKgvp=HZs{%OtpQq=S{*H{qQue5dTnKg zkK>i@3?nv~eWqA{wMxz)2y2~_A;sGnYVtM&;UC9n98ii_(r4iNtM%&d>m9vqQGTCE zUsPHbL~N|C;+m}|*ht@I(E=qzc9Ps!{U(Wp&Qy2GT;DZR~L zqm^xnN-aT$Zb?>%GfUepG~1p+5I&De4K^#k!eA4Xtu|ZkK@j#vr?yD>EQ6h|Y>V0Q zTLj^+=+vewf5N0m1fj`ftvVDUQ@h4DHamtOw3w_(cZu2#Ovq^Xsy)np%N%gEHy z%g6IszO@L|Zq0hEHR~=Knc70-_ZX~H**iWP{Nd=--c){-!D^K?`UFkCN2m5Pf^ghS zOx@{wJUM|N?2byUMrpH2mnfZX<|$KbTU2Vxl`inj3tj9Jc{D~7ubrps))_AHS=IKL zY_|6MNakdiPqn8rfsM~TvDu_AE1jCk7gn1;wY-;=T9q!SNhX>Pe?eQ$HR+dnt|%Yu zMD?H^L4vC#_)y7KN1tuFM^+fDn3D)XTaa_;1ilHSmOio^q0;xH`LRi_SGq3P>3t|G zeJ0Dye&KV^AooshE1zxBWxmubH}PjcwN!3#y)zGfT61i6!sZ7ZX(PiVWGZ+!&V)>xXKJF03nF)mQePjpwk+c~|uHT0}F*Ef3$ zNIC;m)KNYG?LsHFR>Bc;{95@ z#1d>*v0n=D4*~u+g(HvAXVM~YTt#`Ty00000&W%0&^xyGRf)8cQf`57)@&!(OM_o;AC(ce``8Yd@Xy@7e3iWM<7; zYmCW~@f>i1J|u4hYM%<)_>OYci7t+Bz6>0u(4r0zu;U#Cww&+Dadg~ z^s4Y!>mfLpF>I1CGc5XH02;U}4Ag!62qD4xN;XkPk*ZIZZ@v^9Ydp80EigLM$uhoN zM$K2%d$IHPyJdov$V$0Lf$xGCtg7DDs{Qwm z1i2(zKgN*9HV6V{3@?iIuIhTIsPj~{vc)Q67PqOVF5~%=s}K~-81_g+x?6SrL#tW& zuQo%zJIwppKv>hkMv7XlI$r|7HHy{M>{(ePV8(C|fTOB5S;mg@iDe6-0!v5nf$Hhu zh3U5yEr$yLY#MK*09L8m9(x7O#{it>fvCV% z+Y1b;0r*KfQ>g;r0z;z$dkcVHv=T)2WgSUHTLHioMnnZxCeFJn+B^UnwWFNHqK%FU z>;(z3Jrr#&04XgcWT9wdqn@#3s2OYGiKxJ418_!NV-)~g%FY^FI6v)0J}otW!sNiRJtlICEiC?Mq8D551Fq`ZS?hDQ`Kpr zP6*b!SmQKfWx!|JDQJu_oCKhrUOrJj6}S=AT*~?{{)DY%+}ydSuyp*rDt2p_{#c1r zF=6^+y`*o^U`ybgS$eM{OJ#>TK+z$V4(zVa!-s)-j?K4%4NM7+S#%YI$F zu+sVeMg3UM7S^EV=JU9C%~$%eqB34sc~YGnHE}aafiv_s|2O*wPpc1$szc8Z00000 LNkvXXu0mjfX_+vg literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png new file mode 100644 index 0000000000000000000000000000000000000000..345c9a8f3b0aa529535a48be49ad4fe55c10a1c4 GIT binary patch literal 1916 zcmV-?2ZQ*DP)FqbLM4fG8y@M7AhVM2toR zktJ+`vIGRw55ZrVHNQLees}K7oH^&rQ>qLa z35;Vk8Pr$)HKy^O9j3Nl5tBK?ZTpgVe?lg40>`HKwJ#?N$Xj&rCuBTd2+Si?^(SU6 zei4`p{rbsv<(7ubr>9>rr;ENkOo~4t%d=Zx&hTqr-p>sUd4;Ba#hfMj@-x3ergBV7 zKV8e#!#ATH30BI%qAxcSS9T_pgi{oXLz}U1x^tN8Ug=3w=5T z`id=)LTt=dASc+Bi`k-Q=2%^^T3pdR5{Ugc3&2HEgKVLvc;o{5TU_xV|L7}Lh2L?0 zmAt^svV(r@D=?Rm%>|Ma+Edfw z4wX0}+N*2yvm9srD{l}RvKfMbq&=AwaEiVbcNrmCoN83h*V;=Sgq`V476b!H0^jHd zX=U-X{G?kvZkl6xMBINssD8~95kx92WF>ynngdie-fg^SbErXEMXavxhJsj=&#|$R zAma;NZ}d*6!fCBuZ&W*NAGF(#7Violflwfo;vRn2syrSr-gAyt-{4+@_8G0c&fUQw zPT(qp3aJ$R#jat8>5bDyJN+=(pzTP$);?Bxz!a8H8gpGISs;{;8~6S@Z8ybK78$RZ z@|&N{M%Ekt)>(h1c91nVu9e@L_W!)7wYl77(0*NvbH@t8V+N0hu|nI&@ZhcaTWg0| zkKkh@-TvpLC_yHP6>ybte|bo2=Nq&miq_un9Cn`GXNv%M~ zd&r3zWOcEWOfqQ4Xl0IjpprClwRUXOA(djGtwT*)Jt_;_<8f3F5sK-t0NG!wFHp^} zfH}QB!LZN`rEqR_>&6 zEI`iG`bE}p_3iEn2wlnqnY4%n$k9&T!=QXlq|Pf?Do@fO79hLpWX+G1cU6uOan;I} zzSPA)jZc&zTk=;xe~L=3$^p9N%kFhMB4m;iSZ<;WdAANqo7})YqB_7e2qx09S9mxU zAnWTL>Sd}}W;=4J9j zT`}Ohk`gGQ00Q%1$s4F0TA5*Jb-7lqp;q)Edx->1E$dL@L_@1iB5Fw>4j5I)ceHw= zG1cn?m|Y8aDbHc8d@cGho>Ys(u89()Ea4nvc<|;TA3DmQb;^8CP|=A~E@d}+ z*zNlFoftn}$-Q$EAQIUXSHfuCSUC!WhLY&_kexwj=54aS+>4~8gDP@EJnJlV5j#9l8n=u$;%rj@% zE;TlwC6}9-AQRc6XUa~-`^eSwjC;OBDSCKZDam~$6b_olWLHiD*)|gc14WCojOw`} zx>HYiVWjdwkh5zV>87-6z}Y?^B0CL@>Z7&}f9XIc7AfZSvPx1`V;L)~=qip*Oj2GN zUG6B$_O9MFO>C(znR}l1gxA_cd27^WRahXGiq>8?s;i1wd!zE+NMLFaUntZX^uFTb zJ7KZZJR$BqT7}2xFFvl&PZQKBbj*H;h0D21tIttGh0ACnvh|^z3yCW<04&Pp=_dMp zfeNQlk+(g8oF)*@n>JTUDJ5P?9&J^)jS+zwui(#Wbrw4btGxv6#Eh^(MZ##qejqpK zQ+M(RuhlOI6goi?sS}h`VKMWAK0o=Sh=3_>Sf z?|2yf)hHsiE3{o7;77z%eh`Qs`}0#>CJ;~9{Cq~)LmVd%^CS3s9}z@M;#UDV$Df~S zB~f(0#pWka$|mMpdNejrel?PKh3$Ok+h4^B#{LDtu0zmvX}CTB0000 Date: Mon, 6 Jul 2020 20:16:31 +0400 Subject: [PATCH 4/4] Fix crash --- .../MediaPlayer/Sources/MediaPlayerAudioRenderer.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift index 7da7527b03..30a50a3ede 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift @@ -133,7 +133,14 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U var samplePtr = bufferData.advanced(by: dataOffset).assumingMemoryBound(to: Int16.self) for _ in 0 ..< actualConsumedCount / 4 { - let sample: Int16 = abs(samplePtr.pointee) + var sample: Int16 = samplePtr.pointee + if sample < 0 { + if sample <= -32768 { + sample = Int16.max + } else { + sample = -sample + } + } samplePtr = samplePtr.advanced(by: 2) if context.audioLevelPeak < sample {