Swiftgram/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift
2021-03-24 18:47:55 +05:00

1139 lines
67 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AvatarNode
import TelegramStringFormatting
import PeerPresenceStatusManager
import ContextUI
import AccountContext
import LegacyComponents
import AudioBlob
import PeerInfoAvatarListNode
final class VoiceChatParticipantItem: ListViewItem {
enum ParticipantText {
public enum TextColor {
case generic
case accent
case constructive
case destructive
}
case presence
case text(String, TextColor)
case none
}
enum Icon {
case none
case microphone(Bool, UIColor)
case invite(Bool)
case wantsToSpeak
}
struct RevealOption {
enum RevealOptionType {
case neutral
case warning
case destructive
case accent
}
var type: RevealOptionType
var title: String
var action: () -> Void
init(type: RevealOptionType, title: String, action: @escaping () -> Void) {
self.type = type
self.title = title
self.action = action
}
}
let presentationData: ItemListPresentationData
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let context: AccountContext
let peer: Peer
let ssrc: UInt32?
let presence: PeerPresence?
let text: ParticipantText
let expandedText: ParticipantText?
let icon: Icon
let enabled: Bool
public let selectable: Bool
let getAudioLevel: (() -> Signal<Float, NoError>)?
let getVideo: () -> GroupVideoNode?
let revealOptions: [RevealOption]
let revealed: Bool?
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let action: ((ASDisplayNode) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, ssrc: UInt32?, presence: PeerPresence?, text: ParticipantText, expandedText: ParticipantText?, icon: Icon, enabled: Bool, selectable: Bool, getAudioLevel: (() -> Signal<Float, NoError>)?, getVideo: @escaping () -> GroupVideoNode?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: ((ASDisplayNode) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.context = context
self.peer = peer
self.ssrc = ssrc
self.presence = presence
self.text = text
self.expandedText = expandedText
self.icon = icon
self.enabled = enabled
self.selectable = selectable
self.getAudioLevel = getAudioLevel
self.getVideo = getVideo
self.revealOptions = revealOptions
self.revealed = revealed
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.action = action
self.contextAction = contextAction
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = VoiceChatParticipantItemNode()
let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil, nextItem == nil)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (node.avatarNode.ready, { _ in apply(synchronousLoads, false) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? VoiceChatParticipantItemNode {
let makeLayout = nodeValue.asyncLayout()
var animated = true
if case .None = animation {
animated = false
}
async {
let (layout, apply) = makeLayout(self, params, previousItem == nil, nextItem == nil)
Queue.mainQueue().async {
completion(layout, { _ in
apply(false, animated)
})
}
}
}
}
}
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
}
}
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private let offsetContainerNode: ASDisplayNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private var extractedVerticalOffset: CGFloat?
fileprivate let avatarNode: AvatarNode
private let titleNode: TextNode
private let statusNode: TextNode
private let expandedStatusNode: TextNode
private var credibilityIconNode: ASImageNode?
private var avatarTransitionNode: ASImageNode?
private var avatarListContainerNode: ASDisplayNode?
private var avatarListWrapperNode: ASDisplayNode?
private var avatarListNode: PeerInfoAvatarListContainerNode?
private let actionContainerNode: ASDisplayNode
private var animationNode: VoiceChatMicrophoneNode?
private var iconNode: ASImageNode?
private var raiseHandNode: VoiceChatRaiseHandNode?
private var actionButtonNode: HighlightableButtonNode
private var audioLevelView: VoiceBlobView?
private let audioLevelDisposable = MetaDisposable()
private var didSetupAudioLevel = false
private var absoluteLocation: (CGRect, CGSize)?
private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (VoiceChatParticipantItem, ListViewItemLayoutParams, Bool, Bool)?
private var isExtracted = false
private var wavesColor: UIColor?
private var videoNode: GroupVideoNode?
private var raiseHandTimer: SwiftSignalKit.Timer?
var item: VoiceChatParticipantItem? {
return self.layoutParams?.0
}
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.clipsToBounds = true
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.offsetContainerNode = ASDisplayNode()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0))
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.statusNode = TextNode()
self.statusNode.isUserInteractionEnabled = false
self.statusNode.contentMode = .left
self.statusNode.contentsScale = UIScreen.main.scale
self.expandedStatusNode = TextNode()
self.expandedStatusNode.isUserInteractionEnabled = false
self.expandedStatusNode.contentMode = .left
self.expandedStatusNode.contentsScale = UIScreen.main.scale
self.expandedStatusNode.alpha = 0.0
self.actionContainerNode = ASDisplayNode()
self.actionButtonNode = HighlightableButtonNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.avatarNode)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.statusNode)
self.offsetContainerNode.addSubnode(self.expandedStatusNode)
self.offsetContainerNode.addSubnode(self.actionContainerNode)
self.actionContainerNode.addSubnode(self.actionButtonNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.actionButtonNode.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3)
apply(false, true)
}
})
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self else {
return false
}
if strongSelf.actionButtonNode.frame.contains(location) {
return false
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else {
return
}
strongSelf.isExtracted = isExtracted
let inset: CGFloat = 12.0
let cornerRadius: CGFloat = 14.0
if isExtracted {
strongSelf.contextSourceNode.contentNode.customHitTest = { [weak self] point in
if let strongSelf = self {
if let avatarListWrapperNode = strongSelf.avatarListWrapperNode, avatarListWrapperNode.frame.contains(point) {
return strongSelf.avatarListNode?.view
}
}
return nil
}
} else {
strongSelf.contextSourceNode.contentNode.customHitTest = nil
}
let extractedVerticalOffset = strongSelf.extractedVerticalOffset ?? 0.0
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect: CGRect
if isExtracted {
if extractedVerticalOffset > 0.0 {
rect = CGRect(x: extractedRect.minX, y: extractedRect.minY + extractedVerticalOffset, width: extractedRect.width, height: extractedRect.height - extractedVerticalOffset)
} else {
rect = extractedRect
}
} else {
rect = nonExtractedRect
}
let springDuration: Double = isExtracted ? 0.42 : 0.3
let springDamping: CGFloat = isExtracted ? 104.0 : 1000.0
if !extractedVerticalOffset.isZero {
let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut)
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateImage(CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0), rotatedContext: { (size, context) in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(item.presentationData.theme.list.itemBlocksBackgroundColor.cgColor)
context.fillEllipse(in: bounds)
context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
strongSelf.extractedBackgroundImageNode.cornerRadius = cornerRadius
var avatarInitialRect = strongSelf.avatarNode.view.convert(strongSelf.avatarNode.bounds, to: strongSelf.offsetContainerNode.supernode?.view)
if strongSelf.avatarTransitionNode == nil {
transition.updateCornerRadius(node: strongSelf.extractedBackgroundImageNode, cornerRadius: 0.0)
let targetRect = CGRect(x: extractedRect.minX, y: extractedRect.minY, width: extractedRect.width, height: extractedRect.width)
let initialScale = avatarInitialRect.width / targetRect.width
avatarInitialRect.origin.y += cornerRadius / 2.0 * initialScale
let avatarListWrapperNode = ASDisplayNode()
avatarListWrapperNode.clipsToBounds = true
avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.height + cornerRadius)
avatarListWrapperNode.cornerRadius = cornerRadius
let transitionNode = ASImageNode()
transitionNode.clipsToBounds = true
transitionNode.displaysAsynchronously = false
transitionNode.displayWithoutProcessing = true
transitionNode.image = strongSelf.avatarNode.unroundedImage
transitionNode.frame = CGRect(origin: CGPoint(), size: targetRect.size)
transitionNode.cornerRadius = targetRect.width / 2.0
radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0)
strongSelf.avatarNode.isHidden = true
avatarListWrapperNode.addSubnode(transitionNode)
strongSelf.avatarTransitionNode = transitionNode
let avatarListContainerNode = ASDisplayNode()
avatarListContainerNode.clipsToBounds = true
avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetRect.size)
avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
avatarListContainerNode.cornerRadius = targetRect.width / 2.0
avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarInitialRect.center), to: NSValue(cgPoint: avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: 0.0)
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
avatarListNode.backgroundColor = .clear
avatarListNode.peer = item.peer
avatarListNode.firstFullSizeOnly = true
avatarListNode.offsetLocation = true
avatarListNode.customCenterTapAction = { [weak self] in
self?.contextSourceNode.requestDismiss?()
}
avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.height / 2.0, width: targetRect.width, height: targetRect.height)
avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.height / 2.0, width: targetRect.width, height: targetRect.height)
avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.height / 2.0), size: CGSize())
avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0)
avatarListContainerNode.addSubnode(avatarListNode)
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
avatarListWrapperNode.addSubnode(avatarListContainerNode)
avatarListNode.update(size: targetRect.size, peer: item.peer, isExpanded: true, transition: .immediate)
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
strongSelf.avatarListWrapperNode = avatarListWrapperNode
strongSelf.avatarListContainerNode = avatarListContainerNode
strongSelf.avatarListNode = avatarListNode
}
} else if let transitionNode = strongSelf.avatarTransitionNode, let avatarListWrapperNode = strongSelf.avatarListWrapperNode, let avatarListContainerNode = strongSelf.avatarListContainerNode {
transition.updateCornerRadius(node: strongSelf.extractedBackgroundImageNode, cornerRadius: cornerRadius)
var avatarInitialRect = CGRect(origin: strongSelf.avatarNode.frame.origin, size: strongSelf.avatarNode.frame.size)
let targetScale = avatarInitialRect.width / avatarListContainerNode.frame.width
avatarInitialRect.origin.y += cornerRadius / 2.0 * targetScale
strongSelf.avatarTransitionNode = nil
strongSelf.avatarListWrapperNode = nil
strongSelf.avatarListContainerNode = nil
strongSelf.avatarListNode = nil
avatarListContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak avatarListContainerNode] _ in
avatarListContainerNode?.removeFromSupernode()
})
avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: avatarListWrapperNode.position), to: NSValue(cgPoint: avatarInitialRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionNode, weak self] _ in
transitionNode?.removeFromSupernode()
self?.avatarNode.isHidden = false
})
radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: avatarListContainerNode.frame.width / 2.0)
radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: avatarListContainerNode.frame.width / 2.0)
}
transition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0)
let offsetInitialSublayerTransform = strongSelf.offsetContainerNode.layer.sublayerTransform
strongSelf.offsetContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? -33 : 0.0, isExtracted ? extractedVerticalOffset : 0.0, 0.0)
let actionInitialSublayerTransform = strongSelf.actionContainerNode.layer.sublayerTransform
strongSelf.actionContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? 21.0 : 0.0, 0.0, 0.0)
let extractedInitialBackgroundPosition = strongSelf.extractedBackgroundImageNode.position
strongSelf.extractedBackgroundImageNode.layer.position = rect.center
let extractedInitialBackgroundBounds = strongSelf.extractedBackgroundImageNode.bounds
strongSelf.extractedBackgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: rect.size)
if isExtracted {
strongSelf.offsetContainerNode.layer.animateSpring(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping)
strongSelf.actionContainerNode.layer.animateSpring(from: NSValue(caTransform3D: actionInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.actionContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping)
strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: extractedInitialBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping)
strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgRect: extractedInitialBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
} else {
strongSelf.offsetContainerNode.layer.animate(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
strongSelf.actionContainerNode.layer.animate(from: NSValue(caTransform3D: actionInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.actionContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgPoint: extractedInitialBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgRect: extractedInitialBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.alpha = 1.0
strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.06, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
} else {
strongSelf.extractedBackgroundImageNode.alpha = 0.0
strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.1, removeOnCompletion: false, completion: { [weak self] _ in
self?.extractedBackgroundImageNode.image = nil
self?.extractedBackgroundImageNode.layer.removeAllAnimations()
})
}
} else {
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius * 2.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor)
}
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
transition.updateAlpha(node: strongSelf.statusNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateAlpha(node: strongSelf.expandedStatusNode, alpha: isExtracted ? 1.0 : 0.0)
transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0)
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? inset : 0.0, y: isExtracted ? extractedVerticalOffset : 0.0))
transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
}
}
deinit {
self.audioLevelDisposable.dispose()
self.raiseHandTimer?.invalidate()
}
@objc private func handleTap() {
print("tap")
}
override func selected() {
super.selected()
self.layoutParams?.0.action?(self.contextSourceNode)
}
func asyncLayout() -> (_ item: VoiceChatParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeExpandedStatusLayout = TextNode.asyncLayout(self.expandedStatusNode)
var currentDisabledOverlayNode = self.disabledOverlayNode
let currentItem = self.layoutParams?.0
return { item, params, first, last in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let titleFont = Font.regular(17.0)
let statusFont = Font.regular(14.0)
var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString?
var expandedStatusAttributedString: NSAttributedString?
let rightInset: CGFloat = params.rightInset
let titleColor = item.presentationData.theme.list.itemPrimaryTextColor
let currentBoldFont: UIFont = titleFont
if let user = item.peer as? TelegramUser {
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
let string = NSMutableAttributedString()
switch item.nameDisplayOrder {
case .firstLast:
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
case .lastFirst:
string.append(NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor))
string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor))
string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor))
}
titleAttributedString = string
} else if let firstName = user.firstName, !firstName.isEmpty {
titleAttributedString = NSAttributedString(string: firstName, font: currentBoldFont, textColor: titleColor)
} else if let lastName = user.lastName, !lastName.isEmpty {
titleAttributedString = NSAttributedString(string: lastName, font: currentBoldFont, textColor: titleColor)
} else {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.User_DeletedAccount, font: currentBoldFont, textColor: titleColor)
}
} else if let group = item.peer as? TelegramGroup {
titleAttributedString = NSAttributedString(string: group.title, font: currentBoldFont, textColor: titleColor)
} else if let channel = item.peer as? TelegramChannel {
titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor)
}
var wavesColor = UIColor(rgb: 0x34c759)
switch item.text {
case .presence:
if let user = item.peer as? TelegramUser, let botInfo = user.botInfo {
let botStatus: String
if botInfo.flags.contains(.hasAccessToChatHistory) {
botStatus = item.presentationData.strings.Bot_GroupStatusReadsHistory
} else {
botStatus = item.presentationData.strings.Bot_GroupStatusDoesNotReadHistory
}
statusAttributedString = NSAttributedString(string: botStatus, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else if let presence = item.presence as? TelegramUserPresence {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let (string, _) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp))
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
} else {
statusAttributedString = NSAttributedString(string: item.presentationData.strings.LastSeen_Offline, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .text(text, textColor):
let textColorValue: UIColor
switch textColor {
case .generic:
textColorValue = item.presentationData.theme.list.itemSecondaryTextColor
case .accent:
textColorValue = item.presentationData.theme.list.itemAccentColor
wavesColor = textColorValue
case .constructive:
textColorValue = UIColor(rgb: 0x34c759)
case .destructive:
textColorValue = UIColor(rgb: 0xff3b30)
wavesColor = textColorValue
}
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
case .none:
break
}
if let expandedText = item.expandedText, case let .text(text, textColor) = expandedText {
let textColorValue: UIColor
switch textColor {
case .generic:
textColorValue = item.presentationData.theme.list.itemSecondaryTextColor
case .accent:
textColorValue = item.presentationData.theme.list.itemAccentColor
case .constructive:
textColorValue = UIColor(rgb: 0x34c759)
case .destructive:
textColorValue = UIColor(rgb: 0xff3b30)
}
expandedStatusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue)
} else {
expandedStatusAttributedString = statusAttributedString
}
let leftInset: CGFloat = 65.0 + params.leftInset
let verticalInset: CGFloat = 8.0
let verticalOffset: CGFloat = 0.0
let avatarSize: CGFloat = 40.0
var titleIconsWidth: CGFloat = 0.0
var currentCredibilityIconImage: UIImage?
var credibilityIconOffset: CGFloat = 0.0
if item.peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
credibilityIconOffset = 2.0
} else if item.peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
credibilityIconOffset = 2.0
} else if item.peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
credibilityIconOffset = 3.0
}
if let currentCredibilityIconImage = currentCredibilityIconImage {
titleIconsWidth += 4.0 + currentCredibilityIconImage.size.width
}
var expandedRightInset: CGFloat = 30.0
if item.peer.smallProfileImage != nil {
expandedRightInset = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - rightInset - 30.0 - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(TextNodeLayoutArguments(attributedString: expandedStatusAttributedString, backgroundColor: nil, maximumNumberOfLines: 6, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let insets = UIEdgeInsets()
let titleSpacing: CGFloat = statusLayout.size.height == 0.0 ? 0.0 : 1.0
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height
let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
currentDisabledOverlayNode?.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.5)
}
} else {
currentDisabledOverlayNode = nil
}
var animateStatusTransitionFromUp: Bool?
if let currentItem = currentItem {
if case .presence = currentItem.text, case let .text(_, newColor) = item.text {
animateStatusTransitionFromUp = newColor == .constructive
} else if case let .text(_, currentColor) = currentItem.text, case let .text(_, newColor) = item.text, currentColor != newColor {
animateStatusTransitionFromUp = newColor == .constructive
} else if case .text = currentItem.text, case .presence = item.text {
animateStatusTransitionFromUp = false
}
}
let peerRevealOptions: [ItemListRevealOption]
var mappedOptions: [ItemListRevealOption] = []
var index: Int32 = 0
for option in item.revealOptions {
let color: UIColor
let textColor: UIColor
switch option.type {
case .neutral:
color = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.constructive.foregroundColor
case .warning:
color = item.presentationData.theme.list.itemDisclosureActions.warning.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.warning.foregroundColor
case .destructive:
color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor
case .accent:
color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor
textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor
}
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor))
index += 1
}
peerRevealOptions = mappedOptions
return (layout, { [weak self] synchronousLoad, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, params, first, last)
strongSelf.wavesColor = wavesColor
let nonExtractedRect = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: layout.contentSize.width - 32.0, height: layout.contentSize.height))
var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
var extractedHeight = extractedRect.height + expandedStatusLayout.size.height - statusLayout.size.height
var extractedVerticalOffset: CGFloat = 0.0
if item.peer.smallProfileImage != nil {
extractedVerticalOffset = extractedRect.width
extractedHeight += extractedVerticalOffset
}
extractedRect.size.height = extractedHeight
strongSelf.extractedVerticalOffset = extractedVerticalOffset
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.isExtracted {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.containerNode.isGestureEnabled = item.contextAction != nil
strongSelf.actionContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
strongSelf.accessibilityLabel = titleAttributedString?.string
var combinedValueString = ""
if let statusString = statusAttributedString?.string, !statusString.isEmpty {
combinedValueString.append(statusString)
}
strongSelf.accessibilityValue = combinedValueString
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.08)
strongSelf.bottomStripeNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.08)
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.addSubnode(currentDisabledOverlayNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let animateStatusTransitionFromUp = animateStatusTransitionFromUp, !strongSelf.contextSourceNode.isExtractedToContextPreview {
let offset: CGFloat = animateStatusTransitionFromUp ? -7.0 : 7.0
if let snapshotView = strongSelf.statusNode.view.snapshotContentTree() {
strongSelf.statusNode.view.superview?.insertSubview(snapshotView, belowSubview: strongSelf.statusNode.view)
snapshotView.frame = strongSelf.statusNode.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.2, removeOnCompletion: false, additive: true)
strongSelf.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.statusNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.2, additive: true)
}
}
let _ = titleApply()
let _ = statusApply()
let _ = expandedStatusApply()
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 0)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
}
strongSelf.topStripeNode.isHidden = first
strongSelf.bottomStripeNode.isHidden = last
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: leftInset, y: contentSize.height + -separatorHeight), size: CGSize(width: layoutSize.width - leftInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + verticalOffset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
transition.updateFrame(node: strongSelf.expandedStatusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: expandedStatusLayout.size))
if let currentCredibilityIconImage = currentCredibilityIconImage {
let iconNode: ASImageNode
if let current = strongSelf.credibilityIconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true
strongSelf.offsetContainerNode.addSubnode(iconNode)
strongSelf.credibilityIconNode = iconNode
}
iconNode.image = currentCredibilityIconImage
transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + 3.0, y: verticalInset + credibilityIconOffset), size: currentCredibilityIconImage.size))
} else if let credibilityIconNode = strongSelf.credibilityIconNode {
strongSelf.credibilityIconNode = nil
credibilityIconNode.removeFromSupernode()
}
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame)
let blobFrame = avatarFrame.insetBy(dx: -14.0, dy: -14.0)
if let getAudioLevel = item.getAudioLevel {
if !strongSelf.didSetupAudioLevel || currentItem?.peer.id != item.peer.id {
strongSelf.audioLevelView?.frame = blobFrame
strongSelf.didSetupAudioLevel = true
strongSelf.audioLevelDisposable.set((getAudioLevel()
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
if strongSelf.audioLevelView == nil, value > 0.0 {
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 1.5,
smallBlobRange: (0, 0),
mediumBlobRange: (0.69, 0.87),
bigBlobRange: (0.71, 1.0)
)
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
let playbackMaskLayer = CAShapeLayer()
playbackMaskLayer.frame = maskRect
playbackMaskLayer.fillRule = .evenOdd
let maskPath = UIBezierPath()
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 14, dy: 14), cornerRadius: 22))
maskPath.append(UIBezierPath(rect: maskRect))
playbackMaskLayer.path = maskPath.cgPath
audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(wavesColor)
strongSelf.audioLevelView = audioLevelView
strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0)
}
let level = min(1.0, max(0.0, CGFloat(value)))
if let audioLevelView = strongSelf.audioLevelView {
audioLevelView.updateLevel(CGFloat(value))
let avatarScale: CGFloat
if value > 0.0 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.13
if let wavesColor = strongSelf.wavesColor {
audioLevelView.setColor(wavesColor, animated: true)
}
} else {
audioLevelView.stopAnimating(duration: 0.5)
avatarScale = 1.0
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true)
}
}))
}
} else if let audioLevelView = strongSelf.audioLevelView {
strongSelf.audioLevelView = nil
audioLevelView.removeFromSuperview()
strongSelf.audioLevelDisposable.set(nil)
}
var overrideImage: AvatarNodeImageOverride?
if item.peer.isDeleted {
overrideImage = .deletedIcon
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad, storeUnrounded: true)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
var hadMicrophoneNode = false
var hadRaiseHandNode = false
var hadIconNode = false
var nodeToAnimateIn: ASDisplayNode?
if case let .microphone(muted, color) = item.icon {
let animationNode: VoiceChatMicrophoneNode
if let current = strongSelf.animationNode {
animationNode = current
} else {
animationNode = VoiceChatMicrophoneNode()
strongSelf.animationNode = animationNode
strongSelf.actionButtonNode.addSubnode(animationNode)
nodeToAnimateIn = animationNode
}
animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: false, color: color), animated: true)
strongSelf.actionButtonNode.isUserInteractionEnabled = false
} else if let animationNode = strongSelf.animationNode {
hadMicrophoneNode = true
strongSelf.animationNode = nil
animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
animationNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in
animationNode?.removeFromSupernode()
})
}
if case .wantsToSpeak = item.icon {
let raiseHandNode: VoiceChatRaiseHandNode
if let current = strongSelf.raiseHandNode {
raiseHandNode = current
} else {
raiseHandNode = VoiceChatRaiseHandNode(color: item.presentationData.theme.list.itemAccentColor)
raiseHandNode.contentMode = .center
strongSelf.raiseHandNode = raiseHandNode
strongSelf.actionButtonNode.addSubnode(raiseHandNode)
nodeToAnimateIn = raiseHandNode
raiseHandNode.playRandomAnimation()
strongSelf.raiseHandTimer = SwiftSignalKit.Timer(timeout: Double.random(in: 8.0 ... 10.5), repeat: true, completion: {
self?.raiseHandNode?.playRandomAnimation()
}, queue: Queue.mainQueue())
strongSelf.raiseHandTimer?.start()
}
strongSelf.actionButtonNode.isUserInteractionEnabled = false
} else if let raiseHandNode = strongSelf.raiseHandNode {
hadRaiseHandNode = true
strongSelf.raiseHandNode = nil
if let raiseHandTimer = strongSelf.raiseHandTimer {
strongSelf.raiseHandTimer = nil
raiseHandTimer.invalidate()
}
raiseHandNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
raiseHandNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak raiseHandNode] _ in
raiseHandNode?.removeFromSupernode()
})
}
if case let .invite(invited) = item.icon {
let iconNode: ASImageNode
if let current = strongSelf.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.contentMode = .center
strongSelf.iconNode = iconNode
strongSelf.actionButtonNode.addSubnode(iconNode)
nodeToAnimateIn = iconNode
}
if invited {
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Invited"), color: UIColor(rgb: 0x979797))
} else {
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: item.presentationData.theme.list.itemAccentColor)
}
strongSelf.actionButtonNode.isUserInteractionEnabled = false
} else if let iconNode = strongSelf.iconNode {
hadIconNode = true
strongSelf.iconNode = nil
iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false, completion: { [weak iconNode] _ in
iconNode?.removeFromSupernode()
})
}
if let node = nodeToAnimateIn, hadMicrophoneNode || hadRaiseHandNode || hadIconNode {
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
let videoSize = CGSize(width: avatarSize, height: avatarSize)
let videoNode = item.getVideo()
if let current = strongSelf.videoNode, current !== videoNode {
current.removeFromSupernode()
}
let actionOffset: CGFloat = 0.0
strongSelf.videoNode = videoNode
if let videoNode = videoNode {
videoNode.updateLayout(size: videoSize, transition: .immediate)
if videoNode.supernode !== strongSelf.avatarNode {
videoNode.clipsToBounds = true
videoNode.cornerRadius = avatarSize / 2.0
strongSelf.avatarNode.addSubnode(videoNode)
}
videoNode.frame = CGRect(origin: CGPoint(), size: videoSize)
}
let animationSize = CGSize(width: 36.0, height: 36.0)
strongSelf.iconNode?.frame = CGRect(origin: CGPoint(), size: animationSize)
strongSelf.animationNode?.frame = CGRect(origin: CGPoint(), size: animationSize)
strongSelf.raiseHandNode?.frame = CGRect(origin: CGPoint(), size: animationSize).insetBy(dx: -6.0, dy: -6.0).offsetBy(dx: -2.0, dy: 0.0)
strongSelf.actionButtonNode.frame = CGRect(x: params.width - animationSize.width - 6.0 - params.rightInset + actionOffset, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height)
if let presence = item.presence as? TelegramUserPresence {
strongSelf.peerPresenceManager?.reset(presence: presence)
}
strongSelf.updateIsHighlighted(transition: transition)
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
strongSelf.setRevealOptionsOpened(item.revealed ?? false, animated: animated)
}
})
}
}
var isHighlighted = false
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
if self.isHighlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if transition.isAnimated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override func header() -> ListViewItemHeader? {
return nil
}
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
}
@objc private func actionButtonPressed() {
if let item = self.layoutParams?.0, let contextAction = item.contextAction {
contextAction(self.contextSourceNode, nil)
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let _ = self.layoutParams?.0, let params = self.layoutParams?.1 {
let leftInset: CGFloat = 65.0 + params.leftInset
var avatarFrame = self.avatarNode.frame
avatarFrame.origin.x = offset + leftInset - 50.0
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
var titleFrame = self.titleNode.frame
titleFrame.origin.x = leftInset + offset
transition.updateFrame(node: self.titleNode, frame: titleFrame)
var statusFrame = self.statusNode.frame
let previousStatusFrame = statusFrame
statusFrame.origin.x = leftInset + offset
self.statusNode.frame = statusFrame
transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
}
}
override func revealOptionsInteractivelyOpened() {
if let item = self.layoutParams?.0 {
item.setPeerIdWithRevealedOptions(item.peer.id, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.layoutParams?.0 {
item.setPeerIdWithRevealedOptions(nil, item.peer.id)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.layoutParams?.0 {
item.revealOptions[Int(option.key)].action()
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}