Video call UI improvements

This commit is contained in:
Ilya Laktyushin 2020-08-05 01:23:05 +03:00
parent abb6e4a6bf
commit 2ead6ffb5d
23 changed files with 3676 additions and 3423 deletions

View File

@ -2293,7 +2293,9 @@ Unused sets are archived when you add more.";
"Notification.CallIncoming" = "Incoming Call";
"Notification.VideoCallIncoming" = "Incoming Video Call";
"Notification.CallMissed" = "Missed Call";
"Notification.VideoCallMissed" = "Missed Video Call";
"Notification.CallCanceled" = "Cancelled Call";
"Notification.VideoCallCanceled" = "Cancelled Video Call";
"Notification.CallOutgoingShort" = "Outgoing";
"Notification.CallIncomingShort" = "Incoming";
"Notification.CallMissedShort" = "Missed";

View File

@ -201,9 +201,25 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
if let discardReason = discardReason {
switch discardReason {
case .busy, .disconnect:
messageText = strings.Notification_CallCanceled
if isVideo {
messageText = strings.Notification_VideoCallCanceled
} else {
messageText = strings.Notification_CallCanceled
}
case .missed:
messageText = incoming ? strings.Notification_CallMissed : strings.Notification_CallCanceled
if incoming {
if isVideo {
messageText = strings.Notification_VideoCallMissed
} else {
messageText = strings.Notification_CallMissed
}
} else {
if isVideo {
messageText = strings.Notification_VideoCallCanceled
} else {
messageText = strings.Notification_CallCanceled
}
}
case .hangup:
break
}

View File

@ -3,6 +3,7 @@
#import <Foundation/Foundation.h>
NSString *randomCallsEmoji();
NSString *stringForEmojiHashOfData(NSData *data, NSInteger count);
#endif /* CallsEmoji_h */

View File

@ -7,15 +7,23 @@ static int32_t positionExtractor(uint8_t *bytes, int32_t i, int32_t count) {
return num % count;
}
NSArray *emojisArray() {
return @[ @"😉", @"😍", @"😛", @"😭", @"😱", @"😡", @"😎", @"😴", @"😵", @"😈", @"😬", @"😇", @"😏", @"👮", @"👷", @"💂", @"👶", @"👨", @"👩", @"👴", @"👵", @"😻", @"😽", @"🙀", @"👺", @"🙈", @"🙉", @"🙊", @"💀", @"👽", @"💩", @"🔥", @"💥", @"💤", @"👂", @"👀", @"👃", @"👅", @"👄", @"👍", @"👎", @"👌", @"👊", @"✌️", @"✋️", @"👐", @"👆", @"👇", @"👉", @"👈", @"🙏", @"👏", @"💪", @"🚶", @"🏃", @"💃", @"👫", @"👪", @"👬", @"👭", @"💅", @"🎩", @"👑", @"👒", @"👟", @"👞", @"👠", @"👕", @"👗", @"👖", @"👙", @"👜", @"👓", @"🎀", @"💄", @"💛", @"💙", @"💜", @"💚", @"💍", @"💎", @"🐶", @"🐺", @"🐱", @"🐭", @"🐹", @"🐰", @"🐸", @"🐯", @"🐨", @"🐻", @"🐷", @"🐮", @"🐗", @"🐴", @"🐑", @"🐘", @"🐼", @"🐧", @"🐥", @"🐔", @"🐍", @"🐢", @"🐛", @"🐝", @"🐜", @"🐞", @"🐌", @"🐙", @"🐚", @"🐟", @"🐬", @"🐋", @"🐐", @"🐊", @"🐫", @"🍀", @"🌹", @"🌻", @"🍁", @"🌾", @"🍄", @"🌵", @"🌴", @"🌳", @"🌞", @"🌚", @"🌙", @"🌎", @"🌋", @"⚡️", @"☔️", @"❄️", @"⛄️", @"🌀", @"🌈", @"🌊", @"🎓", @"🎆", @"🎃", @"👻", @"🎅", @"🎄", @"🎁", @"🎈", @"🔮", @"🎥", @"📷", @"💿", @"💻", @"☎️", @"📡", @"📺", @"📻", @"🔉", @"🔔", @"⏳", @"⏰", @"⌚️", @"🔒", @"🔑", @"🔎", @"💡", @"🔦", @"🔌", @"🔋", @"🚿", @"🚽", @"🔧", @"🔨", @"🚪", @"🚬", @"💣", @"🔫", @"🔪", @"💊", @"💉", @"💰", @"💵", @"💳", @"✉️", @"📫", @"📦", @"📅", @"📁", @"✂️", @"📌", @"📎", @"✒️", @"✏️", @"📐", @"📚", @"🔬", @"🔭", @"🎨", @"🎬", @"🎤", @"🎧", @"🎵", @"🎹", @"🎻", @"🎺", @"🎸", @"👾", @"🎮", @"🃏", @"🎲", @"🎯", @"🏈", @"🏀", @"⚽️", @"⚾️", @"🎾", @"🎱", @"🏉", @"🎳", @"🏁", @"🏇", @"🏆", @"🏊", @"🏄", @"☕️", @"🍼", @"🍺", @"🍷", @"🍴", @"🍕", @"🍔", @"🍟", @"🍗", @"🍱", @"🍚", @"🍜", @"🍡", @"🍳", @"🍞", @"🍩", @"🍦", @"🎂", @"🍰", @"🍪", @"🍫", @"🍭", @"🍯", @"🍎", @"🍏", @"🍊", @"🍋", @"🍒", @"🍇", @"🍉", @"🍓", @"🍑", @"🍌", @"🍐", @"🍍", @"🍆", @"🍅", @"🌽", @"🏡", @"🏥", @"🏦", @"⛪️", @"🏰", @"⛺️", @"🏭", @"🗻", @"🗽", @"🎠", @"🎡", @"⛲️", @"🎢", @"🚢", @"🚤", @"⚓️", @"🚀", @"✈️", @"🚁", @"🚂", @"🚋", @"🚎", @"🚌", @"🚙", @"🚗", @"🚕", @"🚛", @"🚨", @"🚔", @"🚒", @"🚑", @"🚲", @"🚠", @"🚜", @"🚦", @"⚠️", @"🚧", @"⛽️", @"🎰", @"🗿", @"🎪", @"🎭", @"🇯🇵", @"🇰🇷", @"🇩🇪", @"🇨🇳", @"🇺🇸", @"🇫🇷", @"🇪🇸", @"🇮🇹", @"🇷🇺", @"🇬🇧", @"1⃣", @"2⃣", @"3⃣", @"4⃣", @"5⃣", @"6⃣", @"7⃣", @"8⃣", @"9⃣", @"0⃣", @"🔟", @"❗️", @"❓", @"♥️", @"♦️", @"💯", @"🔗", @"🔱", @"🔴", @"🔵", @"🔶", @"🔷" ];
}
NSString *randomCallsEmoji() {
NSArray *emojis = emojisArray();
return emojis[arc4random() % emojis.count];
}
NSString *stringForEmojiHashOfData(NSData *data, NSInteger count) {
if (data.length != 32)
return @"";
NSArray *emojis = @[ @"😉", @"😍", @"😛", @"😭", @"😱", @"😡", @"😎", @"😴", @"😵", @"😈", @"😬", @"😇", @"😏", @"👮", @"👷", @"💂", @"👶", @"👨", @"👩", @"👴", @"👵", @"😻", @"😽", @"🙀", @"👺", @"🙈", @"🙉", @"🙊", @"💀", @"👽", @"💩", @"🔥", @"💥", @"💤", @"👂", @"👀", @"👃", @"👅", @"👄", @"👍", @"👎", @"👌", @"👊", @"✌️", @"✋️", @"👐", @"👆", @"👇", @"👉", @"👈", @"🙏", @"👏", @"💪", @"🚶", @"🏃", @"💃", @"👫", @"👪", @"👬", @"👭", @"💅", @"🎩", @"👑", @"👒", @"👟", @"👞", @"👠", @"👕", @"👗", @"👖", @"👙", @"👜", @"👓", @"🎀", @"💄", @"💛", @"💙", @"💜", @"💚", @"💍", @"💎", @"🐶", @"🐺", @"🐱", @"🐭", @"🐹", @"🐰", @"🐸", @"🐯", @"🐨", @"🐻", @"🐷", @"🐮", @"🐗", @"🐴", @"🐑", @"🐘", @"🐼", @"🐧", @"🐥", @"🐔", @"🐍", @"🐢", @"🐛", @"🐝", @"🐜", @"🐞", @"🐌", @"🐙", @"🐚", @"🐟", @"🐬", @"🐋", @"🐐", @"🐊", @"🐫", @"🍀", @"🌹", @"🌻", @"🍁", @"🌾", @"🍄", @"🌵", @"🌴", @"🌳", @"🌞", @"🌚", @"🌙", @"🌎", @"🌋", @"⚡️", @"☔️", @"❄️", @"⛄️", @"🌀", @"🌈", @"🌊", @"🎓", @"🎆", @"🎃", @"👻", @"🎅", @"🎄", @"🎁", @"🎈", @"🔮", @"🎥", @"📷", @"💿", @"💻", @"☎️", @"📡", @"📺", @"📻", @"🔉", @"🔔", @"⏳", @"⏰", @"⌚️", @"🔒", @"🔑", @"🔎", @"💡", @"🔦", @"🔌", @"🔋", @"🚿", @"🚽", @"🔧", @"🔨", @"🚪", @"🚬", @"💣", @"🔫", @"🔪", @"💊", @"💉", @"💰", @"💵", @"💳", @"✉️", @"📫", @"📦", @"📅", @"📁", @"✂️", @"📌", @"📎", @"✒️", @"✏️", @"📐", @"📚", @"🔬", @"🔭", @"🎨", @"🎬", @"🎤", @"🎧", @"🎵", @"🎹", @"🎻", @"🎺", @"🎸", @"👾", @"🎮", @"🃏", @"🎲", @"🎯", @"🏈", @"🏀", @"⚽️", @"⚾️", @"🎾", @"🎱", @"🏉", @"🎳", @"🏁", @"🏇", @"🏆", @"🏊", @"🏄", @"☕️", @"🍼", @"🍺", @"🍷", @"🍴", @"🍕", @"🍔", @"🍟", @"🍗", @"🍱", @"🍚", @"🍜", @"🍡", @"🍳", @"🍞", @"🍩", @"🍦", @"🎂", @"🍰", @"🍪", @"🍫", @"🍭", @"🍯", @"🍎", @"🍏", @"🍊", @"🍋", @"🍒", @"🍇", @"🍉", @"🍓", @"🍑", @"🍌", @"🍐", @"🍍", @"🍆", @"🍅", @"🌽", @"🏡", @"🏥", @"🏦", @"⛪️", @"🏰", @"⛺️", @"🏭", @"🗻", @"🗽", @"🎠", @"🎡", @"⛲️", @"🎢", @"🚢", @"🚤", @"⚓️", @"🚀", @"✈️", @"🚁", @"🚂", @"🚋", @"🚎", @"🚌", @"🚙", @"🚗", @"🚕", @"🚛", @"🚨", @"🚔", @"🚒", @"🚑", @"🚲", @"🚠", @"🚜", @"🚦", @"⚠️", @"🚧", @"⛽️", @"🎰", @"🗿", @"🎪", @"🎭", @"🇯🇵", @"🇰🇷", @"🇩🇪", @"🇨🇳", @"🇺🇸", @"🇫🇷", @"🇪🇸", @"🇮🇹", @"🇷🇺", @"🇬🇧", @"1⃣", @"2⃣", @"3⃣", @"4⃣", @"5⃣", @"6⃣", @"7⃣", @"8⃣", @"9⃣", @"0⃣", @"🔟", @"❗️", @"❓", @"♥️", @"♦️", @"💯", @"🔗", @"🔱", @"🔴", @"🔵", @"🔶", @"🔷" ];
uint8_t bytes[32];
[data getBytes:bytes length:32];
NSArray *emojis = emojisArray();
NSString *result = @"";
for (int32_t i = 0; i < count; i++)
{

View File

@ -119,6 +119,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
if self.currentContent != content {
let previousContent = self.currentContent
self.currentContent = content
if content.hasProgress {
@ -176,8 +177,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
context.setBlendMode(.copy)
}
}
let smallButtonSize: CGFloat = 60.0
imageScale = self.largeButtonSize / smallButtonSize
// let smallButtonSize: CGFloat = 60.0
// imageScale = self.largeButtonSize / smallButtonSize
case let .color(color):
switch color {
case .red:
@ -233,7 +234,21 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
self.contentBackgroundNode.image = contentBackgroundImage
}
if transition.isAnimated, let contentImage = contentImage, let previousContent = self.contentNode.image {
if transition.isAnimated, let previousContent = previousContent, previousContent.image == .accept && content.image == .end {
let rotation = CGFloat.pi / 4.0 * 3.0
if let snapshotView = self.contentNode.view.snapshotContentTree() {
snapshotView.frame = self.contentNode.view.frame
self.contentContainer.view.addSubview(snapshotView)
snapshotView.layer.animateRotation(from: 0.0, to: rotation, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.contentNode.image = contentImage
self.contentNode.layer.animateRotation(from: -rotation, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} else if transition.isAnimated, let contentImage = contentImage, let previousContent = self.contentNode.image {
self.contentNode.image = contentImage
self.contentNode.layer.animate(from: previousContent.cgImage!, to: contentImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
} else {

View File

@ -31,7 +31,8 @@ enum CallControllerButtonsMode: Equatable {
private enum ButtonDescription: Equatable {
enum Key: Hashable {
case accept
case end
case acceptOrEnd
case decline
case enableCamera
case switchCamera
case soundOutput
@ -60,9 +61,13 @@ private enum ButtonDescription: Equatable {
var key: Key {
switch self {
case .accept:
return .accept
case .end:
return .end
return .acceptOrEnd
case let .end(type):
if type == .decline {
return .decline
} else {
return .acceptOrEnd
}
case .enableCamera:
return .enableCamera
case .switchCamera:
@ -85,9 +90,9 @@ final class CallControllerButtonsNode: ASDisplayNode {
var isMuted = false
var isCameraPaused = false
var accept: (() -> Void)?
var acceptOrEnd: (() -> Void)?
var decline: (() -> Void)?
var mute: (() -> Void)?
var end: (() -> Void)?
var speaker: (() -> Void)?
var toggleVideo: (() -> Void)?
var rotateCamera: (() -> Void)?
@ -230,14 +235,14 @@ final class CallControllerButtonsNode: ASDisplayNode {
topButtons.append(.soundOutput(soundOutput))
}
let topButtonsContentWidth = CGFloat(topButtons.count) * smallButtonSize
let topButtonsContentWidth = CGFloat(topButtons.count) * largeButtonSize
let topButtonsAvailableSpacingWidth = width - topButtonsContentWidth - minSmallButtonSideInset * 2.0
let topButtonsSpacing = min(maxSmallButtonSpacing, topButtonsAvailableSpacingWidth / CGFloat(topButtons.count - 1))
let topButtonsWidth = CGFloat(topButtons.count) * smallButtonSize + CGFloat(topButtons.count - 1) * topButtonsSpacing
let topButtonsWidth = CGFloat(topButtons.count) * largeButtonSize + CGFloat(topButtons.count - 1) * topButtonsSpacing
var topButtonsLeftOffset = floor((width - topButtonsWidth) / 2.0)
for button in topButtons {
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: 0.0), size: CGSize(width: smallButtonSize, height: smallButtonSize))))
topButtonsLeftOffset += smallButtonSize + topButtonsSpacing
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: 0.0), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
topButtonsLeftOffset += largeButtonSize + topButtonsSpacing
}
if case .incomingRinging = mappedState {
@ -253,11 +258,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
let bottomButtonsWidth = CGFloat(bottomButtons.count) * largeButtonSize + CGFloat(bottomButtons.count - 1) * bottomButtonsSpacing
var bottomButtonsLeftOffset = floor((width - bottomButtonsWidth) / 2.0)
for button in bottomButtons {
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: bottomButtonsLeftOffset, y: smallButtonSize + topBottomSpacing), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: bottomButtonsLeftOffset, y: largeButtonSize + topBottomSpacing), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
bottomButtonsLeftOffset += largeButtonSize + bottomButtonsSpacing
}
height = smallButtonSize + topBottomSpacing + largeButtonSize + max(bottomInset + 32.0, 46.0)
height = largeButtonSize + topBottomSpacing + largeButtonSize + max(bottomInset + 32.0, 46.0)
case .active:
switch videoState {
case .active, .incomingRequested, .outgoingRequested:
@ -345,14 +350,14 @@ final class CallControllerButtonsNode: ASDisplayNode {
topButtons.append(.mute(self.isMuted))
topButtons.append(.soundOutput(soundOutput))
let topButtonsContentWidth = CGFloat(topButtons.count) * smallButtonSize
let topButtonsContentWidth = CGFloat(topButtons.count) * largeButtonSize
let topButtonsAvailableSpacingWidth = width - topButtonsContentWidth - minSmallButtonSideInset * 2.0
let topButtonsSpacing = min(maxSmallButtonSpacing, topButtonsAvailableSpacingWidth / CGFloat(topButtons.count - 1))
let topButtonsWidth = CGFloat(topButtons.count) * smallButtonSize + CGFloat(topButtons.count - 1) * topButtonsSpacing
let topButtonsWidth = CGFloat(topButtons.count) * largeButtonSize + CGFloat(topButtons.count - 1) * topButtonsSpacing
var topButtonsLeftOffset = floor((width - topButtonsWidth) / 2.0)
for button in topButtons {
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: 0.0), size: CGSize(width: smallButtonSize, height: smallButtonSize))))
topButtonsLeftOffset += smallButtonSize + topButtonsSpacing
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: 0.0), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
topButtonsLeftOffset += largeButtonSize + topButtonsSpacing
}
bottomButtons.append(.end(.outgoing))
@ -363,11 +368,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
let bottomButtonsWidth = CGFloat(bottomButtons.count) * largeButtonSize + CGFloat(bottomButtons.count - 1) * bottomButtonsSpacing
var bottomButtonsLeftOffset = floor((width - bottomButtonsWidth) / 2.0)
for button in bottomButtons {
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: bottomButtonsLeftOffset, y: smallButtonSize + topBottomSpacing), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: bottomButtonsLeftOffset, y: largeButtonSize + topBottomSpacing), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
bottomButtonsLeftOffset += largeButtonSize + bottomButtonsSpacing
}
height = smallButtonSize + topBottomSpacing + largeButtonSize + max(bottomInset + 32.0, 46.0)
height = largeButtonSize + topBottomSpacing + largeButtonSize + max(bottomInset + 32.0, 46.0)
}
}
@ -457,7 +462,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
buttonDelay = delayIncrement * 1.0
case .switchCamera:
buttonDelay = delayIncrement * 2.0
case .end:
case .acceptOrEnd:
buttonDelay = delayIncrement * 3.0
default:
break
@ -475,17 +480,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
if !validKeys.contains(key) {
removedKeys.append(key)
if animated {
if case .accept = key {
if let endButton = self.buttonNodes[.end] {
transition.updateFrame(node: button, frame: endButton.frame)
if let content = button.currentContent {
button.update(size: endButton.frame.size, content: content, text: button.currentText, transition: transition)
}
transition.updateTransformScale(node: button, scale: 0.1)
transition.updateAlpha(node: button, alpha: 0.0, completion: { [weak button] _ in
button?.removeFromSupernode()
})
}
if case .decline = key {
transition.updateTransformScale(node: button, scale: 0.1)
transition.updateAlpha(node: button, alpha: 0.0, completion: { [weak button] _ in
button?.removeFromSupernode()
})
} else {
transition.updateAlpha(node: button, alpha: 0.0, completion: { [weak button] _ in
button?.removeFromSupernode()
@ -508,9 +507,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
if button === listButton {
switch key {
case .accept:
self.accept?()
case .end:
self.end?()
self.acceptOrEnd?()
case .acceptOrEnd:
self.acceptOrEnd?()
case .decline:
self.decline?()
case .enableCamera:
self.toggleVideo?()
case .switchCamera:

View File

@ -0,0 +1,127 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import CallsEmoji
private let labelFont = Font.regular(22.0)
private let animationNodesCount = 3
private class EmojiSlotNode: ASDisplayNode {
var emoji: String = "" {
didSet {
self.node.attributedText = NSAttributedString(string: emoji, font: labelFont, textColor: .black)
let _ = self.node.updateLayout(CGSize(width: 100.0, height: 100.0))
}
}
private let maskNode: ASDisplayNode
private let containerNode: ASDisplayNode
private let node: ImmediateTextNode
private let animationNodes: [ImmediateTextNode]
override init() {
self.maskNode = ASDisplayNode()
self.containerNode = ASDisplayNode()
self.node = ImmediateTextNode()
self.animationNodes = (0 ..< animationNodesCount).map { _ in ImmediateTextNode() }
super.init()
let maskLayer = CAGradientLayer()
maskLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
maskLayer.locations = [0.0, 0.2, 0.8, 1.0]
maskLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
maskLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.maskNode.layer.mask = maskLayer
self.addSubnode(self.maskNode)
self.maskNode.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.node)
self.animationNodes.forEach({ self.containerNode.addSubnode($0) })
}
func animateIn(duration: Double) {
for node in self.animationNodes {
node.attributedText = NSAttributedString(string: randomCallsEmoji(), font: labelFont, textColor: .black)
let _ = node.updateLayout(CGSize(width: 100.0, height: 100.0))
}
self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -self.containerNode.frame.height + self.bounds.height), to: CGPoint(), duration: duration, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
override func layout() {
super.layout()
let maskInset: CGFloat = 4.0
let maskFrame = self.bounds.insetBy(dx: 0.0, dy: -maskInset)
self.maskNode.frame = maskFrame
self.maskNode.layer.mask?.frame = CGRect(origin: CGPoint(), size: maskFrame.size)
let spacing: CGFloat = 2.0
let containerSize = CGSize(width: self.bounds.width, height: self.bounds.height * CGFloat(animationNodesCount + 1) + spacing * CGFloat(animationNodesCount))
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: maskInset), size: containerSize)
self.node.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
var offset: CGFloat = self.bounds.height + spacing
for node in self.animationNodes {
node.frame = CGRect(origin: CGPoint(x: 0.0, y: offset), size: self.bounds.size)
offset += self.bounds.height + spacing
}
}
}
final class CallControllerKeyButton: HighlightableButtonNode {
private let containerNode: ASDisplayNode
private let nodes: [EmojiSlotNode]
var key: String = "" {
didSet {
var index = 0
for emoji in self.key {
guard index < 4 else {
return
}
self.nodes[index].emoji = String(emoji)
index += 1
}
}
}
init() {
self.containerNode = ASDisplayNode()
self.nodes = (0 ..< 4).map { _ in EmojiSlotNode() }
super.init(pointerStyle: nil)
self.addSubnode(self.containerNode)
self.nodes.forEach({ self.containerNode.addSubnode($0) })
}
func animateIn() {
self.layoutIfNeeded()
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
var duration: Double = 0.75
for node in self.nodes {
node.animateIn(duration: duration)
duration += 0.3
}
}
override func measure(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 114.0, height: 26.0)
}
override func layout() {
super.layout()
self.containerNode.frame = self.bounds
var index = 0
let nodeSize = CGSize(width: 29.0, height: self.bounds.size.height)
for node in self.nodes {
node.frame = CGRect(origin: CGPoint(x: CGFloat(index) * nodeSize.width, y: 0.0), size: nodeSize)
index += 1
}
self.nodes.forEach({ self.containerNode.addSubnode($0) })
}
}

View File

@ -32,7 +32,7 @@ final class CallControllerKeyPreviewNode: ASDisplayNode {
super.init()
self.keyTextNode.attributedText = NSAttributedString(string: keyText, attributes: [NSAttributedString.Key.font: Font.regular(58.0), NSAttributedString.Key.kern: 9.0 as NSNumber])
self.keyTextNode.attributedText = NSAttributedString(string: keyText, attributes: [NSAttributedString.Key.font: Font.regular(58.0), NSAttributedString.Key.kern: 11.0 as NSNumber])
self.infoTextNode.attributedText = NSAttributedString(string: infoText, font: Font.regular(14.0), textColor: UIColor.white, paragraphAlignment: .center)
@ -53,7 +53,7 @@ final class CallControllerKeyPreviewNode: ASDisplayNode {
let keyTextSize = self.keyTextNode.measure(CGSize(width: 300.0, height: 300.0))
transition.updateFrame(node: self.keyTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - keyTextSize.width) / 2) + 6.0, y: floor((size.height - keyTextSize.height) / 2) - 50.0), size: keyTextSize))
let infoTextSize = self.infoTextNode.measure(CGSize(width: size.width - 20.0, height: CGFloat.greatestFiniteMagnitude))
let infoTextSize = self.infoTextNode.measure(CGSize(width: size.width - 32.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.infoTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - infoTextSize.width) / 2.0), y: floor((size.height - infoTextSize.height) / 2.0) + 30.0), size: infoTextSize))
}

View File

@ -285,7 +285,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
private var debugNode: CallDebugNode?
private var keyTextData: (Data, String)?
private let keyButtonNode: HighlightableButtonNode
private let keyButtonNode: CallControllerKeyButton
private var validLayout: (ContainerViewLayout, CGFloat)?
private var disableActionsUntilTimestamp: Double = 0.0
@ -366,7 +366,7 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
self.videoPausedNode.alpha = 0.0
self.buttonsNode = CallControllerButtonsNode(strings: self.presentationData.strings)
self.keyButtonNode = HighlightableButtonNode()
self.keyButtonNode = CallControllerKeyButton()
super.init()
@ -410,17 +410,18 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
self?.beginAudioOuputSelection?()
}
self.buttonsNode.end = { [weak self] in
self?.endCall?()
}
self.buttonsNode.accept = { [weak self] in
self.buttonsNode.acceptOrEnd = { [weak self] in
guard let strongSelf = self, let callState = strongSelf.callState else {
return
}
switch callState.state {
case .active, .connecting, .reconnecting:
strongSelf.call.acceptVideo()
switch callState.videoState {
case .incomingRequested:
strongSelf.call.acceptVideo()
default:
self?.endCall?()
}
case .ringing:
strongSelf.acceptCall?()
default:
@ -428,6 +429,10 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
}
}
self.buttonsNode.decline = { [weak self] in
self?.endCall?()
}
self.buttonsNode.toggleVideo = { [weak self] in
guard let strongSelf = self, let callState = strongSelf.callState else {
return
@ -745,12 +750,13 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
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: [])
self.keyButtonNode.key = text
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)
self.keyButtonNode.animateIn()
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
@ -1520,6 +1526,9 @@ final class CallControllerNode: ViewControllerTracingNode, CallControllerNodePro
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.debugNode != nil {
return super.hitTest(point, with: event)
}
if self.containerTransformationNode.frame.contains(point) {
return self.containerTransformationNode.view.hitTest(self.view.convert(point, to: self.containerTransformationNode.view), with: event)
}

View File

@ -34,6 +34,7 @@ enum CallControllerStatusValue: Equatable {
final class CallControllerStatusNode: ASDisplayNode {
private let titleNode: TextNode
private let statusContainerNode: ASDisplayNode
private let statusNode: TextNode
private let statusMeasureNode: TextNode
private let receptionNode: CallControllerReceptionNode
@ -46,6 +47,21 @@ final class CallControllerStatusNode: ASDisplayNode {
if self.status != oldValue {
self.statusTimer?.invalidate()
if let snapshotView = self.statusContainerNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.statusContainerNode.frame
self.view.insertSubview(snapshotView, belowSubview: self.statusContainerNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.3, removeOnCompletion: false)
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: snapshotView.frame.height / 2.0), duration: 0.3, delay: 0.0, removeOnCompletion: false, additive: true)
self.statusContainerNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.3)
self.statusContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.statusContainerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -snapshotView.frame.height / 2.0), to: CGPoint(), duration: 0.3, delay: 0.0, additive: true)
}
if case .timer = self.status {
self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth {
@ -90,6 +106,7 @@ final class CallControllerStatusNode: ASDisplayNode {
override init() {
self.titleNode = TextNode()
self.statusContainerNode = ASDisplayNode()
self.statusNode = TextNode()
self.statusNode.displaysAsynchronously = false
self.statusMeasureNode = TextNode()
@ -106,9 +123,10 @@ final class CallControllerStatusNode: ASDisplayNode {
self.isUserInteractionEnabled = false
self.addSubnode(self.titleNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.receptionNode)
self.addSubnode(self.logoNode)
self.addSubnode(self.statusContainerNode)
self.statusContainerNode.addSubnode(self.statusNode)
self.statusContainerNode.addSubnode(self.receptionNode)
self.statusContainerNode.addSubnode(self.logoNode)
}
deinit {
@ -168,12 +186,13 @@ final class CallControllerStatusNode: ASDisplayNode {
let _ = statusMeasureApply()
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: titleLayout.size.height + spacing), size: statusLayout.size)
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: titleLayout.size.height + spacing + 9.0), size: receptionNodeSize)
self.statusContainerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: titleLayout.size.height + spacing), size: CGSize(width: constrainedWidth, height: statusLayout.size.height))
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: 0.0), size: statusLayout.size)
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: 9.0), size: receptionNodeSize)
self.logoNode.isHidden = !statusDisplayLogo
if let image = self.logoNode.image, let firstLineRect = statusMeasureLayout.linesRects().first {
let firstLineOffset = floor((statusMeasureLayout.size.width - firstLineRect.width) / 2.0)
self.logoNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX + firstLineOffset - image.size.width - 7.0, y: self.statusNode.frame.minY + 5.0), size: image.size)
self.logoNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX + firstLineOffset - image.size.width - 7.0, y: 5.0), size: image.size)
}
return titleLayout.size.height + spacing + statusLayout.size.height

View File

@ -0,0 +1,37 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
private let labelFont = Font.regular(17.0)
final class CallControllerToastNode: ASDisplayNode {
struct Content: Equatable {
enum Image {
case cameraOff
}
var image: Image
var text: String
init(image: Image, text: String) {
self.image = image
self.text = text
}
}
let effectView: UIVisualEffectView
override init() {
self.effectView = UIVisualEffectView()
self.effectView.effect = UIBlurEffect(style: .light)
self.effectView.layer.cornerRadius = 16.0
self.effectView.clipsToBounds = true
self.effectView.isUserInteractionEnabled = false
super.init()
self.view.addSubview(self.effectView)
}
}

View File

@ -357,7 +357,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
}, openSettings: {
openSettings()
}, { value in
if isVideo {
if isVideo && value {
DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {
@ -434,7 +434,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
}, openSettings: {
openSettings()
}, { value in
if isVideo {
if isVideo && value {
DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
present(c, a)
}, openSettings: {

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_calls_video.pdf",
"filename" : "ic_call_camera.pdf",
"idiom" : "universal"
}
],

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_calls_mute.pdf",
"filename" : "ic_call_microphone.pdf",
"idiom" : "universal"
}
],

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_calls_speaker.pdf",
"filename" : "ic_call_speaker.pdf",
"idiom" : "universal"
}
],

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_calls_cameraflip.pdf",
"filename" : "ic_call_flip.pdf",
"idiom" : "universal"
}
],

View File

@ -85,10 +85,26 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
switch discardReason {
case .busy, .disconnect:
callSuccessful = false
titleString = item.presentationData.strings.Notification_CallCanceled
if isVideo {
titleString = item.presentationData.strings.Notification_VideoCallCanceled
} else {
titleString = item.presentationData.strings.Notification_CallCanceled
}
case .missed:
callSuccessful = false
titleString = incoming ? item.presentationData.strings.Notification_CallMissed : item.presentationData.strings.Notification_CallCanceled
if incoming {
if isVideo {
titleString = item.presentationData.strings.Notification_VideoCallMissed
} else {
titleString = item.presentationData.strings.Notification_CallMissed
}
} else {
if isVideo {
titleString = item.presentationData.strings.Notification_VideoCallCanceled
} else {
titleString = item.presentationData.strings.Notification_CallCanceled
}
}
case .hangup:
break
}
@ -99,7 +115,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
if titleString == nil {
let baseString: String
if message.flags.contains(.Incoming) {
if incoming {
if isVideo {
baseString = item.presentationData.strings.Notification_VideoCallIncoming
} else {