import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import PresentationDataUtils import AvatarNode import TelegramStringFormatting import ContextUI import AccountContext import LegacyComponents import PeerInfoAvatarListNode private let backgroundCornerRadius: CGFloat = 14.0 final class VoiceChatPeerProfileNode: ASDisplayNode { private let context: AccountContext private let size: CGSize private var peer: EnginePeer private var text: VoiceChatParticipantItem.ParticipantText private let customNode: ASDisplayNode? private let additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError> private let backgroundImageNode: ASImageNode private let avatarListContainerNode: ASDisplayNode let avatarListWrapperNode: PinchSourceContainerNode let avatarListNode: PeerInfoAvatarListContainerNode private var videoFadeNode: ASImageNode private let infoNode: ASDisplayNode private let titleNode: ImmediateTextNode private let statusNode: VoiceChatParticipantStatusNode private var appeared = false init(context: AccountContext, size: CGSize, sourceSize: CGSize, peer: EnginePeer, text: VoiceChatParticipantItem.ParticipantText, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError>, requestDismiss: (() -> Void)?) { self.context = context self.size = size self.peer = peer self.text = text self.customNode = customNode self.additionalEntry = additionalEntry self.backgroundImageNode = ASImageNode() self.backgroundImageNode.clipsToBounds = true self.backgroundImageNode.displaysAsynchronously = false self.backgroundImageNode.displayWithoutProcessing = true self.videoFadeNode = ASImageNode() self.videoFadeNode.displaysAsynchronously = false self.videoFadeNode.contentMode = .scaleToFill self.avatarListContainerNode = ASDisplayNode() self.avatarListContainerNode.clipsToBounds = true self.avatarListWrapperNode = PinchSourceContainerNode() self.avatarListWrapperNode.clipsToBounds = true self.avatarListWrapperNode.cornerRadius = backgroundCornerRadius self.avatarListNode = PeerInfoAvatarListContainerNode(context: context) self.avatarListNode.backgroundColor = .clear self.avatarListNode.peer = peer self.avatarListNode.firstFullSizeOnly = true self.avatarListNode.offsetLocation = true self.avatarListNode.customCenterTapAction = { requestDismiss?() } self.infoNode = ASDisplayNode() self.infoNode.clipsToBounds = true self.titleNode = ImmediateTextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale self.statusNode = VoiceChatParticipantStatusNode() self.statusNode.isUserInteractionEnabled = false super.init() self.clipsToBounds = true self.addSubnode(self.backgroundImageNode) self.addSubnode(self.infoNode) self.addSubnode(self.videoFadeNode) self.addSubnode(self.avatarListWrapperNode) self.infoNode.addSubnode(self.titleNode) self.infoNode.addSubnode(self.statusNode) self.avatarListContainerNode.addSubnode(self.avatarListNode) self.avatarListContainerNode.addSubnode(self.avatarListNode.controlsClippingOffsetNode) self.avatarListWrapperNode.contentNode.addSubnode(self.avatarListContainerNode) self.avatarListWrapperNode.activate = { [weak self] sourceNode in guard let strongSelf = self else { return } strongSelf.avatarListNode.controlsContainerNode.alpha = 0.0 let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) } self.avatarListWrapperNode.deactivated = { [weak self] in guard let strongSelf = self else { return } strongSelf.avatarListWrapperNode.contentNode.layer.animate(from: 0.0 as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, completion: { _ in }) } self.avatarListWrapperNode.animatedOut = { [weak self] in guard let strongSelf = self else { return } strongSelf.avatarListNode.controlsContainerNode.alpha = 1.0 strongSelf.avatarListNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } self.updateInfo(size: size, sourceSize: sourceSize, animate: false) } func updateInfo(size: CGSize, sourceSize: CGSize, animate: Bool) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let titleFont = Font.regular(17.0) let titleColor = UIColor.white var titleAttributedString: NSAttributedString? if case let .user(user) = self.peer { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() switch presentationData.nameDisplayOrder { case .firstLast: string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)) case .lastFirst: string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)) string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) } titleAttributedString = string } else if let firstName = user.firstName, !firstName.isEmpty { titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) } else if let lastName = user.lastName, !lastName.isEmpty { titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor) } else { titleAttributedString = NSAttributedString(string: presentationData.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) } } else if case let .legacyGroup(group) = peer { titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) } else if case let .channel(channel) = peer { titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) } self.titleNode.attributedText = titleAttributedString let titleSize = self.titleNode.updateLayout(CGSize(width: self.size.width - 24.0, height: size.height)) let makeStatusLayout = self.statusNode.asyncLayout() let (statusLayout, statusApply) = makeStatusLayout(CGSize(width: self.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude), self.text, true) let _ = statusApply() self.titleNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: titleSize) self.statusNode.frame = CGRect(origin: CGPoint(x: 14.0, y: titleSize.height + 3.0), size: statusLayout) let totalHeight = titleSize.height + statusLayout.height + 3.0 + 8.0 let infoFrame = CGRect(x: 0.0, y: size.height - totalHeight, width: sourceSize.width, height: totalHeight) if animate { let springDuration: Double = !self.appeared ? 0.42 : 0.3 let springDamping: CGFloat = !self.appeared ? 124.0 : 1000.0 let initialInfoPosition = self.infoNode.position self.infoNode.layer.position = infoFrame.center let initialInfoBounds = self.infoNode.bounds self.infoNode.layer.bounds = CGRect(origin: CGPoint(), size: infoFrame.size) self.infoNode.layer.animateSpring(from: NSValue(cgPoint: initialInfoPosition), to: NSValue(cgPoint: self.infoNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) self.infoNode.layer.animateSpring(from: NSValue(cgRect: initialInfoBounds), to: NSValue(cgRect: self.infoNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) } else { self.infoNode.frame = infoFrame } } func animateIn(from sourceNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition) { let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) let springDuration: Double = 0.42 let springDamping: CGFloat = 124.0 if let sourceNode = sourceNode as? VoiceChatTileItemNode { let sourceRect = sourceNode.bounds self.backgroundImageNode.frame = sourceNode.bounds self.updateInfo(size: sourceNode.bounds.size, sourceSize: sourceNode.bounds.size, animate: false) self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor) context.fillEllipse(in: bounds) context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) self.backgroundImageNode.cornerRadius = backgroundCornerRadius transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0) let initialRect = sourceRect let initialScale: CGFloat = sourceRect.width / targetRect.width let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width) self.avatarListWrapperNode.update(size: targetSize, transition: .immediate) self.avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.width + backgroundCornerRadius) self.avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetSize) self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 var appearanceTransition = transition if transition.isAnimated { appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) } if let videoNode = sourceNode.videoNode { videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition) appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius } self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) { self.videoFadeNode.image = tileFadeImage self.videoFadeNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height) self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode) self.view.insertSubview(snapshotView, aboveSubview: sourceNode.videoContainerNode.view) snapshotView.frame = sourceRect appearanceTransition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - snapshotView.frame.size.height), size: snapshotView.frame.size)) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in snapshotView.removeFromSuperview() }) appearanceTransition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetSize.width, height: self.videoFadeNode.frame.height))) self.videoFadeNode.alpha = 0.0 self.videoFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } self.avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in if let strongSelf = self { strongSelf.avatarListNode.updateCustomItemsOnlySynchronously = false strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode) } }) radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: 0.0) self.avatarListWrapperNode.contentNode.clipsToBounds = true self.avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize()) self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) self.avatarListNode.topShadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0) self.avatarListNode.updateCustomItemsOnlySynchronously = true self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: self.customNode, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate) let backgroundTargetRect = CGRect(x: 0.0, y: targetSize.height - backgroundCornerRadius * 2.0, width: targetRect.width, height: targetRect.height - targetSize.height + backgroundCornerRadius * 2.0) let initialBackgroundPosition = self.backgroundImageNode.position self.backgroundImageNode.layer.position = backgroundTargetRect.center let initialBackgroundBounds = self.backgroundImageNode.bounds self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) } else if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode { let sourceRect = sourceNode.bounds self.backgroundImageNode.frame = sourceNode.bounds self.updateInfo(size: sourceNode.bounds.size, sourceSize: sourceNode.bounds.size, animate: false) self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor) context.fillEllipse(in: bounds) context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) self.backgroundImageNode.cornerRadius = backgroundCornerRadius transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0) let initialRect: CGRect let hasVideo: Bool if let videoNode = sourceNode.videoNode, videoNode.supernode == sourceNode.videoContainerNode, !videoNode.alpha.isZero { initialRect = sourceRect hasVideo = true } else { initialRect = sourceNode.avatarNode.frame hasVideo = false } let initialScale = initialRect.width / targetRect.width let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width) self.avatarListWrapperNode.update(size: targetSize, transition: .immediate) self.avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.width + backgroundCornerRadius) self.avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetSize) self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 var appearanceTransition = transition if transition.isAnimated { appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0)) } if let videoNode = sourceNode.videoNode, hasVideo { videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition) appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) appearanceTransition.updateFrame(node: sourceNode.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - fadeHeight), size: CGSize(width: targetSize.width, height: fadeHeight))) appearanceTransition.updateTransformScale(node: sourceNode.videoContainerNode, scale: 1.0) appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius appearanceTransition.updateAlpha(node: sourceNode.videoFadeNode, alpha: 0.0) } else { let transitionNode = ASImageNode() transitionNode.clipsToBounds = true transitionNode.displaysAsynchronously = false transitionNode.displayWithoutProcessing = true transitionNode.image = sourceNode.avatarNode.unroundedImage transitionNode.frame = CGRect(origin: CGPoint(), size: targetSize) transitionNode.cornerRadius = targetRect.width / 2.0 radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0) sourceNode.avatarNode.isHidden = true self.avatarListWrapperNode.contentNode.insertSubnode(transitionNode, at: 0) } self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) self.avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in if let strongSelf = self { strongSelf.avatarListNode.updateCustomItemsOnlySynchronously = false strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode) } }) radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: 0.0) self.avatarListWrapperNode.contentNode.clipsToBounds = true self.avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize()) self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) self.avatarListNode.topShadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0) self.avatarListNode.updateCustomItemsOnlySynchronously = true self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: self.customNode, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate) let backgroundTargetRect = CGRect(x: 0.0, y: targetSize.height - backgroundCornerRadius * 2.0, width: targetRect.width, height: targetRect.height - targetSize.height + backgroundCornerRadius * 2.0) let initialBackgroundPosition = self.backgroundImageNode.position self.backgroundImageNode.layer.position = backgroundTargetRect.center let initialBackgroundBounds = self.backgroundImageNode.bounds self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) } self.appeared = true } func animateOut(to targetNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) let springDuration: Double = 0.3 let springDamping: CGFloat = 1000.0 if let targetNode = targetNode as? VoiceChatTileItemNode { let initialSize = self.bounds self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) let targetScale = targetRect.width / avatarListContainerNode.frame.width self.insertSubnode(targetNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) self.insertSubnode(self.videoFadeNode, aboveSubnode: targetNode.videoContainerNode) self.avatarListWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) self.avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: self.avatarListWrapperNode.position), to: NSValue(cgPoint: targetRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak targetNode] _ in if let targetNode = targetNode { targetNode.contentNode.insertSubnode(targetNode.videoContainerNode, aboveSubnode: targetNode.backgroundNode) } completion() self?.removeFromSupernode() }) radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: backgroundCornerRadius) if let snapshotView = targetNode.infoNode.view.snapshotView(afterScreenUpdates: true) { self.view.insertSubview(snapshotView, aboveSubview: targetNode.videoContainerNode.view) let snapshotFrame = snapshotView.frame snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: initialSize.width - snapshotView.frame.size.height), size: snapshotView.frame.size) transition.updateFrame(view: snapshotView, frame: snapshotFrame) snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetRect.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetRect.width, height: self.videoFadeNode.frame.height))) self.videoFadeNode.alpha = 1.0 self.videoFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } if let videoNode = targetNode.videoNode { videoNode.updateLayout(size: targetRect.size, layoutMode: .fillOrFitToSquare, transition: transition) transition.updateFrame(node: videoNode, frame: targetRect) transition.updateFrame(node: targetNode.videoContainerNode, frame: targetRect) } let backgroundTargetRect = targetRect let initialBackgroundPosition = self.backgroundImageNode.position self.backgroundImageNode.layer.position = backgroundTargetRect.center let initialBackgroundBounds = self.backgroundImageNode.bounds self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) self.avatarListNode.stripContainerNode.alpha = 0.0 self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.avatarListNode.topShadowNode.alpha = 0.0 self.avatarListNode.topShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.infoNode.alpha = 0.0 self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode { let backgroundTargetRect = targetRect self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true) targetNode.avatarNode.isHidden = false transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) var targetRect = targetRect let hasVideo: Bool if let videoNode = targetNode.videoNode, !videoNode.alpha.isZero { hasVideo = true } else { targetRect = targetNode.avatarNode.frame hasVideo = false } let targetScale = targetRect.width / self.avatarListContainerNode.frame.width self.insertSubnode(targetNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) self.insertSubnode(self.videoFadeNode, aboveSubnode: targetNode.videoContainerNode) self.avatarListWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) self.avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: self.avatarListWrapperNode.position), to: NSValue(cgPoint: targetRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak targetNode] _ in if let targetNode = targetNode { targetNode.offsetContainerNode.insertSubnode(targetNode.videoContainerNode, at: 0) } completion() self?.removeFromSupernode() }) radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: backgroundCornerRadius) if hasVideo, let videoNode = targetNode.videoNode { videoNode.updateLayout(size: CGSize(width: 180.0, height: 180.0), layoutMode: .fillOrFitToSquare, transition: transition) transition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 180.0, height: 180.0))) transition.updateTransformScale(node: targetNode.videoContainerNode, scale: 84.0 / 180.0) transition.updateFrameAsPositionAndBounds(node: targetNode.videoContainerNode, frame: CGRect(x: 0.0, y: 0.0, width: 180.0, height: 180.0)) transition.updatePosition(node: targetNode.videoContainerNode, position: CGPoint(x: 42.0, y: 42.0)) transition.updateFrame(node: targetNode.videoFadeNode, frame: CGRect(x: 0.0, y: 180.0 - fadeHeight, width: 180.0, height: fadeHeight)) transition.updateAlpha(node: targetNode.videoFadeNode, alpha: 1.0) } let initialBackgroundPosition = self.backgroundImageNode.position self.backgroundImageNode.layer.position = backgroundTargetRect.center let initialBackgroundBounds = self.backgroundImageNode.bounds self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) self.avatarListNode.stripContainerNode.alpha = 0.0 self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.avatarListNode.topShadowNode.alpha = 0.0 self.avatarListNode.topShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.infoNode.alpha = 0.0 self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } } }