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/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/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 { 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/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/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 0000000000..07403f47ec Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png new file mode 100644 index 0000000000..62a62518d8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallMuteButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallMuteButton.imageset_CallMuteIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/Contents.json new file mode 100644 index 0000000000..b052ec35c4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png new file mode 100644 index 0000000000..f3587ea0f3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png differ 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 0000000000..8237f34688 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallPhoneButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallPhoneButton.imageset_CallPhoneIcon@3x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json new file mode 100644 index 0000000000..2ef9a3ee1a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png new file mode 100644 index 0000000000..9b5e566eb4 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png differ 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 0000000000..0026e6063d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/Contents.json new file mode 100644 index 0000000000..421006291a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png new file mode 100644 index 0000000000..996959b567 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png differ 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 0000000000..345c9a8f3b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallSpeakerButton.imageset/submodules_TelegramUI_Images.xcassets_Call_CallSpeakerButton.imageset_CallSpeakerIcon@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png differ 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) } 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"