import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import AvatarNode import AccountContext import SwiftSignalKit import TelegramPresentationData import PhotoResources import PeerAvatarGalleryUI import TelegramStringFormatting import PhoneNumberFormat import ActivityIndicator import TelegramUniversalVideoContent import GalleryUI import UniversalMediaPlayer import RadialStatusNode import TelegramUIPreferences import PeerInfoAvatarListNode import AnimationUI import ContextUI import ManagedAnimationNode import ComponentFlow import EmojiStatusComponent import AnimationCache import MultiAnimationRenderer import ComponentDisplayAdapters import ChatTitleView import AppBundle import AvatarVideoNode enum PeerInfoHeaderButtonKey: Hashable { case message case discussion case call case videoCall case voiceChat case mute case more case addMember case search case leave case stop } enum PeerInfoHeaderButtonIcon { case message case call case videoCall case voiceChat case mute case unmute case more case addMember case search case leave case stop } final class PeerInfoHeaderButtonNode: HighlightableButtonNode { let key: PeerInfoHeaderButtonKey private let action: (PeerInfoHeaderButtonNode, ContextGesture?) -> Void let referenceNode: ContextReferenceContentNode let containerNode: ContextControllerSourceNode private let backgroundNode: ASDisplayNode private let iconNode: ASImageNode private let textNode: ImmediateTextNode private var animationNode: AnimationNode? private var theme: PresentationTheme? private var icon: PeerInfoHeaderButtonIcon? private var isActive: Bool? init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode, ContextGesture?) -> Void) { self.key = key self.action = action self.referenceNode = ContextReferenceContentNode() self.containerNode = ContextControllerSourceNode() self.containerNode.animateScale = false self.backgroundNode = ASDisplayNode() self.backgroundNode.cornerRadius = 11.0 self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true self.iconNode.isUserInteractionEnabled = false self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.isUserInteractionEnabled = false super.init() self.accessibilityTraits = .button self.containerNode.addSubnode(self.referenceNode) self.referenceNode.addSubnode(self.backgroundNode) self.referenceNode.addSubnode(self.iconNode) self.addSubnode(self.containerNode) self.addSubnode(self.textNode) self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.layer.removeAnimation(forKey: "opacity") strongSelf.alpha = 0.4 } else { strongSelf.alpha = 1.0 strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.containerNode.activated = { [weak self] gesture, _ in if let strongSelf = self { strongSelf.action(strongSelf, gesture) } } self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } @objc private func buttonPressed() { switch self.icon { case .voiceChat, .more, .leave: self.animationNode?.playOnce() default: break } self.action(self, nil) } func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isActive: Bool, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { let previousIcon = self.icon let themeUpdated = self.theme != presentationData.theme let iconUpdated = self.icon != icon let isActiveUpdated = self.isActive != isActive self.isActive = isActive let iconSize = CGSize(width: 40.0, height: 40.0) if themeUpdated || iconUpdated { self.theme = presentationData.theme self.icon = icon var isGestureEnabled = false if [.mute, .voiceChat, .more].contains(icon) { isGestureEnabled = true } self.containerNode.isGestureEnabled = isGestureEnabled let animationName: String? var colors: [String: UIColor] = [:] var playOnce = false var seekToEnd = false let iconColor = presentationData.theme.list.itemAccentColor switch icon { case .voiceChat: animationName = "anim_profilevc" colors = ["Line 3.Group 1.Stroke 1": iconColor, "Line 1.Group 1.Stroke 1": iconColor, "Line 2.Group 1.Stroke 1": iconColor] case .mute: animationName = "anim_profileunmute" colors = ["Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor] if previousIcon == .unmute { playOnce = true } else { seekToEnd = true } case .unmute: animationName = "anim_profilemute" colors = ["Middle.Group 1.Fill 1": iconColor, "Top.Group 1.Fill 1": iconColor, "Bottom.Group 1.Fill 1": iconColor, "EXAMPLE.Group 1.Fill 1": iconColor, "Line.Group 1.Stroke 1": iconColor] if previousIcon == .mute { playOnce = true } else { seekToEnd = true } case .more: animationName = "anim_profilemore" colors = ["Point 2.Group 1.Fill 1": iconColor, "Point 3.Group 1.Fill 1": iconColor, "Point 1.Group 1.Fill 1": iconColor] case .leave: animationName = "anim_profileleave" colors = ["Arrow.Group 2.Stroke 1": iconColor, "Door.Group 1.Stroke 1": iconColor, "Arrow.Group 1.Stroke 1": iconColor] default: animationName = nil } if let animationName = animationName { let animationNode: AnimationNode if let current = self.animationNode { animationNode = current animationNode.setAnimation(name: animationName, colors: colors) } else { animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) self.referenceNode.addSubnode(animationNode) self.animationNode = animationNode } } else if let animationNode = self.animationNode { self.animationNode = nil animationNode.removeFromSupernode() } if playOnce { self.animationNode?.play() } else if seekToEnd { self.animationNode?.seekToEnd() } self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) self.iconNode.image = generateImage(iconSize, contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.normal) context.setFillColor(iconColor.cgColor) let imageName: String? switch icon { case .message: imageName = "Peer Info/ButtonMessage" case .call: imageName = "Peer Info/ButtonCall" case .videoCall: imageName = "Peer Info/ButtonVideo" case .voiceChat: imageName = nil case .mute: imageName = nil case .unmute: imageName = nil case .more: imageName = nil case .addMember: imageName = "Peer Info/ButtonAddMember" case .search: imageName = "Peer Info/ButtonSearch" case .leave: imageName = nil case .stop: imageName = "Peer Info/ButtonStop" } if let imageName = imageName, let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) { let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) context.clip(to: imageRect, mask: image.cgImage!) context.fill(imageRect) } }) } if isActiveUpdated { let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) alphaTransition.updateAlpha(node: self.iconNode, alpha: isActive ? 1.0 : 0.3) if let animationNode = self.animationNode { alphaTransition.updateAlpha(node: animationNode, alpha: isActive ? 1.0 : 0.3) } alphaTransition.updateAlpha(node: self.textNode, alpha: isActive ? 1.0 : 0.3) } self.textNode.attributedText = NSAttributedString(string: text.lowercased(), font: Font.regular(11.0), textColor: presentationData.theme.list.itemAccentColor) self.accessibilityLabel = text let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize)) if let animationNode = self.animationNode { transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: 1.0), size: iconSize)) } transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 9.0), size: titleSize)) self.referenceNode.frame = self.containerNode.bounds } } final class PeerInfoHeaderNavigationTransition { let sourceNavigationBar: NavigationBar let sourceTitleView: ChatTitleView let sourceTitleFrame: CGRect let sourceSubtitleFrame: CGRect let fraction: CGFloat init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, fraction: CGFloat) { self.sourceNavigationBar = sourceNavigationBar self.sourceTitleView = sourceTitleView self.sourceTitleFrame = sourceTitleFrame self.sourceSubtitleFrame = sourceSubtitleFrame self.fraction = fraction } } final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let context: AccountContext let containerNode: ContextControllerSourceNode let avatarNode: AvatarNode fileprivate var videoNode: UniversalVideoNode? fileprivate var markupNode: AvatarVideoNode? fileprivate var iconView: ComponentView? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? var isExpanded: Bool = false var canAttachVideo: Bool = true { didSet { if oldValue != self.canAttachVideo { self.videoNode?.canAttachContent = !self.isExpanded && self.canAttachVideo } } } var tapped: (() -> Void)? var emojiTapped: (() -> Void)? var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? private var isFirstAvatarLoading = true var item: PeerInfoAvatarListItem? private let playbackStartDisposable = MetaDisposable() init(context: AccountContext) { self.context = context self.containerNode = ContextControllerSourceNode() let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) self.avatarNode = AvatarNode(font: avatarFont) super.init() self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) self.containerNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) self.avatarNode.frame = self.containerNode.bounds let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.avatarNode.view.addGestureRecognizer(tapGestureRecognizer) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self else { return } tapGestureRecognizer.isEnabled = false tapGestureRecognizer.isEnabled = true strongSelf.contextAction?(strongSelf.containerNode, gesture) } } deinit { self.playbackStartDisposable.dispose() } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.tapped?() } } @objc private func emojiTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.emojiTapped?() } } func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { if let videoNode = self.videoNode { if case .immediate = transition, fraction == 1.0 { return } if fraction > 0.0 { videoNode.pause() } else { videoNode.play() } transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) } if let markupNode = self.markupNode { if case .immediate = transition, fraction == 1.0 { return } if fraction > 0.0 { markupNode.updateVisibility(false) } else { markupNode.updateVisibility(true) } transition.updateAlpha(node: markupNode, alpha: 1.0 - fraction) } } var removedPhotoResourceIds = Set() func update(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { if let peer = peer { let previousItem = self.item var item = item self.item = item var overrideImage: AvatarNodeImageOverride? if peer.isDeleted { overrideImage = .deletedIcon } else if let previousItem = previousItem, item == nil { if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last { self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) } overrideImage = AvatarNodeImageOverride.none item = nil } else if let rep = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(rep.resource.id.stringRepresentation) { overrideImage = AvatarNodeImageOverride.none item = nil } if let _ = overrideImage { self.containerNode.isGestureEnabled = false } else if peer.profileImageRepresentations.isEmpty { self.containerNode.isGestureEnabled = false } else { self.containerNode.isGestureEnabled = false } self.avatarNode.imageNode.animateFirstTransition = !isSettings self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true) if let threadInfo = threadInfo { self.avatarNode.isHidden = true let iconView: ComponentView if let current = self.iconView { iconView = current } else { iconView = ComponentView() self.iconView = iconView } let content: EmojiStatusComponent.Content if threadId == 1 { content = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(theme)) } else if let iconFileId = threadInfo.icon { content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .forever) } else { content = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: avatarSize, height: avatarSize)) } let _ = iconView.update( transition: .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, content: content, isVisibleForAnimations: true, action: nil )), environment: {}, containerSize: CGSize(width: avatarSize, height: avatarSize) ) if let iconComponentView = iconView.view { iconComponentView.isUserInteractionEnabled = true if iconComponentView.superview == nil { iconComponentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.emojiTapGesture(_:)))) self.avatarNode.view.superview?.addSubview(iconComponentView) } iconComponentView.frame = CGRect(origin: CGPoint(), size: CGSize(width: avatarSize, height: avatarSize)) } } var isForum = false let avatarCornerRadius: CGFloat if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { avatarCornerRadius = floor(avatarSize * 0.25) isForum = true } else { avatarCornerRadius = avatarSize / 2.0 } if self.avatarNode.layer.cornerRadius != 0.0 { ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius) } else { self.avatarNode.layer.cornerRadius = avatarCornerRadius } self.avatarNode.layer.masksToBounds = true self.isFirstAvatarLoading = false self.containerNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.frame = self.containerNode.bounds self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) if let item = item { let representations: [ImageRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference] let immediateThumbnailData: Data? var videoId: Int64 let markup: TelegramMediaImage.EmojiMarkup? switch item { case .custom: representations = [] videoRepresentations = [] immediateThumbnailData = nil videoId = 0 markup = nil case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail videoId = peer.id.id._internalGetInt64Value() if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { videoId = videoId &+ resource.photoId } markup = nil case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail if case let .cloud(imageId, _, _) = reference { videoId = imageId } else { videoId = peer.id.id._internalGetInt64Value() } markup = markupValue } self.containerNode.isGestureEnabled = !isSettings if let markup { if let videoNode = self.videoNode { self.videoContent = nil self.videoStartTimestamp = nil self.videoNode = nil videoNode.removeFromSupernode() } let markupNode: AvatarVideoNode if let current = self.markupNode { markupNode = current } else { markupNode = AvatarVideoNode(context: self.context) self.containerNode.addSubnode(markupNode) self.markupNode = markupNode } markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) markupNode.updateVisibility(true) } else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true if let startTimestamp = video.representation.startTimestamp { self.videoStartTimestamp = startTimestamp self.playbackStartDisposable.set((videoNode.status |> map { status -> Bool in if let status = status, case .playing = status.status { return true } else { return false } } |> filter { playing in return playing } |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { Queue.mainQueue().after(0.15) { strongSelf.videoNode?.isHidden = false } } })) } else { self.videoStartTimestamp = nil self.playbackStartDisposable.set(nil) videoNode.isHidden = false } self.videoContent = videoContent self.videoNode = videoNode let maskPath: UIBezierPath if isForum { maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) } else { maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) } let shape = CAShapeLayer() shape.path = maskPath.cgPath videoNode.layer.mask = shape self.containerNode.addSubnode(videoNode) } } else { if let markupNode = self.markupNode { self.markupNode = nil markupNode.removeFromSupernode() } if let videoNode = self.videoNode { self.videoStartTimestamp = nil self.videoContent = nil self.videoNode = nil videoNode.removeFromSupernode() } } } else { if let markupNode = self.markupNode { self.markupNode = nil markupNode.removeFromSupernode() } if let videoNode = self.videoNode { self.videoStartTimestamp = nil self.videoContent = nil self.videoNode = nil videoNode.removeFromSupernode() } self.containerNode.isGestureEnabled = false } if let markupNode = self.markupNode { markupNode.frame = self.avatarNode.frame markupNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: avatarCornerRadius, transition: .immediate) } if let videoNode = self.videoNode { if self.canAttachVideo { videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) } videoNode.frame = self.avatarNode.frame if isExpanded == videoNode.canAttachContent { self.isExpanded = isExpanded let update = { videoNode.canAttachContent = !self.isExpanded && self.canAttachVideo if videoNode.canAttachContent { videoNode.play() } } if isExpanded { DispatchQueue.main.async { update() } } else { update() } } } } } } final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode { private let context: AccountContext private let imageNode: ImageNode private let updatingAvatarOverlay: ASImageNode private let iconNode: ASImageNode private var statusNode: RadialStatusNode private var currentRepresentation: TelegramMediaImageRepresentation? init(context: AccountContext) { self.context = context self.imageNode = ImageNode(enableEmpty: true) self.updatingAvatarOverlay = ASImageNode() self.updatingAvatarOverlay.displayWithoutProcessing = true self.updatingAvatarOverlay.displaysAsynchronously = false self.updatingAvatarOverlay.alpha = 0.0 self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6)) self.statusNode.isUserInteractionEnabled = false self.iconNode = ASImageNode() self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) self.iconNode.alpha = 0.0 super.init() self.imageNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) self.updatingAvatarOverlay.frame = self.imageNode.frame let radialStatusSize: CGFloat = 50.0 let imagePosition = self.imageNode.position self.statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) if let image = self.iconNode.image { self.iconNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - image.size.width / 2.0), y: floor(imagePosition.y - image.size.height / 2.0)), size: image.size) } self.addSubnode(self.imageNode) self.addSubnode(self.updatingAvatarOverlay) self.addSubnode(self.statusNode) } func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateAlpha(node: self, alpha: 1.0 - fraction) } func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { guard let peer = peer else { return } self.imageNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) self.updatingAvatarOverlay.frame = self.imageNode.frame let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) let clipStyle: AvatarNodeClipStyle if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { clipStyle = .roundedRect } else { clipStyle = .round } var isPersonal = false if let updatingAvatar, case let .image(image) = updatingAvatar, image.isPersonal { isPersonal = true } if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isPersonal || self.currentRepresentation != nil && updatingAvatar == nil { var overlayHidden = true if let updatingAvatar = updatingAvatar { overlayHidden = false var cancelEnabled = true let progressValue: CGFloat? if let uploadProgress { switch uploadProgress { case let .value(value): progressValue = max(0.027, value) case .indefinite: progressValue = nil cancelEnabled = false } } else { progressValue = 0.027 } self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: progressValue, cancelEnabled: cancelEnabled, animateRotation: true)) if case let .image(representation) = updatingAvatar { if representation != self.currentRepresentation { self.currentRepresentation = representation if let signal = peerAvatarImage(account: context.account, peerReference: nil, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: avatarSize, height: avatarSize), clipStyle: clipStyle, emptyColor: nil, synchronousLoad: false, provideUnrounded: false) { self.imageNode.setSignal(signal |> map { $0?.0 }) } } } transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 1.0) } else { let targetOverlayAlpha: CGFloat = 0.0 if self.updatingAvatarOverlay.alpha != targetOverlayAlpha { let update = { self.statusNode.transitionToState(.none) self.currentRepresentation = nil self.imageNode.setSignal(.single(nil)) transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: overlayHidden ? 0.0 : 1.0) } Queue.mainQueue().after(0.3) { update() } } } if !overlayHidden && self.updatingAvatarOverlay.image == nil { switch clipStyle { case .round: self.updatingAvatarOverlay.image = generateFilledCircleImage(diameter: avatarSize, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) case .roundedRect: self.updatingAvatarOverlay.image = generateFilledRoundedRectImage(size: CGSize(width: avatarSize, height: avatarSize), cornerRadius: avatarSize * 0.25, color: UIColor(white: 0.0, alpha: 0.4), backgroundColor: nil) default: break } } } else { self.statusNode.transitionToState(.none) self.currentRepresentation = nil transition.updateAlpha(node: self.iconNode, alpha: 0.0) transition.updateAlpha(node: self.updatingAvatarOverlay, alpha: 0.0) } } } final class PeerInfoEditingAvatarNode: ASDisplayNode { private let context: AccountContext let avatarNode: AvatarNode fileprivate var videoNode: UniversalVideoNode? fileprivate var markupNode: AvatarVideoNode? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? var item: PeerInfoAvatarListItem? var tapped: ((Bool) -> Void)? var canAttachVideo: Bool = true init(context: AccountContext) { self.context = context let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) self.avatarNode = AvatarNode(font: avatarFont) super.init() self.addSubnode(self.avatarNode) self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.tapped?(false) } } func reset() { guard let videoNode = self.videoNode else { return } videoNode.isHidden = true videoNode.seek(self.videoStartTimestamp ?? 0.0) Queue.mainQueue().after(0.15) { videoNode.isHidden = false } } var removedPhotoResourceIds = Set() func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { guard let peer = peer else { return } let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) let previousItem = self.item var item = item self.item = item let overrideImage: AvatarNodeImageOverride? if canEdit, peer.profileImageRepresentations.isEmpty { overrideImage = .editAvatarIcon(forceNone: true) } else if let previousItem = previousItem, item == nil { if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last { self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) } overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none item = nil } else if let representation = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(representation.resource.id.stringRepresentation) { overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none item = nil } else { overrideImage = item == nil && canEdit ? .editAvatarIcon(forceNone: true) : nil } self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) var isForum = false let avatarCornerRadius: CGFloat if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { isForum = true avatarCornerRadius = floor(avatarSize * 0.25) } else { avatarCornerRadius = avatarSize / 2.0 } if self.avatarNode.layer.cornerRadius != 0.0 { ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius) } else { self.avatarNode.layer.cornerRadius = avatarCornerRadius } self.avatarNode.layer.masksToBounds = true if let item = item { let representations: [ImageRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference] let immediateThumbnailData: Data? var videoId: Int64 let markup: TelegramMediaImage.EmojiMarkup? switch item { case .custom: representations = [] videoRepresentations = [] immediateThumbnailData = nil videoId = 0 markup = nil case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail videoId = peer.id.id._internalGetInt64Value() if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { videoId = videoId &+ resource.photoId } markup = nil case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail if case let .cloud(imageId, _, _) = reference { videoId = imageId } else { videoId = peer.id.id._internalGetInt64Value() } markup = markupValue } if let markup { if let videoNode = self.videoNode { self.videoContent = nil self.videoStartTimestamp = nil self.videoNode = nil videoNode.removeFromSupernode() } let markupNode: AvatarVideoNode if let current = self.markupNode { markupNode = current } else { markupNode = AvatarVideoNode(context: self.context) self.insertSubnode(markupNode, aboveSubnode: self.avatarNode) self.markupNode = markupNode } markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) markupNode.updateVisibility(true) } else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { if let markupNode = self.markupNode { self.markupNode = nil markupNode.removeFromSupernode() } let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() let mediaManager = self.context.sharedContext.mediaManager let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery) videoNode.isUserInteractionEnabled = false self.videoStartTimestamp = video.representation.startTimestamp self.videoContent = videoContent self.videoNode = videoNode let maskPath: UIBezierPath if isForum { maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) } else { maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) } let shape = CAShapeLayer() shape.path = maskPath.cgPath videoNode.layer.mask = shape self.insertSubnode(videoNode, aboveSubnode: self.avatarNode) } } else { if let markupNode = self.markupNode { self.markupNode = nil markupNode.removeFromSupernode() } if let videoNode = self.videoNode { self.videoStartTimestamp = nil self.videoContent = nil self.videoNode = nil videoNode.removeFromSupernode() } } } else if let videoNode = self.videoNode { self.videoStartTimestamp = nil self.videoContent = nil self.videoNode = nil videoNode.removeFromSupernode() } if let markupNode = self.markupNode { markupNode.frame = self.avatarNode.frame markupNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: avatarCornerRadius, transition: .immediate) } if let videoNode = self.videoNode { if self.canAttachVideo { videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) } videoNode.frame = self.avatarNode.frame if isEditing != videoNode.canAttachContent { videoNode.canAttachContent = isEditing && self.canAttachVideo } } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.avatarNode.frame.contains(point) { return self.avatarNode.view } return super.hitTest(point, with: event) } } final class PeerInfoAvatarListNode: ASDisplayNode { private let isSettings: Bool let containerNode: ASDisplayNode let pinchSourceNode: PinchSourceContainerNode let bottomCoverNode: ASDisplayNode fileprivate let maskNode: DynamicIslandMaskNode fileprivate let topCoverNode: DynamicIslandBlurNode let avatarContainerNode: PeerInfoAvatarTransformContainerNode let listContainerTransformNode: ASDisplayNode let listContainerNode: PeerInfoAvatarListContainerNode let isReady = Promise() var arguments: (Peer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)? var item: PeerInfoAvatarListItem? var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? var animateOverlaysFadeIn: (() -> Void)? init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) { self.isSettings = isSettings self.containerNode = ASDisplayNode() self.bottomCoverNode = ASDisplayNode() self.bottomCoverNode.backgroundColor = .black self.maskNode = DynamicIslandMaskNode() self.pinchSourceNode = PinchSourceContainerNode() self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) self.listContainerTransformNode = ASDisplayNode() self.listContainerNode = PeerInfoAvatarListContainerNode(context: context, isSettings: isSettings) self.listContainerNode.clipsToBounds = true self.listContainerNode.isHidden = true self.topCoverNode = DynamicIslandBlurNode() super.init() self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.bottomCoverNode) self.containerNode.addSubnode(self.pinchSourceNode) self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode) self.listContainerTransformNode.addSubnode(self.listContainerNode) self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode) self.containerNode.addSubnode(self.topCoverNode) let avatarReady = (self.avatarContainerNode.avatarNode.ready |> mapToSignal { _ -> Signal in return .complete() } |> then(.single(true))) let galleryReady = self.listContainerNode.isReady.get() |> filter { value in return value } |> take(1) let combinedSignal: Signal if readyWhenGalleryLoads { combinedSignal = combineLatest(queue: .mainQueue(), avatarReady, galleryReady ) |> map { lhs, rhs in return lhs && rhs } } else { combinedSignal = avatarReady } self.isReady.set(combinedSignal |> filter { value in return value } |> take(1)) self.listContainerNode.itemsUpdated = { [weak self] items in if let strongSelf = self { strongSelf.item = items.first strongSelf.itemsUpdated?(items) if let (peer, threadId, threadInfo, theme, avatarSize, isExpanded) = strongSelf.arguments { strongSelf.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings) } } } self.pinchSourceNode.activate = { [weak self] sourceNode in guard let strongSelf = self, let (_, _, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else { return } let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { return UIScreen.main.bounds }) context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) strongSelf.listContainerNode.bottomShadowNode.alpha = 0.0 } self.pinchSourceNode.animatedOut = { [weak self] in guard let strongSelf = self else { return } strongSelf.animateOverlaysFadeIn?() } } func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded) self.maskNode.isForum = isForum self.pinchSourceNode.update(size: size, transition: transition) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size) self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.listContainerNode.isHidden { if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) { return result } } else { if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) { return result } else if let result = self.avatarContainerNode.iconView?.view?.hitTest(self.view.convert(point, to: self.avatarContainerNode.iconView?.view), with: event) { return result } } return super.hitTest(point, with: event) } func animateAvatarCollapse(transition: ContainedViewLayoutTransition) { if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition { if let _ = self.avatarContainerNode.videoNode { } else if let _ = self.avatarContainerNode.markupNode { } else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage { let avatarCopyView = UIImageView() avatarCopyView.image = unroundedImage avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame avatarCopyView.center = currentItemNode.imageNode.position currentItemNode.view.addSubview(avatarCopyView) let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale) avatarCopyView.alpha = 0.0 transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in Queue.mainQueue().after(0.1, { avatarCopyView?.removeFromSuperview() }) }) } } } } private enum MoreIconNodeState: Equatable { case more case search case moreToSearch(Float) } private final class MoreIconNode: ManagedAnimationNode { private let duration: Double = 0.21 private var iconState: MoreIconNodeState = .more init() { super.init(size: CGSize(width: 30.0, height: 30.0)) self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) } func play() { if case .more = self.iconState { self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) } } func enqueueState(_ state: MoreIconNodeState, animated: Bool) { guard self.iconState != state else { return } let previousState = self.iconState self.iconState = state let source = ManagedAnimationSource.local("anim_moretosearch") let totalLength: Int = 90 if animated { switch previousState { case .more: switch state { case .more: break case .search: self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration)) case let .moreToSearch(progress): let frame = Int(progress * Float(totalLength)) let duration = self.duration * Double(progress) self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: frame), duration: duration)) } case .search: switch state { case .more: self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration)) case .search: break case let .moreToSearch(progress): let frame = Int(progress * Float(totalLength)) let duration = self.duration * Double((1.0 - progress)) self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: frame), duration: duration)) } case let .moreToSearch(currentProgress): let currentFrame = Int(currentProgress * Float(totalLength)) switch state { case .more: let duration = self.duration * Double(currentProgress) self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: 0), duration: duration)) case .search: let duration = self.duration * (1.0 - Double(currentProgress)) self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: totalLength), duration: duration)) case let .moreToSearch(progress): let frame = Int(progress * Float(totalLength)) let duration = self.duration * Double(abs(currentProgress - progress)) self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: frame), duration: duration)) } } } else { switch state { case .more: self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) case .search: self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0)) case let .moreToSearch(progress): let frame = Int(progress * Float(totalLength)) self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: frame, endFrame: frame), duration: 0.0)) } } } } final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { let containerNode: ContextControllerSourceNode let contextSourceNode: ContextReferenceContentNode private let regularTextNode: ImmediateTextNode private let whiteTextNode: ImmediateTextNode private let iconNode: ASImageNode private var animationNode: MoreIconNode? private var key: PeerInfoHeaderNavigationButtonKey? private var theme: PresentationTheme? var isWhite: Bool = false { didSet { if self.isWhite != oldValue { if case .qrCode = self.key, let theme = self.theme { self.iconNode.image = self.isWhite ? generateTintedImage(image: PresentationResourcesRootController.navigationQrCodeIcon(theme), color: .white) : PresentationResourcesRootController.navigationQrCodeIcon(theme) } self.regularTextNode.isHidden = self.isWhite self.whiteTextNode.isHidden = !self.isWhite } } } var action: ((ASDisplayNode, ContextGesture?) -> Void)? init() { self.contextSourceNode = ContextReferenceContentNode() self.containerNode = ContextControllerSourceNode() self.containerNode.animateScale = false self.regularTextNode = ImmediateTextNode() self.whiteTextNode = ImmediateTextNode() self.whiteTextNode.isHidden = true self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true super.init(pointerStyle: .insetRectangle(-8.0, 2.0)) self.isAccessibilityElement = true self.accessibilityTraits = .button self.containerNode.addSubnode(self.contextSourceNode) self.contextSourceNode.addSubnode(self.regularTextNode) self.contextSourceNode.addSubnode(self.whiteTextNode) self.contextSourceNode.addSubnode(self.iconNode) self.addSubnode(self.containerNode) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self else { return } strongSelf.action?(strongSelf.contextSourceNode, gesture) } self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } @objc private func pressed() { self.animationNode?.play() self.action?(self.contextSourceNode, nil) } func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData, height: CGFloat) -> CGSize { let textSize: CGSize let isFirstTime = self.key == nil if self.key != key || self.theme !== presentationData.theme { self.key = key self.theme = presentationData.theme let text: String var accessibilityText: String var icon: UIImage? var isBold = false var isGestureEnabled = false var isAnimation = false var animationState: MoreIconNodeState = .more switch key { case .edit: text = presentationData.strings.Common_Edit accessibilityText = text case .done, .cancel, .selectionDone: text = presentationData.strings.Common_Done accessibilityText = text isBold = true case .select: text = presentationData.strings.Common_Select accessibilityText = text case .search: text = "" accessibilityText = presentationData.strings.Common_Search icon = nil// PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme) isAnimation = true animationState = .search case .editPhoto: text = presentationData.strings.Settings_EditPhoto accessibilityText = text case .editVideo: text = presentationData.strings.Settings_EditVideo accessibilityText = text case .more: text = "" accessibilityText = presentationData.strings.Common_More icon = nil// PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme) isGestureEnabled = true isAnimation = true animationState = .more case .qrCode: text = "" accessibilityText = presentationData.strings.PeerInfo_QRCode_Title icon = PresentationResourcesRootController.navigationQrCodeIcon(presentationData.theme) case .moreToSearch: text = "" accessibilityText = "" } self.accessibilityLabel = accessibilityText self.containerNode.isGestureEnabled = isGestureEnabled let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0) self.regularTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: presentationData.theme.rootController.navigationBar.accentTextColor) self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white) self.iconNode.image = icon if isAnimation { self.iconNode.isHidden = true let animationNode: MoreIconNode if let current = self.animationNode { animationNode = current } else { animationNode = MoreIconNode() self.animationNode = animationNode self.contextSourceNode.addSubnode(animationNode) } animationNode.customColor = presentationData.theme.rootController.navigationBar.accentTextColor animationNode.enqueueState(animationState, animated: !isFirstTime) } else { self.iconNode.isHidden = false if let current = self.animationNode { self.animationNode = nil current.removeFromSupernode() } } textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) } else { textSize = self.regularTextNode.bounds.size } let inset: CGFloat = 0.0 let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) self.regularTextNode.frame = textFrame self.whiteTextNode.frame = textFrame if let animationNode = self.animationNode { let animationSize = CGSize(width: 30.0, height: 30.0) animationNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - animationSize.height) / 2.0)), size: animationSize) let size = CGSize(width: animationSize.width + inset * 2.0, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) return size } else if let image = self.iconNode.image { self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size) let size = CGSize(width: image.size.width + inset * 2.0, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) return size } else { let size = CGSize(width: textSize.width + inset * 2.0, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) return size } } } enum PeerInfoHeaderNavigationButtonKey { case edit case done case cancel case select case selectionDone case search case editPhoto case editVideo case more case qrCode case moreToSearch } struct PeerInfoHeaderNavigationButtonSpec: Equatable { let key: PeerInfoHeaderNavigationButtonKey let isForExpandedView: Bool } final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { private var presentationData: PresentationData? private(set) var leftButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] private(set) var rightButtonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] private var currentLeftButtons: [PeerInfoHeaderNavigationButtonSpec] = [] private var currentRightButtons: [PeerInfoHeaderNavigationButtonSpec] = [] var isWhite: Bool = false { didSet { if self.isWhite != oldValue { for (_, buttonNode) in self.leftButtonNodes { buttonNode.isWhite = self.isWhite } for (_, buttonNode) in self.rightButtonNodes { buttonNode.isWhite = self.isWhite } } } } var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)? func update(size: CGSize, presentationData: PresentationData, leftButtons: [PeerInfoHeaderNavigationButtonSpec], rightButtons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, transition: ContainedViewLayoutTransition) { let maximumExpandOffset: CGFloat = 14.0 let expandOffset: CGFloat = -expandFraction * maximumExpandOffset if self.currentLeftButtons != leftButtons || presentationData.strings !== self.presentationData?.strings { self.currentLeftButtons = leftButtons var nextRegularButtonOrigin = 16.0 var nextExpandedButtonOrigin = 16.0 for spec in leftButtons.reversed() { let buttonNode: PeerInfoHeaderNavigationButton var wasAdded = false if let current = self.leftButtonNodes[spec.key] { buttonNode = current } else { wasAdded = true buttonNode = PeerInfoHeaderNavigationButton() self.leftButtonNodes[spec.key] = buttonNode self.addSubnode(buttonNode) buttonNode.isWhite = self.isWhite buttonNode.action = { [weak self] _, gesture in guard let strongSelf = self, let buttonNode = strongSelf.leftButtonNodes[spec.key] else { return } strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture) } } let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height) var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) nextButtonOrigin += buttonSize.width + 4.0 if spec.isForExpandedView { nextExpandedButtonOrigin = nextButtonOrigin } else { nextRegularButtonOrigin = nextButtonOrigin } let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) if wasAdded { buttonNode.frame = buttonFrame buttonNode.alpha = 0.0 transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) } else { transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) } } var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] for (key, _) in self.leftButtonNodes { if !leftButtons.contains(where: { $0.key == key }) { removeKeys.append(key) } } for key in removeKeys { if let buttonNode = self.leftButtonNodes.removeValue(forKey: key) { buttonNode.removeFromSupernode() } } } else { var nextRegularButtonOrigin = 16.0 var nextExpandedButtonOrigin = 16.0 for spec in leftButtons.reversed() { if let buttonNode = self.leftButtonNodes[spec.key] { let buttonSize = buttonNode.bounds.size var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) nextButtonOrigin += buttonSize.width + 4.0 if spec.isForExpandedView { nextExpandedButtonOrigin = nextButtonOrigin } else { nextRegularButtonOrigin = nextButtonOrigin } transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) var buttonTransition = transition if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 { buttonTransition = .animated(duration: duration * 0.25, curve: curve) } buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) } } } if self.currentRightButtons != rightButtons || presentationData.strings !== self.presentationData?.strings { self.currentRightButtons = rightButtons var nextRegularButtonOrigin = size.width - 16.0 var nextExpandedButtonOrigin = size.width - 16.0 for spec in rightButtons.reversed() { let buttonNode: PeerInfoHeaderNavigationButton var wasAdded = false var key = spec.key if key == .more || key == .search { key = .moreToSearch } if let current = self.rightButtonNodes[key] { buttonNode = current } else { wasAdded = true buttonNode = PeerInfoHeaderNavigationButton() self.rightButtonNodes[key] = buttonNode self.addSubnode(buttonNode) buttonNode.isWhite = self.isWhite } buttonNode.action = { [weak self] _, gesture in guard let strongSelf = self, let buttonNode = strongSelf.rightButtonNodes[key] else { return } strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture) } let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height) var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) nextButtonOrigin -= buttonSize.width + 4.0 if spec.isForExpandedView { nextExpandedButtonOrigin = nextButtonOrigin } else { nextRegularButtonOrigin = nextButtonOrigin } let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) if wasAdded { if key == .moreToSearch { buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } buttonNode.frame = buttonFrame buttonNode.alpha = 0.0 transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) } else { transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) } } var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] for (key, _) in self.rightButtonNodes { if key == .moreToSearch { if !rightButtons.contains(where: { $0.key == .more || $0.key == .search }) { removeKeys.append(key) } } else if !rightButtons.contains(where: { $0.key == key }) { removeKeys.append(key) } } for key in removeKeys { if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) { if key == .moreToSearch { buttonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in buttonNode?.removeFromSupernode() }) buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } else { buttonNode.removeFromSupernode() } } } } else { var nextRegularButtonOrigin = size.width - 16.0 var nextExpandedButtonOrigin = size.width - 16.0 for spec in rightButtons.reversed() { var key = spec.key if key == .more || key == .search { key = .moreToSearch } if let buttonNode = self.rightButtonNodes[key] { let buttonSize = buttonNode.bounds.size var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) nextButtonOrigin -= buttonSize.width + 4.0 if spec.isForExpandedView { nextExpandedButtonOrigin = nextButtonOrigin } else { nextRegularButtonOrigin = nextButtonOrigin } transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) var buttonTransition = transition if case let .animated(duration, curve) = buttonTransition, alphaFactor == 0.0 { buttonTransition = .animated(duration: duration * 0.25, curve: curve) } buttonTransition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) } } } self.presentationData = presentationData } } final class PeerInfoHeaderRegularContentNode: ASDisplayNode { } enum PeerInfoHeaderTextFieldNodeKey: Equatable { case firstName case lastName case title case description } protocol PeerInfoHeaderTextFieldNode: ASDisplayNode { var text: String { get } func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat } final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, UITextFieldDelegate { private let backgroundNode: ASDisplayNode private let textNode: TextFieldNode private let measureTextNode: ImmediateTextNode private let clearIconNode: ASImageNode private let clearButtonNode: HighlightableButtonNode private let topSeparator: ASDisplayNode private let maskNode: ASImageNode private var theme: PresentationTheme? var text: String { return self.textNode.textField.text ?? "" } override init() { self.backgroundNode = ASDisplayNode() self.textNode = TextFieldNode() self.measureTextNode = ImmediateTextNode() self.measureTextNode.maximumNumberOfLines = 0 self.clearIconNode = ASImageNode() self.clearIconNode.isLayerBacked = true self.clearIconNode.displayWithoutProcessing = true self.clearIconNode.displaysAsynchronously = false self.clearIconNode.isHidden = true self.clearButtonNode = HighlightableButtonNode() self.clearButtonNode.isHidden = true self.clearButtonNode.isAccessibilityElement = false self.topSeparator = ASDisplayNode() self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.textNode) self.addSubnode(self.clearIconNode) self.addSubnode(self.clearButtonNode) self.addSubnode(self.topSeparator) self.addSubnode(self.maskNode) self.textNode.textField.delegate = self self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) self.clearButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.clearIconNode.alpha = 0.4 } else { strongSelf.clearIconNode.alpha = 1.0 strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } } @objc private func clearButtonPressed() { self.textNode.textField.text = "" self.updateClearButtonVisibility() } @objc func textFieldDidBeginEditing(_ textField: UITextField) { self.updateClearButtonVisibility() } @objc func textFieldDidEndEditing(_ textField: UITextField) { self.updateClearButtonVisibility() } private func updateClearButtonVisibility() { let isHidden = !self.textNode.textField.isFirstResponder || self.text.isEmpty self.clearIconNode.isHidden = isHidden self.clearButtonNode.isHidden = isHidden self.clearButtonNode.isAccessibilityElement = isHidden } func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) self.textNode.textField.font = titleFont if self.theme !== presentationData.theme { self.theme = presentationData.theme self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor self.textNode.textField.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) } let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { self.textNode.textField.attributedPlaceholder = attributedPlaceholderText self.textNode.textField.accessibilityHint = attributedPlaceholderText.string } if let updateText = updateText { self.textNode.textField.text = updateText } if !hasPrevious { self.topSeparator.isHidden = true } self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0) self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset, height: UIScreenPixel)) let measureText = "|" let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .black) self.measureTextNode.attributedText = attributedMeasureText let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) let height = measureTextSize.height + 22.0 let buttonSize = CGSize(width: 38.0, height: height) self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) if let image = self.clearIconNode.image { self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) } self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height)) self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - safeInset * 2.0 - 16.0 * 2.0 - 38.0), height: 40.0)) let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext) let hasTopCorners = hasCorners && !hasPrevious let hasBottomCorners = hasCorners && !hasNext self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height)) self.textNode.isUserInteractionEnabled = isEnabled self.textNode.alpha = isEnabled ? 1.0 : 0.6 return height } } final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode, ASEditableTextNodeDelegate { private let backgroundNode: ASDisplayNode private let textNode: EditableTextNode private let textNodeContainer: ASDisplayNode private let measureTextNode: ImmediateTextNode private let clearIconNode: ASImageNode private let clearButtonNode: HighlightableButtonNode private let topSeparator: ASDisplayNode private let maskNode: ASImageNode private let requestUpdateHeight: () -> Void private var fontSize: PresentationFontSize? private var theme: PresentationTheme? private var currentParams: (width: CGFloat, safeInset: CGFloat)? private var currentMeasuredHeight: CGFloat? var text: String { return self.textNode.attributedText?.string ?? "" } init(requestUpdateHeight: @escaping () -> Void) { self.requestUpdateHeight = requestUpdateHeight self.backgroundNode = ASDisplayNode() self.textNode = EditableTextNode() self.textNode.clipsToBounds = false self.textNode.textView.clipsToBounds = false self.textNode.textContainerInset = UIEdgeInsets() self.textNodeContainer = ASDisplayNode() self.measureTextNode = ImmediateTextNode() self.measureTextNode.maximumNumberOfLines = 0 self.measureTextNode.isUserInteractionEnabled = false self.measureTextNode.lineSpacing = 0.1 self.topSeparator = ASDisplayNode() self.clearIconNode = ASImageNode() self.clearIconNode.isLayerBacked = true self.clearIconNode.displayWithoutProcessing = true self.clearIconNode.displaysAsynchronously = false self.clearIconNode.isHidden = true self.clearButtonNode = HighlightableButtonNode() self.clearButtonNode.isHidden = true self.clearButtonNode.isAccessibilityElement = false self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.backgroundNode) self.textNodeContainer.addSubnode(self.textNode) self.addSubnode(self.textNodeContainer) self.addSubnode(self.clearIconNode) self.addSubnode(self.clearButtonNode) self.addSubnode(self.topSeparator) self.addSubnode(self.maskNode) self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) self.clearButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.clearIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.clearIconNode.alpha = 0.4 } else { strongSelf.clearIconNode.alpha = 1.0 strongSelf.clearIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } } @objc private func clearButtonPressed() { guard let theme = self.theme else { return } let font: UIFont if let fontSize = self.fontSize { font = Font.regular(fontSize.itemListBaseFontSize) } else { font = Font.regular(17.0) } let attributedText = NSAttributedString(string: "", font: font, textColor: theme.list.itemPrimaryTextColor) self.textNode.attributedText = attributedText self.requestUpdateHeight() self.updateClearButtonVisibility() } func update(width: CGFloat, safeInset: CGFloat, isSettings: Bool, hasPrevious: Bool, hasNext: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { self.currentParams = (width, safeInset) self.fontSize = presentationData.listsFontSize let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize) if self.theme !== presentationData.theme { self.theme = presentationData.theme self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor let textColor = presentationData.theme.list.itemPrimaryTextColor self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: titleFont, NSAttributedString.Key.foregroundColor.rawValue: textColor] self.textNode.keyboardAppearance = presentationData.theme.rootController.keyboardColor.keyboardAppearance self.textNode.tintColor = presentationData.theme.list.itemAccentColor self.textNode.clipsToBounds = true self.textNode.delegate = self self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) self.clearIconNode.image = PresentationResourcesItemList.itemListClearInputIcon(presentationData.theme) } self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor let separatorX = safeInset + (hasPrevious ? 16.0 : 0.0) self.topSeparator.frame = CGRect(origin: CGPoint(x: separatorX, y: 0.0), size: CGSize(width: width - separatorX - safeInset, height: UIScreenPixel)) let attributedPlaceholderText = NSAttributedString(string: placeholder, font: titleFont, textColor: presentationData.theme.list.itemPlaceholderTextColor) if self.textNode.attributedPlaceholderText == nil || !self.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) { self.textNode.attributedPlaceholderText = attributedPlaceholderText } if let updateText = updateText { let attributedText = NSAttributedString(string: updateText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor) self.textNode.attributedText = attributedText } var measureText = self.textNode.attributedText?.string ?? "" if measureText.hasSuffix("\n") || measureText.isEmpty { measureText += "|" } let attributedMeasureText = NSAttributedString(string: measureText, font: titleFont, textColor: .gray) self.measureTextNode.attributedText = attributedMeasureText let measureTextSize = self.measureTextNode.updateLayout(CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: .greatestFiniteMagnitude)) self.measureTextNode.frame = CGRect(origin: CGPoint(), size: measureTextSize) self.currentMeasuredHeight = measureTextSize.height let height = measureTextSize.height + 22.0 let buttonSize = CGSize(width: 38.0, height: height) self.clearButtonNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width, y: 0.0), size: buttonSize) if let image = self.clearIconNode.image { self.clearIconNode.frame = CGRect(origin: CGPoint(x: width - safeInset - buttonSize.width + floor((buttonSize.width - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) } let textNodeFrame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: 10.0), size: CGSize(width: width - safeInset * 2.0 - 16.0 * 2.0 - 38.0, height: max(height, 1000.0))) self.textNodeContainer.frame = textNodeFrame self.textNode.frame = CGRect(origin: CGPoint(), size: textNodeFrame.size) self.backgroundNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: max(1.0, width - safeInset * 2.0), height: height)) let hasCorners = safeInset > 0.0 && (!hasPrevious || !hasNext) let hasTopCorners = hasCorners && !hasPrevious let hasBottomCorners = hasCorners && !hasNext self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil self.maskNode.frame = CGRect(origin: CGPoint(x: safeInset, y: 0.0), size: CGSize(width: width - safeInset - safeInset, height: height)) return height } func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { self.updateClearButtonVisibility() } func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { self.updateClearButtonVisibility() } private func updateClearButtonVisibility() { let isHidden = !self.textNode.isFirstResponder() || self.text.isEmpty self.clearIconNode.isHidden = isHidden self.clearButtonNode.isHidden = isHidden self.clearButtonNode.isAccessibilityElement = isHidden } func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { guard let theme = self.theme else { return true } let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) if updatedText.count > 255 { let attributedText = NSAttributedString(string: String(updatedText[updatedText.startIndex.. 0.1 { self.requestUpdateHeight() } } } func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { let text: String? = UIPasteboard.general.string if let _ = text { return true } else { return false } } } final class PeerInfoHeaderEditingContentNode: ASDisplayNode { private let context: AccountContext private let requestUpdateLayout: () -> Void var requestEditing: (() -> Void)? let avatarNode: PeerInfoEditingAvatarNode let avatarTextNode: ImmediateTextNode let avatarButtonNode: HighlightableButtonNode var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:] init(context: AccountContext, requestUpdateLayout: @escaping () -> Void) { self.context = context self.requestUpdateLayout = requestUpdateLayout self.avatarNode = PeerInfoEditingAvatarNode(context: context) self.avatarTextNode = ImmediateTextNode() self.avatarButtonNode = HighlightableButtonNode() super.init() self.addSubnode(self.avatarNode) self.avatarButtonNode.addSubnode(self.avatarTextNode) self.avatarButtonNode.addTarget(self, action: #selector(textPressed), forControlEvents: .touchUpInside) } @objc private func textPressed() { self.requestEditing?() } func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? { return self.itemNodes[key]?.text } func shakeTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) { self.itemNodes[key]?.layer.addShakeAnimation() } func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0 let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize)) transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) var contentHeight: CGFloat = statusBarHeight + 10.0 + avatarSize + 20.0 if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { if self.avatarButtonNode.supernode == nil { self.addSubnode(self.avatarButtonNode) } self.avatarTextNode.attributedText = NSAttributedString(string: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor) self.avatarButtonNode.accessibilityLabel = self.avatarTextNode.attributedText?.string let avatarTextSize = self.avatarTextNode.updateLayout(CGSize(width: width, height: 32.0)) transition.updateFrame(node: self.avatarTextNode, frame: CGRect(origin: CGPoint(), size: avatarTextSize)) transition.updateFrame(node: self.avatarButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - avatarTextSize.width) / 2.0), y: contentHeight - 1.0), size: avatarTextSize)) contentHeight += 32.0 } var isEditableBot = false if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { isEditableBot = true } var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] if let user = peer as? TelegramUser { if !user.isDeleted { fieldKeys.append(.firstName) if isEditableBot { fieldKeys.append(.description) } else if user.botInfo == nil { fieldKeys.append(.lastName) } } } else if let _ = peer as? TelegramGroup { fieldKeys.append(.title) if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { fieldKeys.append(.description) } } else if let _ = peer as? TelegramChannel { fieldKeys.append(.title) if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { fieldKeys.append(.description) } } var hasPrevious = false for key in fieldKeys { let itemNode: PeerInfoHeaderTextFieldNode var updateText: String? if let current = self.itemNodes[key] { itemNode = current } else { var isMultiline = false switch key { case .firstName: if let peer = peer as? TelegramUser { if let editableBotInfo = (cachedData as? CachedUserData)?.editableBotInfo { updateText = editableBotInfo.name } else { updateText = peer.firstName ?? "" } } case .lastName: updateText = (peer as? TelegramUser)?.lastName ?? "" case .title: updateText = peer?.debugDisplayTitle ?? "" case .description: isMultiline = true if let cachedData = cachedData as? CachedChannelData { updateText = cachedData.about ?? "" } else if let cachedData = cachedData as? CachedGroupData { updateText = cachedData.about ?? "" } else if let cachedData = cachedData as? CachedUserData { if let editableBotInfo = cachedData.editableBotInfo { updateText = editableBotInfo.about } else { updateText = cachedData.about ?? "" } } else { updateText = "" } } if isMultiline { itemNode = PeerInfoHeaderMultiLineTextFieldNode(requestUpdateHeight: { [weak self] in self?.requestUpdateLayout() }) } else { itemNode = PeerInfoHeaderSingleLineTextFieldNode() } self.itemNodes[key] = itemNode self.addSubnode(itemNode) } let placeholder: String var isEnabled = true switch key { case .firstName: placeholder = isEditableBot ? presentationData.strings.UserInfo_BotNamePlaceholder : presentationData.strings.UserInfo_FirstNamePlaceholder isEnabled = isContact || isSettings || isEditableBot case .lastName: placeholder = presentationData.strings.UserInfo_LastNamePlaceholder isEnabled = isContact || isSettings case .title: if let channel = peer as? TelegramChannel, case .broadcast = channel.info { placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder } else { placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder } isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) case .description: placeholder = presentationData.strings.Channel_Edit_AboutItem isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot } let itemHeight = itemNode.update(width: width, safeInset: safeInset, isSettings: isSettings, hasPrevious: hasPrevious, hasNext: key != fieldKeys.last, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText) transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))) contentHeight += itemHeight hasPrevious = true } var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = [] for (key, _) in self.itemNodes { if !fieldKeys.contains(key) { removeKeys.append(key) } } for key in removeKeys { if let itemNode = self.itemNodes.removeValue(forKey: key) { itemNode.removeFromSupernode() } } return contentHeight } } private let TitleNodeStateRegular = 0 private let TitleNodeStateExpanded = 1 final class PeerInfoHeaderNode: ASDisplayNode { private var context: AccountContext private var presentationData: PresentationData? private var state: PeerInfoState? private var peer: Peer? private var threadData: MessageHistoryThreadData? private var avatarSize: CGFloat? private let isOpenedFromChat: Bool private let isSettings: Bool private let videoCallsEnabled: Bool private let forumTopicThreadId: Int64? private let chatLocation: ChatLocation private(set) var isAvatarExpanded: Bool var skipCollapseCompletion = false var ignoreCollapse = false let avatarClippingNode: SparseNode let avatarListNode: PeerInfoAvatarListNode let buttonsContainerNode: SparseNode let regularContentNode: PeerInfoHeaderRegularContentNode let editingContentNode: PeerInfoHeaderEditingContentNode let avatarOverlayNode: PeerInfoEditingAvatarOverlayNode let titleNodeContainer: ASDisplayNode let titleNodeRawContainer: ASDisplayNode let titleNode: MultiScaleTextNode let titleCredibilityIconView: ComponentHostView var credibilityIconSize: CGSize? let titleExpandedCredibilityIconView: ComponentHostView var titleExpandedCredibilityIconSize: CGSize? let subtitleNodeContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode let subtitleNode: MultiScaleTextNode var subtitleBackgroundNode: ASDisplayNode? var subtitleBackgroundButton: HighlightTrackingButtonNode? var subtitleArrowNode: ASImageNode? let panelSubtitleNode: MultiScaleTextNode let nextPanelSubtitleNode: MultiScaleTextNode let usernameNodeContainer: ASDisplayNode let usernameNodeRawContainer: ASDisplayNode let usernameNode: MultiScaleTextNode var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:] let backgroundNode: NavigationBackgroundNode let expandedBackgroundNode: NavigationBackgroundNode let separatorNode: ASDisplayNode let navigationBackgroundNode: ASDisplayNode let navigationBackgroundBackgroundNode: ASDisplayNode var navigationTitle: String? let navigationTitleNode: ImmediateTextNode let navigationSeparatorNode: ASDisplayNode let navigationButtonContainer: PeerInfoHeaderNavigationButtonContainerNode var performButtonAction: ((PeerInfoHeaderButtonKey, ContextGesture?) -> Void)? var requestAvatarExpansion: ((Bool, [AvatarGalleryEntry], AvatarGalleryEntry?, (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?) -> Void)? var requestOpenAvatarForEditing: ((Bool) -> Void)? var cancelUpload: (() -> Void)? var requestUpdateLayout: ((Bool) -> Void)? var animateOverlaysFadeIn: (() -> Void)? var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)? var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)? var displayEmojiPackTooltip: (() -> Void)? var displayPremiumIntro: ((UIView, PeerEmojiStatus?, Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError>, Bool) -> Void)? var navigateToForum: (() -> Void)? var navigationTransition: PeerInfoHeaderNavigationTransition? var backgroundAlpha: CGFloat = 1.0 var updateHeaderAlpha: ((CGFloat, ContainedViewLayoutTransition) -> Void)? let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer var emojiStatusPackDisposable = MetaDisposable() var emojiStatusFileAndPackTitle = Promise<(TelegramMediaFile, LoadedStickerPack)?>() private var validLayout: (width: CGFloat, deviceMetrics: DeviceMetrics)? init(context: AccountContext, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, forumTopicThreadId: Int64?, chatLocation: ChatLocation) { self.context = context self.isAvatarExpanded = avatarInitiallyExpanded self.isOpenedFromChat = isOpenedFromChat self.isSettings = isSettings self.videoCallsEnabled = true self.forumTopicThreadId = forumTopicThreadId self.chatLocation = chatLocation self.avatarClippingNode = SparseNode() self.avatarClippingNode.clipsToBounds = true self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded, isSettings: isSettings) self.titleNodeContainer = ASDisplayNode() self.titleNodeRawContainer = ASDisplayNode() self.titleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.titleNode.displaysAsynchronously = false self.titleCredibilityIconView = ComponentHostView() self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.view.addSubview(self.titleCredibilityIconView) self.titleExpandedCredibilityIconView = ComponentHostView() self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.view.addSubview(self.titleExpandedCredibilityIconView) self.subtitleNodeContainer = ASDisplayNode() self.subtitleNodeRawContainer = ASDisplayNode() self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.subtitleNode.displaysAsynchronously = false self.panelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.panelSubtitleNode.displaysAsynchronously = false self.nextPanelSubtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.nextPanelSubtitleNode.displaysAsynchronously = false self.usernameNodeContainer = ASDisplayNode() self.usernameNodeRawContainer = ASDisplayNode() self.usernameNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) self.usernameNode.displaysAsynchronously = false self.buttonsContainerNode = SparseNode() self.buttonsContainerNode.clipsToBounds = true self.regularContentNode = PeerInfoHeaderRegularContentNode() var requestUpdateLayoutImpl: (() -> Void)? self.editingContentNode = PeerInfoHeaderEditingContentNode(context: context, requestUpdateLayout: { requestUpdateLayoutImpl?() }) self.editingContentNode.alpha = 0.0 self.avatarOverlayNode = PeerInfoEditingAvatarOverlayNode(context: context) self.avatarOverlayNode.isUserInteractionEnabled = false self.navigationBackgroundNode = ASDisplayNode() self.navigationBackgroundNode.isHidden = true self.navigationBackgroundNode.isUserInteractionEnabled = false self.navigationBackgroundBackgroundNode = ASDisplayNode() self.navigationBackgroundBackgroundNode.isUserInteractionEnabled = false self.navigationTitleNode = ImmediateTextNode() self.navigationSeparatorNode = ASDisplayNode() self.navigationButtonContainer = PeerInfoHeaderNavigationButtonContainerNode() self.backgroundNode = NavigationBackgroundNode(color: .clear) self.backgroundNode.isHidden = true self.backgroundNode.isUserInteractionEnabled = false self.expandedBackgroundNode = NavigationBackgroundNode(color: .clear) self.expandedBackgroundNode.isHidden = false self.expandedBackgroundNode.isUserInteractionEnabled = false self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer super.init() requestUpdateLayoutImpl = { [weak self] in self?.requestUpdateLayout?(false) } if !isMediaOnly { self.addSubnode(self.buttonsContainerNode) } self.addSubnode(self.backgroundNode) self.addSubnode(self.expandedBackgroundNode) self.titleNodeContainer.addSubnode(self.titleNode) self.subtitleNodeContainer.addSubnode(self.subtitleNode) self.subtitleNodeContainer.addSubnode(self.panelSubtitleNode) // self.subtitleNodeContainer.addSubnode(self.nextPanelSubtitleNode) self.usernameNodeContainer.addSubnode(self.usernameNode) self.regularContentNode.addSubnode(self.avatarClippingNode) self.avatarClippingNode.addSubnode(self.avatarListNode) self.regularContentNode.addSubnode(self.avatarListNode.listContainerNode.controlsClippingOffsetNode) self.regularContentNode.addSubnode(self.titleNodeContainer) self.regularContentNode.addSubnode(self.subtitleNodeContainer) self.regularContentNode.addSubnode(self.subtitleNodeRawContainer) self.regularContentNode.addSubnode(self.usernameNodeContainer) self.regularContentNode.addSubnode(self.usernameNodeRawContainer) self.addSubnode(self.regularContentNode) self.addSubnode(self.editingContentNode) self.addSubnode(self.avatarOverlayNode) self.addSubnode(self.navigationBackgroundNode) self.navigationBackgroundNode.addSubnode(self.navigationBackgroundBackgroundNode) self.navigationBackgroundNode.addSubnode(self.navigationTitleNode) self.navigationBackgroundNode.addSubnode(self.navigationSeparatorNode) self.addSubnode(self.navigationButtonContainer) self.addSubnode(self.separatorNode) self.avatarListNode.avatarContainerNode.tapped = { [weak self] in self?.initiateAvatarExpansion(gallery: false, first: false) } self.avatarListNode.avatarContainerNode.contextAction = { [weak self] node, gesture in self?.displayAvatarContextMenu?(node, gesture) } self.avatarListNode.avatarContainerNode.emojiTapped = { [weak self] in self?.displayEmojiPackTooltip?() } self.editingContentNode.avatarNode.tapped = { [weak self] confirm in self?.initiateAvatarExpansion(gallery: true, first: true) } self.editingContentNode.requestEditing = { [weak self] in self?.requestOpenAvatarForEditing?(true) } self.avatarListNode.itemsUpdated = { [weak self] items in guard let strongSelf = self, let state = strongSelf.state, let peer = strongSelf.peer, let presentationData = strongSelf.presentationData, let avatarSize = strongSelf.avatarSize else { return } strongSelf.editingContentNode.avatarNode.update(peer: peer, threadData: strongSelf.threadData, chatLocation: chatLocation, item: strongSelf.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) } self.avatarListNode.animateOverlaysFadeIn = { [weak self] in guard let strongSelf = self else { return } strongSelf.navigationButtonContainer.layer.animateAlpha(from: 0.0, to: strongSelf.navigationButtonContainer.alpha, duration: 0.25) strongSelf.avatarListNode.listContainerNode.topShadowNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.topShadowNode.alpha, duration: 0.25) strongSelf.avatarListNode.listContainerNode.bottomShadowNode.alpha = 1.0 strongSelf.avatarListNode.listContainerNode.bottomShadowNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.bottomShadowNode.alpha, duration: 0.25) strongSelf.avatarListNode.listContainerNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: strongSelf.avatarListNode.listContainerNode.controlsContainerNode.alpha, duration: 0.25) strongSelf.titleNode.layer.animateAlpha(from: 0.0, to: strongSelf.titleNode.alpha, duration: 0.25) strongSelf.subtitleNode.layer.animateAlpha(from: 0.0, to: strongSelf.subtitleNode.alpha, duration: 0.25) strongSelf.animateOverlaysFadeIn?() } } deinit { self.emojiStatusPackDisposable.dispose() } override func didLoad() { super.didLoad() let usernameGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleUsernameLongPress(_:))) self.usernameNodeRawContainer.view.addGestureRecognizer(usernameGestureRecognizer) let phoneGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePhoneLongPress(_:))) self.subtitleNodeRawContainer.view.addGestureRecognizer(phoneGestureRecognizer) } @objc private func handleUsernameLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began { self.displayCopyContextMenu?(self.usernameNodeRawContainer, !self.isAvatarExpanded, true) } } @objc private func handlePhoneLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began { self.displayCopyContextMenu?(self.subtitleNodeRawContainer, true, !self.isAvatarExpanded) } } @objc private func subtitleBackgroundPressed() { self.navigateToForum?() } func invokeDisplayPremiumIntro() { self.displayPremiumIntro?(self.isAvatarExpanded ? self.titleExpandedCredibilityIconView : self.titleCredibilityIconView, nil, .never(), self.isAvatarExpanded) } func initiateAvatarExpansion(gallery: Bool, first: Bool) { if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery { self.requestOpenAvatarForEditing?(false) return } if self.isAvatarExpanded || gallery { if let currentEntry = self.avatarListNode.listContainerNode.currentEntry, let firstEntry = self.avatarListNode.listContainerNode.galleryEntries.first { let entry = first ? firstEntry : currentEntry self.requestAvatarExpansion?(true, self.avatarListNode.listContainerNode.galleryEntries, entry, self.avatarTransitionArguments(entry: currentEntry)) } } else if let entry = self.avatarListNode.listContainerNode.galleryEntries.first { let _ = self.avatarListNode.avatarContainerNode.avatarNode self.requestAvatarExpansion?(false, self.avatarListNode.listContainerNode.galleryEntries, nil, self.avatarTransitionArguments(entry: entry)) } else { self.cancelUpload?() } } func avatarTransitionArguments(entry: AvatarGalleryEntry) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.isAvatarExpanded { if let avatarNode = self.avatarListNode.listContainerNode.currentItemNode?.imageNode { return (avatarNode, avatarNode.bounds, { [weak avatarNode] in return (avatarNode?.view.snapshotContentTree(unhide: true), nil) }) } else { return nil } } else if entry == self.avatarListNode.listContainerNode.galleryEntries.first { let avatarNode = self.avatarListNode.avatarContainerNode.avatarNode return (avatarNode, avatarNode.bounds, { [weak avatarNode] in return (avatarNode?.view.snapshotContentTree(unhide: true), nil) }) } else { return nil } } func addToAvatarTransitionSurface(view: UIView) { if self.isAvatarExpanded { self.avatarListNode.listContainerNode.view.addSubview(view) } else { self.view.addSubview(view) } } func updateAvatarIsHidden(entry: AvatarGalleryEntry?) { if let entry = entry { self.avatarListNode.avatarContainerNode.containerNode.isHidden = entry == self.avatarListNode.listContainerNode.galleryEntries.first self.editingContentNode.avatarNode.isHidden = entry == self.avatarListNode.listContainerNode.galleryEntries.first } else { self.avatarListNode.avatarContainerNode.containerNode.isHidden = false self.editingContentNode.avatarNode.isHidden = false } self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry) } private enum CredibilityIcon: Equatable { case none case premium case verified case fake case scam case emojiStatus(PeerEmojiStatus) } private var currentCredibilityIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { self.state = state self.peer = peer self.threadData = threadData self.avatarListNode.listContainerNode.peer = peer let isFirstTime = self.validLayout == nil self.validLayout = (width, deviceMetrics) let previousPanelStatusData = self.currentPanelStatusData self.currentPanelStatusData = panelStatusData.0 let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0 self.avatarSize = avatarSize var contentOffset = contentOffset if isMediaOnly { if isModalOverlay { contentOffset = 312.0 } else { contentOffset = 212.0 } } let isLandscape = containerInset > 16.0 let themeUpdated = self.presentationData?.theme !== presentationData.theme self.presentationData = presentationData let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let credibilityIcon: CredibilityIcon if let peer = peer { if peer.isFake { credibilityIcon = .fake } else if peer.isScam { credibilityIcon = .scam } else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { credibilityIcon = .emojiStatus(emojiStatus) } else if peer.isVerified { credibilityIcon = .verified } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != self.context.account.peerId || self.isSettings) { credibilityIcon = .premium } else { credibilityIcon = .none } } else { credibilityIcon = .none } var isForum = false if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { isForum = true } if themeUpdated || self.currentCredibilityIcon != credibilityIcon { self.currentCredibilityIcon = credibilityIcon var currentEmojiStatus: PeerEmojiStatus? let emojiRegularStatusContent: EmojiStatusComponent.Content let emojiExpandedStatusContent: EmojiStatusComponent.Content switch credibilityIcon { case .none: emojiRegularStatusContent = .none emojiExpandedStatusContent = .none case .premium: emojiRegularStatusContent = .premium(color: presentationData.theme.list.itemAccentColor) emojiExpandedStatusContent = .premium(color: UIColor(rgb: 0xffffff, alpha: 0.75)) case .verified: emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large) emojiExpandedStatusContent = .verified(fillColor: UIColor(rgb: 0xffffff, alpha: 0.75), foregroundColor: .clear, sizeType: .large) case .fake: emojiRegularStatusContent = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_FakeAccount.uppercased()) emojiExpandedStatusContent = emojiRegularStatusContent case .scam: emojiRegularStatusContent = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_ScamAccount.uppercased()) emojiExpandedStatusContent = emojiRegularStatusContent case let .emojiStatus(emojiStatus): currentEmojiStatus = emojiStatus emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever) emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15), themeColor: presentationData.theme.list.itemAccentColor, loopMode: .forever) } let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty let iconSize = self.titleCredibilityIconView.update( transition: animateStatusIcon ? Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: emojiRegularStatusContent, isVisibleForAnimations: true, useSharedAnimation: true, action: { [weak self] in guard let strongSelf = self else { return } strongSelf.displayPremiumIntro?(strongSelf.titleCredibilityIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), false) }, emojiFileUpdated: { [weak self] emojiFile in guard let strongSelf = self else { return } if let emojiFile = emojiFile { strongSelf.emojiStatusFileAndPackTitle.set(.never()) for attribute in emojiFile.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { strongSelf.emojiStatusPackDisposable.set((strongSelf.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) |> filter { result in if case .result = result { return true } else { return false } } |> mapToSignal { result -> Signal<(TelegramMediaFile, LoadedStickerPack)?, NoError> in if case let .result(_, items, _) = result { return .single(items.first.flatMap { ($0.file, result) }) } else { return .complete() } }).start(next: { fileAndPackTitle in guard let strongSelf = self else { return } strongSelf.emojiStatusFileAndPackTitle.set(.single(fileAndPackTitle)) })) break } } } else { strongSelf.emojiStatusFileAndPackTitle.set(.never()) } } )), environment: {}, containerSize: CGSize(width: 34.0, height: 34.0) ) let expandedIconSize = self.titleExpandedCredibilityIconView.update( transition: animateStatusIcon ? Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate, component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, content: emojiExpandedStatusContent, isVisibleForAnimations: true, useSharedAnimation: true, action: { [weak self] in guard let strongSelf = self else { return } strongSelf.displayPremiumIntro?(strongSelf.titleExpandedCredibilityIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) } )), environment: {}, containerSize: CGSize(width: 34.0, height: 34.0) ) self.credibilityIconSize = iconSize self.titleExpandedCredibilityIconSize = expandedIconSize } self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 self.buttonsContainerNode.alpha = self.regularContentNode.alpha self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, isModalOverlay: isModalOverlay, peer: state.isEditing ? peer : nil, threadData: threadData, chatLocation: self.chatLocation, cachedData: cachedData, isContact: isContact, isSettings: isSettings, presentationData: presentationData, transition: transition) transition.updateFrame(node: self.editingContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -contentOffset), size: CGSize(width: width, height: editingContentHeight))) let avatarOverlayFarme = self.editingContentNode.convert(self.editingContentNode.avatarNode.frame, to: self) transition.updateFrame(node: self.avatarOverlayNode, frame: avatarOverlayFarme) var transitionSourceHeight: CGFloat = 0.0 var transitionFraction: CGFloat = 0.0 var transitionSourceAvatarFrame: CGRect? var transitionSourceTitleFrame = CGRect() var transitionSourceSubtitleFrame = CGRect() let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize)) self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) let headerBackgroundColor: UIColor = presentationData.theme.list.blocksBackgroundColor var effectiveSeparatorAlpha: CGFloat if let navigationTransition = self.navigationTransition { transitionSourceHeight = navigationTransition.sourceNavigationBar.backgroundNode.bounds.height transitionFraction = navigationTransition.fraction if let avatarNavigationNode = navigationTransition.sourceNavigationBar.rightButtonNode.singleCustomNode as? ChatAvatarNavigationNode { if let statusView = avatarNavigationNode.statusView.view { transitionSourceAvatarFrame = statusView.convert(statusView.bounds, to: navigationTransition.sourceNavigationBar.view) } else { transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view) } } else { if deviceMetrics.hasDynamicIsland && !isLandscape { transitionSourceAvatarFrame = CGRect(origin: CGPoint(x: avatarFrame.minX, y: -20.0), size: avatarFrame.size).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4) } else { transitionSourceAvatarFrame = avatarFrame.offsetBy(dx: 0.0, dy: -avatarFrame.maxY).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4) } } transitionSourceTitleFrame = navigationTransition.sourceTitleFrame transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor.mixedWith(headerBackgroundColor, alpha: 1.0 - transitionFraction), forceKeepBlur: true, transition: transition) effectiveSeparatorAlpha = transitionFraction if self.isAvatarExpanded, case .animated = transition, transitionFraction == 1.0 { self.avatarListNode.animateAvatarCollapse(transition: transition) } self.avatarClippingNode.clipsToBounds = false } else { let contentOffset = max(0.0, contentOffset - 140.0) let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / 30.0)) self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.mixedWith(headerBackgroundColor, alpha: 1.0 - backgroundTransitionFraction), forceKeepBlur: true, transition: transition) effectiveSeparatorAlpha = backgroundTransitionFraction self.avatarClippingNode.clipsToBounds = true } self.avatarListNode.avatarContainerNode.updateTransitionFraction(transitionFraction, transition: transition) self.avatarListNode.listContainerNode.currentItemNode?.updateTransitionFraction(transitionFraction, transition: transition) self.avatarOverlayNode.updateTransitionFraction(transitionFraction, transition: transition) if self.navigationTitle != presentationData.strings.EditProfile_Title || themeUpdated { self.navigationTitleNode.attributedText = NSAttributedString(string: presentationData.strings.EditProfile_Title, font: Font.semibold(17.0), textColor: presentationData.theme.rootController.navigationBar.primaryTextColor) } let navigationTitleSize = self.navigationTitleNode.updateLayout(CGSize(width: width, height: navigationHeight)) self.navigationTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - navigationTitleSize.width) / 2.0), y: navigationHeight - 44.0 + floorToScreenPixels((44.0 - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) self.navigationBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: navigationHeight)) self.navigationBackgroundBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: navigationHeight)) self.navigationSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: width, height: UIScreenPixel)) self.navigationBackgroundBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.navigationSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor let navigationSeparatorAlpha: CGFloat = state.isEditing && self.isSettings ? min(1.0, contentOffset / (navigationHeight * 0.5)) : 0.0 transition.updateAlpha(node: self.navigationBackgroundBackgroundNode, alpha: 1.0 - navigationSeparatorAlpha) transition.updateAlpha(node: self.navigationSeparatorNode, alpha: navigationSeparatorAlpha) self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor let expandedAvatarControlsHeight: CGFloat = 61.0 let expandedAvatarListHeight = min(width, containerHeight - expandedAvatarControlsHeight) let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight) let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact, threadInfo: threadData?.info) var isPremium = false var isVerified = false var isFake = false let titleStringText: String let smallTitleAttributes: MultiScaleTextState.Attributes let titleAttributes: MultiScaleTextState.Attributes let subtitleStringText: String let smallSubtitleAttributes: MultiScaleTextState.Attributes let subtitleAttributes: MultiScaleTextState.Attributes var subtitleIsButton: Bool = false var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? var nextPanelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? let usernameString: (text: String, attributes: MultiScaleTextState.Attributes) if let peer = peer { isPremium = peer.isPremium isVerified = peer.isVerified isFake = peer.isFake || peer.isScam } if let peer = peer { var title: String if peer.id == self.context.account.peerId && !self.isSettings { title = presentationData.strings.Conversation_SavedMessages } else if peer.id == self.context.account.peerId && !self.isSettings { title = presentationData.strings.DialogList_Replies } else if let threadData = threadData { title = threadData.info.title } else { title = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } title = title.replacingOccurrences(of: "\u{1160}", with: "").replacingOccurrences(of: "\u{3164}", with: "") if title.isEmpty { if let peer = peer as? TelegramUser, let phone = peer.phone { title = formatPhoneNumber(context: self.context, number: phone) } else if let addressName = peer.addressName { title = "@\(addressName)" } else { title = " " } } titleStringText = title titleAttributes = MultiScaleTextState.Attributes(font: Font.medium(30.0), color: presentationData.theme.list.itemPrimaryTextColor) smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: .white) if self.isSettings, let user = peer as? TelegramUser { var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "") if let mainUsername = user.addressName, !mainUsername.isEmpty { subtitle = "\(subtitle) • @\(mainUsername)" } subtitleStringText = subtitle subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor) smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) } else if let _ = threadData { let subtitleColor: UIColor subtitleColor = presentationData.theme.list.itemAccentColor let statusText: String statusText = peer.debugDisplayTitle subtitleStringText = statusText subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(15.0), color: subtitleColor) smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) subtitleIsButton = true let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData if let panelStatusData = maybePanelStatusData { let subtitleColor: UIColor if panelStatusData.isActivity { subtitleColor = presentationData.theme.list.itemAccentColor } else { subtitleColor = presentationData.theme.list.itemSecondaryTextColor } panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } if let nextPanelStatusData = maybeNextPanelStatusData { nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)) } } else if let statusData = statusData { let subtitleColor: UIColor if statusData.isActivity { subtitleColor = presentationData.theme.list.itemAccentColor } else { subtitleColor = presentationData.theme.list.itemSecondaryTextColor } subtitleStringText = statusData.text subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor) smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData if let panelStatusData = maybePanelStatusData { let subtitleColor: UIColor if panelStatusData.isActivity { subtitleColor = presentationData.theme.list.itemAccentColor } else { subtitleColor = presentationData.theme.list.itemSecondaryTextColor } panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } if let nextPanelStatusData = maybeNextPanelStatusData { nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)) } } else { subtitleStringText = " " subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) } } else { titleStringText = " " titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: presentationData.theme.list.itemPrimaryTextColor) smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: .white) subtitleStringText = " " subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) } let textSideInset: CGFloat = 36.0 let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude) let titleNodeLayout = self.titleNode.updateLayout(text: titleStringText, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: titleAttributes, constrainedSize: titleConstrainedSize), TitleNodeStateExpanded: MultiScaleTextState(attributes: smallTitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) let subtitleNodeLayout = self.subtitleNode.updateLayout(text: subtitleStringText, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: subtitleAttributes, constrainedSize: titleConstrainedSize), TitleNodeStateExpanded: MultiScaleTextState(attributes: smallSubtitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) self.subtitleNode.accessibilityLabel = subtitleStringText if subtitleIsButton { let subtitleBackgroundNode: ASDisplayNode if let current = self.subtitleBackgroundNode { subtitleBackgroundNode = current } else { subtitleBackgroundNode = ASDisplayNode() self.subtitleBackgroundNode = subtitleBackgroundNode self.subtitleNode.insertSubnode(subtitleBackgroundNode, at: 0) } let subtitleBackgroundButton: HighlightTrackingButtonNode if let current = self.subtitleBackgroundButton { subtitleBackgroundButton = current } else { subtitleBackgroundButton = HighlightTrackingButtonNode() self.subtitleBackgroundButton = subtitleBackgroundButton self.subtitleNode.addSubnode(subtitleBackgroundButton) subtitleBackgroundButton.addTarget(self, action: #selector(self.subtitleBackgroundPressed), forControlEvents: .touchUpInside) subtitleBackgroundButton.highligthedChanged = { [weak self] highlighted in guard let self else { return } if highlighted { self.subtitleNode.layer.removeAnimation(forKey: "opacity") self.subtitleNode.alpha = 0.4 } else { self.subtitleNode.alpha = 1.0 self.subtitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } let subtitleArrowNode: ASImageNode if let current = self.subtitleArrowNode { subtitleArrowNode = current if themeUpdated { subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)) } } else { subtitleArrowNode = ASImageNode() self.subtitleArrowNode = subtitleArrowNode self.subtitleNode.insertSubnode(subtitleArrowNode, at: 1) subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)) } subtitleBackgroundNode.backgroundColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1) let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -6.0, dy: -4.0) subtitleBackgroundFrame.size.width += 12.0 transition.updateFrame(node: subtitleBackgroundNode, frame: subtitleBackgroundFrame) transition.updateCornerRadius(node: subtitleBackgroundNode, cornerRadius: subtitleBackgroundFrame.height * 0.5) transition.updateFrame(node: subtitleBackgroundButton, frame: subtitleBackgroundFrame) if let arrowImage = subtitleArrowNode.image { let scaleFactor: CGFloat = 0.8 let arrowSize = CGSize(width: floorToScreenPixels(arrowImage.size.width * scaleFactor), height: floorToScreenPixels(arrowImage.size.height * scaleFactor)) subtitleArrowNode.frame = CGRect(origin: CGPoint(x: subtitleBackgroundFrame.maxX - arrowSize.width - 1.0, y: subtitleBackgroundFrame.minY + floor((subtitleBackgroundFrame.height - arrowSize.height) / 2.0)), size: arrowSize) } } else { if let subtitleBackgroundNode = self.subtitleBackgroundNode { self.subtitleBackgroundNode = nil subtitleBackgroundNode.removeFromSupernode() } if let subtitleArrowNode = self.subtitleArrowNode { self.subtitleArrowNode = nil subtitleArrowNode.removeFromSupernode() } if let subtitleBackgroundButton = self.subtitleBackgroundButton { self.subtitleBackgroundButton = nil subtitleBackgroundButton.removeFromSupernode() } } if let previousPanelStatusData = previousPanelStatusData, let currentPanelStatusData = panelStatusData.0, let previousPanelStatusDataKey = previousPanelStatusData.key, let currentPanelStatusDataKey = currentPanelStatusData.key, previousPanelStatusDataKey != currentPanelStatusDataKey { if let snapshotView = self.panelSubtitleNode.view.snapshotContentTree() { let direction: CGFloat = previousPanelStatusDataKey.rawValue > currentPanelStatusDataKey.rawValue ? 1.0 : -1.0 self.panelSubtitleNode.view.superview?.addSubview(snapshotView) snapshotView.frame = self.panelSubtitleNode.frame snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 100.0 * direction, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.panelSubtitleNode.layer.animatePosition(from: CGPoint(x: 100.0 * direction * -1.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.panelSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(text: panelSubtitleString?.text ?? subtitleStringText, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize), TitleNodeStateExpanded: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(text: nextPanelSubtitleString?.text ?? subtitleStringText, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize), TitleNodeStateExpanded: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) if let _ = nextPanelSubtitleString { self.nextPanelSubtitleNode.isHidden = false } let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height)) ], mainState: TitleNodeStateRegular) self.usernameNode.accessibilityLabel = usernameString.text let avatarCenter: CGPoint if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { avatarCenter = CGPoint(x: (1.0 - transitionFraction) * avatarFrame.midX + transitionFraction * transitionSourceAvatarFrame.midX, y: (1.0 - transitionFraction) * avatarFrame.midY + transitionFraction * transitionSourceAvatarFrame.midY) } else { avatarCenter = avatarFrame.center } let titleSize = titleNodeLayout[TitleNodeStateRegular]!.size let titleExpandedSize = titleNodeLayout[TitleNodeStateExpanded]!.size let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size let _ = panelSubtitleNodeLayout[TitleNodeStateRegular]!.size let _ = nextPanelSubtitleNodeLayout[TitleNodeStateRegular]!.size let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size var titleHorizontalOffset: CGFloat = 0.0 if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { titleHorizontalOffset = -(credibilityIconSize.width + 4.0) / 2.0 var collapsedTransitionOffset: CGFloat = 0.0 if let navigationTransition = self.navigationTransition { collapsedTransitionOffset = -10.0 * navigationTransition.fraction } transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0) + 2.0), size: credibilityIconSize)) transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize)) } var titleFrame: CGRect var subtitleFrame: CGRect let usernameFrame: CGRect let usernameSpacing: CGFloat = 4.0 transition.updateFrame(node: self.avatarListNode.listContainerNode.bottomShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: expandedAvatarHeight - 70.0), size: CGSize(width: width, height: 70.0))) if self.isAvatarExpanded { let minTitleSize = CGSize(width: titleSize.width * 0.7, height: titleSize.height * 0.7) let minTitleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - 58.0 - UIScreenPixel + (subtitleSize.height.isZero ? 10.0 : 0.0)), size: minTitleSize) titleFrame = CGRect(origin: CGPoint(x: minTitleFrame.midX - titleSize.width / 2.0, y: minTitleFrame.midY - titleSize.height / 2.0), size: titleSize) subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 2.0), size: subtitleSize) usernameFrame = CGRect(origin: CGPoint(x: width - usernameSize.width - 16.0, y: minTitleFrame.midY - usernameSize.height / 2.0), size: usernameSize) } else { titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: avatarFrame.maxY + 9.0 + (subtitleSize.height.isZero ? 11.0 : 0.0)), size: titleSize) let totalSubtitleWidth = subtitleSize.width + usernameSpacing + usernameSize.width if usernameSize.width == 0.0 { subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) usernameFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - usernameSize.width) / 2.0), y: subtitleFrame.maxY + 1.0), size: usernameSize) } else { subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - totalSubtitleWidth) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) usernameFrame = CGRect(origin: CGPoint(x: subtitleFrame.maxX + usernameSpacing, y: titleFrame.maxY + 1.0), size: usernameSize) } } let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0 let titleLockOffset: CGFloat = 7.0 + singleTitleLockOffset let titleMaxLockOffset: CGFloat = 7.0 var titleCollapseOffset = titleFrame.midY - statusBarHeight - titleLockOffset if case .regular = metrics.widthClass, !isSettings { titleCollapseOffset -= 7.0 } let titleOffset = -min(titleCollapseOffset, contentOffset) let titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) let titleMinScale: CGFloat = 0.6 let subtitleMinScale: CGFloat = 0.8 let avatarMinScale: CGFloat = 0.55 let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset let paneAreaExpansionDistance: CGFloat = 32.0 let effectiveAreaExpansionFraction: CGFloat if state.isEditing { effectiveAreaExpansionFraction = 0.0 } else if isSettings { var paneAreaExpansionDelta = (self.frame.maxY - navigationHeight) - contentOffset paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) effectiveAreaExpansionFraction = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance } else { var paneAreaExpansionDelta = (paneContainerY - navigationHeight) - contentOffset paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) effectiveAreaExpansionFraction = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance } let secondarySeparatorAlpha = 1.0 - effectiveAreaExpansionFraction if self.navigationTransition == nil && !self.isSettings && effectiveSeparatorAlpha == 1.0 && secondarySeparatorAlpha < 1.0 { effectiveSeparatorAlpha = secondarySeparatorAlpha } transition.updateAlpha(node: self.separatorNode, alpha: effectiveSeparatorAlpha) self.titleNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], transition: transition) let subtitleAlpha: CGFloat var subtitleOffset: CGFloat = 0.0 let panelSubtitleAlpha: CGFloat var panelSubtitleOffset: CGFloat = 0.0 if self.isSettings { subtitleAlpha = 1.0 - titleCollapseFraction panelSubtitleAlpha = 0.0 } else { if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText { subtitleAlpha = 1.0 - effectiveAreaExpansionFraction panelSubtitleAlpha = effectiveAreaExpansionFraction subtitleOffset = -effectiveAreaExpansionFraction * 5.0 panelSubtitleOffset = (1.0 - effectiveAreaExpansionFraction) * 5.0 } else { subtitleAlpha = 1.0 panelSubtitleAlpha = 0.0 } } self.subtitleNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], alpha: subtitleAlpha, transition: transition) self.panelSubtitleNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], alpha: panelSubtitleAlpha, transition: transition) self.nextPanelSubtitleNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], alpha: panelSubtitleAlpha, transition: transition) self.usernameNode.update(stateFractions: [ TitleNodeStateRegular: self.isAvatarExpanded ? 0.0 : 1.0, TitleNodeStateExpanded: self.isAvatarExpanded ? 1.0 : 0.0 ], alpha: subtitleAlpha, transition: transition) let avatarScale: CGFloat let avatarOffset: CGFloat if self.navigationTransition != nil { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / avatarFrame.width } else { avatarScale = 1.0 } avatarOffset = 0.0 } else { avatarScale = 1.0 * (1.0 - titleCollapseFraction) + avatarMinScale * titleCollapseFraction avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction } if subtitleIsButton { subtitleFrame.origin.y += 11.0 * (1.0 - titleCollapseFraction) if let subtitleBackgroundButton = self.subtitleBackgroundButton { transition.updateAlpha(node: subtitleBackgroundButton, alpha: (1.0 - titleCollapseFraction)) } if let subtitleBackgroundNode = self.subtitleBackgroundNode { transition.updateAlpha(node: subtitleBackgroundNode, alpha: (1.0 - titleCollapseFraction)) } if let subtitleArrowNode = self.subtitleArrowNode { transition.updateAlpha(node: subtitleArrowNode, alpha: (1.0 - titleCollapseFraction)) } } let avatarCornerRadius: CGFloat = isForum ? floor(avatarSize * 0.25) : avatarSize / 2.0 if self.isAvatarExpanded { self.avatarListNode.listContainerNode.isHidden = false if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) } else { transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0) transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: 0.0) } } else if self.avatarListNode.listContainerNode.cornerRadius != avatarCornerRadius { transition.updateCornerRadius(node: self.avatarListNode.listContainerNode.controlsClippingNode, cornerRadius: avatarCornerRadius) transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: avatarCornerRadius, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.avatarListNode.avatarContainerNode.canAttachVideo = true strongSelf.avatarListNode.listContainerNode.isHidden = true if !strongSelf.skipCollapseCompletion { DispatchQueue.main.async { strongSelf.avatarListNode.listContainerNode.isCollapsing = false } } }) } self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, isForum: isForum, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition) self.editingContentNode.avatarNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) self.avatarOverlayNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing) if additive { transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) transition.updateSublayerTransformScaleAdditive(node: self.avatarOverlayNode, scale: avatarScale) } else { transition.updateSublayerTransformScale(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) transition.updateSublayerTransformScale(node: self.avatarOverlayNode, scale: avatarScale) } let apparentAvatarFrame: CGRect let controlsClippingFrame: CGRect if self.isAvatarExpanded { let expandedAvatarCenter = CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - contentOffset / 2.0) apparentAvatarFrame = CGRect(origin: CGPoint(x: expandedAvatarCenter.x * (1.0 - transitionFraction) + transitionFraction * avatarCenter.x, y: expandedAvatarCenter.y * (1.0 - transitionFraction) + transitionFraction * avatarCenter.y), size: CGSize()) if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { let expandedFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) controlsClippingFrame = CGRect(origin: CGPoint(x: transitionFraction * transitionSourceAvatarFrame.minX + (1.0 - transitionFraction) * expandedFrame.minX, y: transitionFraction * transitionSourceAvatarFrame.minY + (1.0 - transitionFraction) * expandedFrame.minY), size: CGSize(width: transitionFraction * transitionSourceAvatarFrame.width + (1.0 - transitionFraction) * expandedFrame.width, height: transitionFraction * transitionSourceAvatarFrame.height + (1.0 - transitionFraction) * expandedFrame.height)) } else { controlsClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedAvatarListSize) } } else { apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size) controlsClippingFrame = apparentAvatarFrame } let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 48.0 : 0.0 let clippingNodeTransition = ContainedViewLayoutTransition.immediate clippingNodeTransition.updateFrame(layer: self.avatarClippingNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: avatarClipOffset), size: CGSize(width: width, height: 1000.0))) clippingNodeTransition.updateSublayerTransformOffset(layer: self.avatarClippingNode.layer, offset: CGPoint(x: 0.0, y: -avatarClipOffset)) let clippingNodeRadiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) clippingNodeRadiusTransition.updateCornerRadius(node: self.avatarClippingNode, cornerRadius: avatarClipOffset > 0.0 ? width / 2.5 : 0.0) transition.updateFrameAdditive(node: self.avatarListNode, frame: CGRect(origin: apparentAvatarFrame.center, size: CGSize())) transition.updateFrameAdditive(node: self.avatarOverlayNode, frame: CGRect(origin: apparentAvatarFrame.center, size: CGSize())) let avatarListContainerFrame: CGRect let avatarListContainerScale: CGFloat if self.isAvatarExpanded { if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { let neutralAvatarListContainerSize = expandedAvatarListSize let avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction) avatarListContainerFrame = CGRect(origin: CGPoint(x: -avatarListContainerSize.width / 2.0, y: -avatarListContainerSize.height / 2.0), size: avatarListContainerSize) } else { avatarListContainerFrame = CGRect(origin: CGPoint(x: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize) } avatarListContainerScale = 1.0 + max(0.0, -contentOffset / avatarListContainerFrame.height) } else { avatarListContainerFrame = CGRect(origin: CGPoint(x: -apparentAvatarFrame.width / 2.0, y: -apparentAvatarFrame.height / 2.0), size: apparentAvatarFrame.size) avatarListContainerScale = avatarScale } transition.updateFrame(node: self.avatarListNode.listContainerNode, frame: avatarListContainerFrame) let innerScale = avatarListContainerFrame.height / expandedAvatarListSize.height let innerDeltaX = (avatarListContainerFrame.width - expandedAvatarListSize.width) / 2.0 let innerDeltaY = (avatarListContainerFrame.height - expandedAvatarListSize.height) / 2.0 transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode, scale: innerScale) transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.contentNode, frame: CGRect(origin: CGPoint(x: innerDeltaX + expandedAvatarListSize.width / 2.0, y: innerDeltaY + expandedAvatarListSize.height / 2.0), size: CGSize())) transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsClippingOffsetNode, frame: CGRect(origin: controlsClippingFrame.center, size: CGSize())) transition.updateFrame(node: self.avatarListNode.listContainerNode.controlsClippingNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.width / 2.0, y: -controlsClippingFrame.height / 2.0), size: controlsClippingFrame.size)) transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.minX, y: -controlsClippingFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height))) transition.updateFrame(node: self.avatarListNode.listContainerNode.topShadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight + 20.0))) transition.updateFrame(node: self.avatarListNode.listContainerNode.stripContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusBarHeight < 25.0 ? (statusBarHeight + 2.0) : (statusBarHeight - 3.0)), size: CGSize(width: expandedAvatarListSize.width, height: 2.0))) transition.updateFrame(node: self.avatarListNode.listContainerNode.highlightContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height))) transition.updateAlpha(node: self.avatarListNode.listContainerNode.controlsContainerNode, alpha: self.isAvatarExpanded ? (1.0 - transitionFraction) : 0.0) if additive { transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) } else { transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) } if deviceMetrics.hasDynamicIsland && self.forumTopicThreadId == nil && self.navigationTransition == nil && !isLandscape { let maskValue = max(0.0, min(1.0, contentOffset / 120.0)) self.avatarListNode.containerNode.view.mask = self.avatarListNode.maskNode.view if maskValue > 0.03 { self.avatarListNode.bottomCoverNode.isHidden = false self.avatarListNode.topCoverNode.isHidden = false self.avatarListNode.maskNode.backgroundColor = .clear } else { self.avatarListNode.bottomCoverNode.isHidden = true self.avatarListNode.topCoverNode.isHidden = true self.avatarListNode.maskNode.backgroundColor = .white } self.avatarListNode.topCoverNode.update(maskValue) self.avatarListNode.maskNode.update(maskValue) self.avatarListNode.listContainerNode.topShadowNode.isHidden = !self.isAvatarExpanded var avatarMaskOffset: CGFloat = 0.0 if contentOffset < 0.0 { avatarMaskOffset -= contentOffset } self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.5 + avatarMaskOffset) self.avatarListNode.maskNode.bounds = CGRect(origin: .zero, size: CGSize(width: 171.0, height: 171.0)) self.avatarListNode.bottomCoverNode.position = self.avatarListNode.maskNode.position self.avatarListNode.bottomCoverNode.bounds = self.avatarListNode.maskNode.bounds self.avatarListNode.topCoverNode.position = self.avatarListNode.maskNode.position self.avatarListNode.topCoverNode.bounds = self.avatarListNode.maskNode.bounds } else { self.avatarListNode.bottomCoverNode.isHidden = true self.avatarListNode.topCoverNode.isHidden = true self.avatarListNode.containerNode.view.mask = nil } self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer, isExpanded: self.isAvatarExpanded, transition: transition) if self.avatarListNode.listContainerNode.isCollapsing && !self.ignoreCollapse { self.avatarListNode.avatarContainerNode.canAttachVideo = false } var panelWithAvatarHeight: CGFloat = 35.0 + avatarSize if threadData != nil { panelWithAvatarHeight += 10.0 } let rawHeight: CGFloat let height: CGFloat let maxY: CGFloat if self.isAvatarExpanded { rawHeight = expandedAvatarHeight height = max(navigationHeight, rawHeight - contentOffset) maxY = height } else { rawHeight = navigationHeight + panelWithAvatarHeight height = navigationHeight + max(0.0, panelWithAvatarHeight - contentOffset) maxY = navigationHeight + panelWithAvatarHeight - contentOffset } let apparentHeight = (1.0 - transitionFraction) * height + transitionFraction * transitionSourceHeight if !titleSize.width.isZero && !titleSize.height.isZero { if self.navigationTransition != nil { var neutralTitleScale: CGFloat = 1.0 var neutralSubtitleScale: CGFloat = 1.0 if self.isAvatarExpanded { neutralTitleScale = 0.7 neutralSubtitleScale = 1.0 } let titleScale = (transitionFraction * transitionSourceTitleFrame.height + (1.0 - transitionFraction) * titleFrame.height * neutralTitleScale) / (titleFrame.height) let subtitleScale = max(0.01, min(10.0, (transitionFraction * transitionSourceSubtitleFrame.height + (1.0 - transitionFraction) * subtitleFrame.height * neutralSubtitleScale) / (subtitleFrame.height))) var titleFrame = titleFrame if !self.isAvatarExpanded { titleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? 0.0 : titleHorizontalOffset * titleScale, dy: 0.0) } let titleCenter = CGPoint(x: transitionFraction * transitionSourceTitleFrame.midX + (1.0 - transitionFraction) * titleFrame.midX, y: transitionFraction * transitionSourceTitleFrame.midY + (1.0 - transitionFraction) * titleFrame.midY) let subtitleCenter = CGPoint(x: transitionFraction * transitionSourceSubtitleFrame.midX + (1.0 - transitionFraction) * subtitleFrame.midX, y: transitionFraction * transitionSourceSubtitleFrame.midY + (1.0 - transitionFraction) * subtitleFrame.midY) let rawTitleFrame = CGRect(origin: CGPoint(x: titleCenter.x - titleFrame.size.width * neutralTitleScale / 2.0, y: titleCenter.y - titleFrame.size.height * neutralTitleScale / 2.0), size: CGSize(width: titleFrame.size.width * neutralTitleScale, height: titleFrame.size.height * neutralTitleScale)) self.titleNodeRawContainer.frame = rawTitleFrame transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize())) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) let rawSubtitleFrame = CGRect(origin: CGPoint(x: subtitleCenter.x - subtitleFrame.size.width / 2.0, y: subtitleCenter.y - subtitleFrame.size.height / 2.0), size: subtitleFrame.size) self.subtitleNodeRawContainer.frame = rawSubtitleFrame transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize())) transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.nextPanelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScale(node: self.usernameNodeContainer, scale: subtitleScale) } else { let titleScale: CGFloat let subtitleScale: CGFloat var subtitleOffset: CGFloat = 0.0 if self.isAvatarExpanded { titleScale = 0.7 subtitleScale = 1.0 } else { titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale subtitleOffset = titleCollapseFraction * -2.0 } let rawTitleFrame = titleFrame.offsetBy(dx: self.isAvatarExpanded ? 0.0 : titleHorizontalOffset * titleScale, dy: 0.0) self.titleNodeRawContainer.frame = rawTitleFrame transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: CGSize())) let rawSubtitleFrame = subtitleFrame self.subtitleNodeRawContainer.frame = rawSubtitleFrame let rawUsernameFrame = usernameFrame self.usernameNodeRawContainer.frame = rawUsernameFrame if self.isAvatarExpanded { transition.updateFrameAdditive(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset)) transition.updateFrameAdditive(node: self.subtitleNodeContainer, frame: CGRect(origin: rawSubtitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) transition.updateFrameAdditive(node: self.usernameNodeContainer, frame: CGRect(origin: rawUsernameFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) } else { transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: CGRect(origin: rawTitleFrame.center, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset)) var subtitleCenter = rawSubtitleFrame.center subtitleCenter.x = rawTitleFrame.center.x + (subtitleCenter.x - rawTitleFrame.center.x) * subtitleScale subtitleCenter.y += subtitleOffset transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: CGRect(origin: subtitleCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) var usernameCenter = rawUsernameFrame.center usernameCenter.x = rawTitleFrame.center.x + (usernameCenter.x - rawTitleFrame.center.x) * subtitleScale transition.updateFrameAdditiveToCenter(node: self.usernameNodeContainer, frame: CGRect(origin: usernameCenter, size: CGSize()).offsetBy(dx: 0.0, dy: titleOffset)) } transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: subtitleOffset), size: CGSize())) transition.updateFrame(node: self.panelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.nextPanelSubtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelSubtitleOffset), size: CGSize())) transition.updateFrame(node: self.usernameNode, frame: CGRect(origin: CGPoint(), size: CGSize())) transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) transition.updateSublayerTransformScaleAdditive(node: self.usernameNodeContainer, scale: subtitleScale) } } let buttonSpacing: CGFloat = 8.0 let buttonSideInset = max(16.0, containerInset) var buttonRightOrigin = CGPoint(x: width - buttonSideInset, y: maxY + 25.0 - navigationHeight - UIScreenPixel) let buttonWidth = (width - buttonSideInset * 2.0 + buttonSpacing) / CGFloat(buttonKeys.count) - buttonSpacing let apparentButtonSize = CGSize(width: buttonWidth, height: 58.0) let buttonsAlpha: CGFloat = 1.0 let buttonsVerticalOffset: CGFloat = 0.0 let buttonsAlphaTransition = transition for buttonKey in buttonKeys.reversed() { let buttonNode: PeerInfoHeaderButtonNode var wasAdded = false if let current = self.buttonNodes[buttonKey] { buttonNode = current } else { wasAdded = true buttonNode = PeerInfoHeaderButtonNode(key: buttonKey, action: { [weak self] buttonNode, gesture in self?.buttonPressed(buttonNode, gesture: gesture) }) self.buttonNodes[buttonKey] = buttonNode self.buttonsContainerNode.addSubnode(buttonNode) } let buttonFrame = CGRect(origin: CGPoint(x: buttonRightOrigin.x - apparentButtonSize.width, y: buttonRightOrigin.y), size: apparentButtonSize) let buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition let apparentButtonFrame = buttonFrame.offsetBy(dx: 0.0, dy: buttonsVerticalOffset) if additive { buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: apparentButtonFrame) } else { buttonTransition.updateFrame(node: buttonNode, frame: apparentButtonFrame) } let buttonText: String let buttonIcon: PeerInfoHeaderButtonIcon switch buttonKey { case .message: buttonText = presentationData.strings.PeerInfo_ButtonMessage buttonIcon = .message case .discussion: buttonText = presentationData.strings.PeerInfo_ButtonDiscuss buttonIcon = .message case .call: buttonText = presentationData.strings.PeerInfo_ButtonCall buttonIcon = .call case .videoCall: buttonText = presentationData.strings.PeerInfo_ButtonVideoCall buttonIcon = .videoCall case .voiceChat: if let channel = peer as? TelegramChannel, case .broadcast = channel.info { buttonText = presentationData.strings.PeerInfo_ButtonLiveStream } else { buttonText = presentationData.strings.PeerInfo_ButtonVoiceChat } buttonIcon = .voiceChat case .mute: let chatIsMuted = peerInfoIsChatMuted(peer: peer, peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings) if chatIsMuted { buttonText = presentationData.strings.PeerInfo_ButtonUnmute buttonIcon = .unmute } else { buttonText = presentationData.strings.PeerInfo_ButtonMute buttonIcon = .mute } case .more: buttonText = presentationData.strings.PeerInfo_ButtonMore buttonIcon = .more case .addMember: buttonText = presentationData.strings.PeerInfo_ButtonAddMember buttonIcon = .addMember case .search: buttonText = presentationData.strings.PeerInfo_ButtonSearch buttonIcon = .search case .leave: buttonText = presentationData.strings.PeerInfo_ButtonLeave buttonIcon = .leave case .stop: buttonText = presentationData.strings.PeerInfo_ButtonStop buttonIcon = .stop } var isActive = true if let highlightedButton = state.highlightedButton { isActive = buttonKey == highlightedButton } buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isActive: isActive, isExpanded: false, presentationData: presentationData, transition: buttonTransition) if wasAdded { buttonNode.alpha = 0.0 } buttonsAlphaTransition.updateAlpha(node: buttonNode, alpha: buttonsAlpha) if case .mute = buttonKey, buttonNode.containerNode.alpha.isZero, additive { if case let .animated(duration, curve) = transition { ContainedViewLayoutTransition.animated(duration: duration * 0.3, curve: curve).updateAlpha(node: buttonNode.containerNode, alpha: 1.0) } else { transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0) } } else { transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0) } buttonRightOrigin.x -= apparentButtonSize.width + buttonSpacing } for key in self.buttonNodes.keys { if !buttonKeys.contains(key) { if let buttonNode = self.buttonNodes[key] { self.buttonNodes.removeValue(forKey: key) transition.updateAlpha(node: buttonNode, alpha: 0.0) { [weak buttonNode] _ in buttonNode?.removeFromSupernode() } } } } let resolvedRegularHeight: CGFloat if self.isAvatarExpanded { resolvedRegularHeight = expandedAvatarListSize.height } else { resolvedRegularHeight = panelWithAvatarHeight + navigationHeight } let backgroundFrame: CGRect let separatorFrame: CGRect let resolvedHeight: CGFloat if state.isEditing { resolvedHeight = editingContentHeight backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + max(navigationHeight, resolvedHeight - contentOffset)), size: CGSize(width: width, height: 2000.0)) separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(navigationHeight, resolvedHeight - contentOffset)), size: CGSize(width: width, height: UIScreenPixel)) } else { resolvedHeight = resolvedRegularHeight backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentHeight), size: CGSize(width: width, height: 2000.0)) separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: apparentHeight), size: CGSize(width: width, height: UIScreenPixel)) } transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight))) transition.updateFrame(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight + UIScreenPixel), size: CGSize(width: width, height: resolvedHeight - navigationHeight + 180.0))) if additive { transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrameAdditive(node: self.expandedBackgroundNode, frame: backgroundFrame) self.expandedBackgroundNode.update(size: self.expandedBackgroundNode.bounds.size, transition: transition) transition.updateFrameAdditive(node: self.separatorNode, frame: separatorFrame) } else { transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.expandedBackgroundNode, frame: backgroundFrame) self.expandedBackgroundNode.update(size: self.expandedBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.separatorNode, frame: separatorFrame) } if isFirstTime { self.updateAvatarMask(transition: .immediate) } return resolvedHeight } private func buttonPressed(_ buttonNode: PeerInfoHeaderButtonNode, gesture: ContextGesture?) { self.performButtonAction?(buttonNode.key, gesture) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil } if !self.backgroundNode.frame.contains(point) { return nil } let setByFrame = self.avatarListNode.listContainerNode.setByYouNode.view.convert(self.avatarListNode.listContainerNode.setByYouNode.bounds, to: self.view).insetBy(dx: -44.0, dy: 0.0) if self.avatarListNode.listContainerNode.setByYouNode.alpha > 0.0, setByFrame.contains(point) { return self.avatarListNode.listContainerNode.setByYouNode.view } if !(self.state?.isEditing ?? false) { switch self.currentCredibilityIcon { case .premium, .emojiStatus: let iconFrame = self.titleCredibilityIconView.convert(self.titleCredibilityIconView.bounds, to: self.view) let expandedIconFrame = self.titleExpandedCredibilityIconView.convert(self.titleExpandedCredibilityIconView.bounds, to: self.view) if expandedIconFrame.contains(point) && self.isAvatarExpanded { return self.titleExpandedCredibilityIconView.hitTest(self.view.convert(point, to: self.titleExpandedCredibilityIconView), with: event) } else if iconFrame.contains(point) { return self.titleCredibilityIconView.hitTest(self.view.convert(point, to: self.titleCredibilityIconView), with: event) } default: break } } if let subtitleBackgroundButton = self.subtitleBackgroundButton, subtitleBackgroundButton.view.convert(subtitleBackgroundButton.bounds, to: self.view).contains(point) { if let result = subtitleBackgroundButton.view.hitTest(self.view.convert(point, to: subtitleBackgroundButton.view), with: event) { return result } } if result.isDescendant(of: self.navigationButtonContainer.view) { return result } if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { return nil } return result } func updateIsAvatarExpanded(_ isAvatarExpanded: Bool, transition: ContainedViewLayoutTransition) { if self.isAvatarExpanded != isAvatarExpanded { self.isAvatarExpanded = isAvatarExpanded if isAvatarExpanded { self.avatarListNode.listContainerNode.selectFirstItem() } if case .animated = transition, !isAvatarExpanded { self.avatarListNode.animateAvatarCollapse(transition: transition) } self.updateAvatarMask(transition: transition) } } private func updateAvatarMask(transition: ContainedViewLayoutTransition) { guard let (width, deviceMetrics) = self.validLayout, deviceMetrics.hasDynamicIsland else { return } let maskScale: CGFloat = isAvatarExpanded ? width / 100.0 : 1.0 transition.updateTransformScale(layer: self.avatarListNode.maskNode.layer, scale: maskScale) transition.updateTransformScale(layer: self.avatarListNode.bottomCoverNode.layer, scale: maskScale) transition.updateTransformScale(layer: self.avatarListNode.topCoverNode.layer, scale: maskScale) let maskAnchorPoint = CGPoint(x: 0.5, y: isAvatarExpanded ? 0.37 : 0.5) transition.updateAnchorPoint(layer: self.avatarListNode.maskNode.layer, anchorPoint: maskAnchorPoint) } } private class DynamicIslandMaskNode: ASDisplayNode { var animationNode: AnimationNode? var isForum = false { didSet { if self.isForum != oldValue { self.animationNode?.removeFromSupernode() let animationNode = AnimationNode(animation: "ForumAvatarMask") self.addSubnode(animationNode) self.animationNode = animationNode } } } override init() { let animationNode = AnimationNode(animation: "UserAvatarMask") self.animationNode = animationNode super.init() self.addSubnode(animationNode) } func update(_ value: CGFloat) { self.animationNode?.setProgress(value) } var animating = false override func layout() { self.animationNode?.frame = self.bounds } } private class DynamicIslandBlurNode: ASDisplayNode { private var effectView: UIVisualEffectView? private let fadeNode = ASDisplayNode() let gradientNode = ASImageNode() private var hierarchyTrackingNode: HierarchyTrackingNode? deinit { self.animator?.stopAnimation(true) } override func didLoad() { super.didLoad() let hierarchyTrackingNode = HierarchyTrackingNode({ [weak self] value in if !value { self?.animator?.stopAnimation(true) self?.animator = nil } }) self.hierarchyTrackingNode = hierarchyTrackingNode self.addSubnode(hierarchyTrackingNode) self.fadeNode.backgroundColor = .black self.fadeNode.alpha = 0.0 self.gradientNode.displaysAsynchronously = false let gradientImage = generateImage(CGSize(width: 100.0, height: 100.0), rotatedContext: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) var locations: [CGFloat] = [0.0, 0.87, 1.0] let colors: [CGColor] = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 1.0).cgColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! let endRadius: CGFloat = 90.0 let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 38.0) context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: .drawsAfterEndLocation) }) self.gradientNode.image = gradientImage let effectView = UIVisualEffectView(effect: nil) self.effectView = effectView self.view.insertSubview(effectView, at: 0) self.addSubnode(self.gradientNode) self.addSubnode(self.fadeNode) } private var animator: UIViewPropertyAnimator? func prepare() -> Bool { guard self.animator == nil else { return false } let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) self.animator = animator self.effectView?.effect = nil animator.addAnimations { [weak self] in self?.effectView?.effect = UIBlurEffect(style: .dark) } return true } func update(_ value: CGFloat) { let fadeAlpha = min(1.0, max(0.0, -0.25 + value * 1.55)) if value > 0.0 { var value = value let updated = self.prepare() if value > 0.99 && updated { value = 0.99 } self.animator?.fractionComplete = max(0.0, -0.1 + value * 1.1) } else { self.animator?.stopAnimation(true) self.animator = nil self.effectView?.effect = nil } self.fadeNode.alpha = fadeAlpha } override func layout() { super.layout() self.effectView?.frame = self.bounds self.fadeNode.frame = self.bounds let gradientSize = CGSize(width: 100.0, height: 100.0) self.gradientNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - gradientSize.width) / 2.0, y: 0.0), size: gradientSize) } }