Swiftgram/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift
Ilya Laktyushin 4d51f5fcb3 Various fixes
2023-04-13 20:40:29 +04:00

3832 lines
200 KiB
Swift

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<Empty>?
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<String>()
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<Empty>
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<String>()
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<Bool>()
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<Bool, NoError> in
return .complete()
}
|> then(.single(true)))
let galleryReady = self.listContainerNode.isReady.get()
|> filter { value in
return value
}
|> take(1)
let combinedSignal: Signal<Bool, NoError>
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..<updatedText.index(updatedText.startIndex, offsetBy: 255)]), font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor)
self.textNode.attributedText = attributedText
self.requestUpdateHeight()
return false
} else {
return true
}
}
func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let (width, safeInset) = self.currentParams {
var measureText = self.textNode.attributedText?.string ?? ""
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), 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))
if let currentMeasuredHeight = self.currentMeasuredHeight, abs(measureTextSize.height - currentMeasuredHeight) > 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:
updateText = (peer as? TelegramUser)?.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 {
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 = 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<Empty>
var credibilityIconSize: CGSize?
let titleExpandedCredibilityIconView: ComponentHostView<Empty>
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 validWidth: CGFloat?
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<Empty>()
self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.view.addSubview(self.titleCredibilityIconView)
self.titleExpandedCredibilityIconView = ComponentHostView<Empty>()
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
self.validWidth = width
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 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 {
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)
}
} 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.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 ? 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 {
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
self.avatarListNode.maskNode.position = CGPoint(x: 0.0, y: -self.avatarListNode.frame.minY + 48.0 + 85.5)
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)
}
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)
}
if let width = self.validWidth {
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)
}
}