import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import Postbox import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AvatarNode import TelegramStringFormatting import PeerPresenceStatusManager import ContextUI import AccountContext import ComponentFlow import EmojiStatusComponent import CheckNode import AnimationCache import MultiAnimationRenderer import TextNodeWithEntities private final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private let imageNodeContainer: ASDisplayNode private let imageNode: ASImageNode private var absoluteLocation: (CGRect, CGSize)? private var isCurrentlyInHierarchy = false private var shouldBeAnimating = false override init() { self.imageNodeContainer = ASDisplayNode() self.imageNodeContainer.isLayerBacked = true self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true self.imageNode.displaysAsynchronously = false self.imageNode.displayWithoutProcessing = true self.imageNode.contentMode = .scaleToFill super.init() self.isLayerBacked = true self.clipsToBounds = true self.imageNodeContainer.addSubnode(self.imageNode) self.addSubnode(self.imageNodeContainer) } override func didEnterHierarchy() { super.didEnterHierarchy() self.isCurrentlyInHierarchy = true self.updateAnimation() } override func didExitHierarchy() { super.didExitHierarchy() self.isCurrentlyInHierarchy = false self.updateAnimation() } func update(backgroundColor: UIColor, foregroundColor: UIColor) { if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor let peakColor = foregroundColor.cgColor var locations: [CGFloat] = [0.0, 0.5, 1.0] let colors: [CGColor] = [transparentColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) } func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { return } let sizeUpdated = self.absoluteLocation?.1 != containerSize let frameUpdated = self.absoluteLocation?.0 != rect self.absoluteLocation = (rect, containerSize) if sizeUpdated { if self.shouldBeAnimating { self.imageNode.layer.removeAnimation(forKey: "shimmer") self.addImageAnimation() } } if frameUpdated { self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) } } private func updateAnimation() { let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil if shouldBeAnimating != self.shouldBeAnimating { self.shouldBeAnimating = shouldBeAnimating if shouldBeAnimating { self.addImageAnimation() } else { self.imageNode.layer.removeAnimation(forKey: "shimmer") } } } private func addImageAnimation() { guard let containerSize = self.absoluteLocation?.1 else { return } let gradientHeight: CGFloat = 250.0 self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) animation.repeatCount = Float.infinity animation.beginTime = 1.0 self.imageNode.layer.add(animation, forKey: "shimmer") } } private final class LoadingShimmerNode: ASDisplayNode { enum Shape: Equatable { case circle(CGRect) case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat) } private let backgroundNode: ASDisplayNode private let effectNode: ShimmerEffectNode private let foregroundNode: ASImageNode private var currentShapes: [Shape] = [] private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private var currentShimmeringColor: UIColor? private var currentSize = CGSize() override init() { self.backgroundNode = ASDisplayNode() self.effectNode = ShimmerEffectNode() self.foregroundNode = ASImageNode() self.foregroundNode.displaysAsynchronously = false self.foregroundNode.displayWithoutProcessing = true super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.effectNode) self.addSubnode(self.foregroundNode) } func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.effectNode.updateAbsoluteRect(rect, within: containerSize) } func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], size: CGSize) { if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor self.currentShimmeringColor = shimmeringColor self.currentShapes = shapes self.currentSize = size self.backgroundNode.backgroundColor = foregroundColor self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor) self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in context.setFillColor(backgroundColor.cgColor) context.setBlendMode(.copy) context.fill(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.clear.cgColor) for shape in shapes { switch shape { case let .circle(frame): context.fillEllipse(in: frame) case let .roundedRectLine(startPoint, width, diameter): context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) } } }) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size) self.effectNode.frame = CGRect(origin: CGPoint(), size: size) } } public struct ItemListPeerItemEditing: Equatable { public var editable: Bool public var editing: Bool public var canBeReordered: Bool public var revealed: Bool? public init(editable: Bool, editing: Bool, canBeReordered: Bool = false, revealed: Bool?) { self.editable = editable self.editing = editing self.canBeReordered = canBeReordered self.revealed = revealed } } public enum ItemListPeerItemHeight { case generic case peerList } public enum ItemListPeerItemText { public enum TextColor { case secondary case accent case constructive } case presence case text(String, TextColor) case none } public enum ItemListPeerItemLabelFont { case standard case custom(UIFont) } public enum ItemListPeerItemLabel { case none case text(String, ItemListPeerItemLabelFont) case disclosure(String) case badge(String) case attributedText(NSAttributedString) } public struct ItemListPeerItemSwitch { public var value: Bool public var style: ItemListPeerItemSwitchStyle public var isEnabled: Bool public init(value: Bool, style: ItemListPeerItemSwitchStyle, isEnabled: Bool = true) { self.value = value self.style = style self.isEnabled = isEnabled } } public enum ItemListPeerItemSwitchStyle { case standard case check case leftCheck } public enum ItemListPeerItemAliasHandling { case standard case threatSelfAsSaved } public enum ItemListPeerItemNameColor { case primary case secret } public enum ItemListPeerItemNameStyle { case distinctBold case plain } public enum ItemListPeerItemRevealOptionType { case neutral case warning case destructive case accent } public struct ItemListPeerItemRevealOption { public var type: ItemListPeerItemRevealOptionType public var title: String public var action: () -> Void public init(type: ItemListPeerItemRevealOptionType, title: String, action: @escaping () -> Void) { self.type = type self.title = title self.action = action } } public struct ItemListPeerItemRevealOptions { public var options: [ItemListPeerItemRevealOption] public init(options: [ItemListPeerItemRevealOption]) { self.options = options } } public struct ItemListPeerItemShimmering { public var alternationIndex: Int public init(alternationIndex: Int) { self.alternationIndex = alternationIndex } } public final class ItemListPeerItem: ListViewItem, ItemListItem { public enum Context { public final class Custom { public let accountPeerId: EnginePeer.Id public let postbox: Postbox public let network: Network public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let isPremiumDisabled: Bool public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> public init( accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isPremiumDisabled: Bool, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> ) { self.accountPeerId = accountPeerId self.postbox = postbox self.network = network self.animationCache = animationCache self.animationRenderer = animationRenderer self.isPremiumDisabled = isPremiumDisabled self.resolveInlineStickers = resolveInlineStickers } } case account(AccountContext) case custom(Custom) public var accountPeerId: EnginePeer.Id { switch self { case let .account(context): return context.account.peerId case let .custom(custom): return custom.accountPeerId } } public var postbox: Postbox { switch self { case let .account(context): return context.account.postbox case let .custom(custom): return custom.postbox } } public var network: Network { switch self { case let .account(context): return context.account.network case let .custom(custom): return custom.network } } public var animationCache: AnimationCache { switch self { case let .account(context): return context.animationCache case let .custom(custom): return custom.animationCache } } public var animationRenderer: MultiAnimationRenderer { switch self { case let .account(context): return context.animationRenderer case let .custom(custom): return custom.animationRenderer } } public var isPremiumDisabled: Bool { switch self { case let .account(context): return PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 })).isPremiumDisabled case let .custom(custom): return custom.isPremiumDisabled } } public var resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { switch self { case let .account(context): return { fileIds in return context.engine.stickers.resolveInlineStickers(fileIds: fileIds) } case let .custom(custom): return custom.resolveInlineStickers } } public var energyUsageSettings: EnergyUsageSettings { switch self { case let .account(context): return context.sharedContext.energyUsageSettings case .custom: return .default } } public var contentSettings: ContentSettings { switch self { case let .account(context): return context.currentContentSettings.with { $0 } case .custom: return .default } } } let presentationData: ItemListPresentationData let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder let context: Context let peer: EnginePeer let threadInfo: EngineMessageHistoryThread.Info? let height: ItemListPeerItemHeight let aliasHandling: ItemListPeerItemAliasHandling let customAvatarIcon: UIImage? let nameColor: ItemListPeerItemNameColor let nameStyle: ItemListPeerItemNameStyle let presence: EnginePeer.Presence? let text: ItemListPeerItemText let label: ItemListPeerItemLabel let editing: ItemListPeerItemEditing let revealOptions: ItemListPeerItemRevealOptions? let switchValue: ItemListPeerItemSwitch? let enabled: Bool let highlighted: Bool public let selectable: Bool let highlightable: Bool let animateFirstAvatarTransition: Bool public let sectionId: ItemListSectionId let action: (() -> Void)? let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void let removePeer: (EnginePeer.Id) -> Void let toggleUpdated: ((Bool) -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let hasTopStripe: Bool let hasTopGroupInset: Bool let noInsets: Bool let noCorners: Bool let style: ItemListStyle public let tag: ItemListItemTag? let header: ListViewItemHeader? let shimmering: ItemListPeerItemShimmering? let displayDecorations: Bool let disableInteractiveTransitionIfNecessary: Bool let storyStats: PeerStoryStats? let openStories: ((UIView) -> Void)? public init( presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, customAvatarIcon: UIImage? = nil, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch? = nil, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, storyStats: PeerStoryStats? = nil, openStories: ((UIView) -> Void)? = nil ) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.context = .account(context) self.peer = peer self.threadInfo = threadInfo self.height = height self.aliasHandling = aliasHandling self.customAvatarIcon = customAvatarIcon self.nameColor = nameColor self.nameStyle = nameStyle self.presence = presence self.text = text self.label = label self.editing = editing self.revealOptions = revealOptions self.switchValue = switchValue self.enabled = enabled self.highlighted = highlighted self.selectable = selectable self.highlightable = highlightable self.animateFirstAvatarTransition = animateFirstAvatarTransition self.sectionId = sectionId self.action = action self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.toggleUpdated = toggleUpdated self.contextAction = contextAction self.hasTopStripe = hasTopStripe self.hasTopGroupInset = hasTopGroupInset self.noInsets = noInsets self.noCorners = noCorners self.style = style self.tag = tag self.header = header self.shimmering = shimmering self.displayDecorations = displayDecorations self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary self.storyStats = storyStats self.openStories = openStories } public init( presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: Context, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, customAvatarIcon: UIImage? = nil, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false, storyStats: PeerStoryStats? = nil, openStories: ((UIView) -> Void)? = nil ) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.context = context self.peer = peer self.threadInfo = threadInfo self.height = height self.aliasHandling = aliasHandling self.customAvatarIcon = customAvatarIcon self.nameColor = nameColor self.nameStyle = nameStyle self.presence = presence self.text = text self.label = label self.editing = editing self.revealOptions = revealOptions self.switchValue = switchValue self.enabled = enabled self.highlighted = highlighted self.selectable = selectable self.highlightable = highlightable self.animateFirstAvatarTransition = animateFirstAvatarTransition self.sectionId = sectionId self.action = action self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.toggleUpdated = toggleUpdated self.contextAction = contextAction self.hasTopStripe = hasTopStripe self.hasTopGroupInset = hasTopGroupInset self.noInsets = noInsets self.noCorners = noCorners self.style = style self.tag = tag self.header = header self.shimmering = shimmering self.displayDecorations = displayDecorations self.disableInteractiveTransitionIfNecessary = disableInteractiveTransitionIfNecessary self.storyStats = storyStats self.openStories = openStories } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = ItemListPeerItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem)) node.contentSize = layout.contentSize node.insets = layout.insets Queue.mainQueue().async { completion(node, { return (node.avatarNode.ready, { _ in apply(synchronousLoads, false) }) }) } } } private func getHeaderAtTop(top: ListViewItem?, bottom: ListViewItem?) -> Bool { var headerAtTop = false if let top = top as? ItemListPeerItem, top.header != nil { if top.header?.id != self.header?.id { headerAtTop = true } } else if self.header != nil { headerAtTop = true } return headerAtTop } 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? ItemListPeerItemNode { let makeLayout = nodeValue.asyncLayout() var animated = true if case .None = animation { animated = false } async { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), self.getHeaderAtTop(top: previousItem, bottom: nextItem)) Queue.mainQueue().async { completion(layout, { _ in apply(false, animated) }) } } } } } public func selected(listView: ListView){ listView.clearHighlightAnimated(true) self.action?() } } private let badgeFont = Font.regular(15.0) public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? private let maskNode: ASImageNode private let containerNode: ContextControllerSourceNode public override var controlsContainer: ASDisplayNode { return self.containerNode } fileprivate let avatarNode: AvatarNode private var avatarIconComponent: EmojiStatusComponent? private var avatarIconView: ComponentView? private var customAvatarIconView: UIImageView? private var avatarButton: HighlightTrackingButton? private let titleNode: TextNode private let labelNode: TextNodeWithEntities private let labelBadgeNode: ASImageNode private var labelArrowNode: ASImageNode? private let statusNode: TextNode private var credibilityIconComponent: EmojiStatusComponent? private var credibilityIconView: ComponentHostView? private var verifiedIconComponent: EmojiStatusComponent? private var verifiedIconView: ComponentHostView? private var switchNode: SwitchNode? private var checkNode: ASImageNode? private var leftCheckNode: CheckNode? private var shimmerNode: LoadingShimmerNode? private var absoluteLocation: (CGRect, CGSize)? private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ItemListPeerItem, ListViewItemLayoutParams, ItemListNeighbors, Bool)? private var editableControlNode: ItemListEditableControlNode? private var reorderControlNode: ItemListEditableReorderControlNode? override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = self.visibilityStatus let isVisible: Bool switch self.visibility { case let .visible(fraction, _): isVisible = fraction > 0.01 case .none: isVisible = false } if wasVisible != isVisible { self.visibilityStatus = isVisible } } } private var visibilityStatus: Bool = false { didSet { if self.visibilityStatus != oldValue { if let credibilityIconView = self.credibilityIconView, let credibilityIconComponent = self.credibilityIconComponent { let _ = credibilityIconView.update( transition: .immediate, component: AnyComponent(credibilityIconComponent.withVisibleForAnimations(self.visibilityStatus)), environment: {}, containerSize: credibilityIconView.bounds.size ) } if let verifiedIconView = self.verifiedIconView, let verifiedIconComponent = self.verifiedIconComponent { let _ = verifiedIconView.update( transition: .immediate, component: AnyComponent(verifiedIconComponent.withVisibleForAnimations(self.visibilityStatus)), environment: {}, containerSize: verifiedIconView.bounds.size ) } if let avatarIconView = self.avatarIconView, let avatarIconComponentView = avatarIconView.view, let avatarIconComponent = self.avatarIconComponent { let _ = avatarIconView.update( transition: .immediate, component: AnyComponent(avatarIconComponent.withVisibleForAnimations(self.visibilityStatus)), environment: {}, containerSize: avatarIconComponentView.bounds.size ) } } } } override public var canBeSelected: Bool { if self.editableControlNode != nil || self.disabledOverlayNode != nil { return false } if let item = self.layoutParams?.0, item.action != nil { return true } else { return false } } public var tag: ItemListItemTag? { return self.layoutParams?.0.tag } public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false self.containerNode = ContextControllerSourceNode() self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))) //self.avatarNode.isLayerBacked = !smartInvertColorsEnabled() 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.labelNode = TextNodeWithEntities() self.labelBadgeNode = ASImageNode() self.labelBadgeNode.displayWithoutProcessing = true self.labelBadgeNode.displaysAsynchronously = false self.labelBadgeNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.isAccessibilityElement = true self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.avatarNode) self.containerNode.addSubnode(self.titleNode) self.containerNode.addSubnode(self.statusNode) self.containerNode.addSubnode(self.labelNode.textNode) 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.activated = { [weak self] gesture, _ in guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let contextAction = item.contextAction else { gesture.cancel() return } contextAction(strongSelf.containerNode, gesture) } } override public func didLoad() { super.didLoad() self.updateEnableGestures() } private func updateEnableGestures() { if let item = self.layoutParams?.0, item.disableInteractiveTransitionIfNecessary, let revealOptions = item.revealOptions, !revealOptions.options.isEmpty { self.view.disablesInteractiveTransitionGestureRecognizer = true } else { self.view.disablesInteractiveTransitionGestureRecognizer = false } } public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) var currentDisabledOverlayNode = self.disabledOverlayNode var currentSwitchNode = self.switchNode var currentCheckNode = self.checkNode let currentLabelArrowNode = self.labelArrowNode let currentItem = self.layoutParams?.0 let currentHasBadge = self.labelBadgeNode.image != nil return { item, params, neighbors, headerAtTop in var updateArrowImage: UIImage? let statusFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0) let labelFontSize: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0) let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) let statusFont = Font.regular(statusFontSize) let labelFont = Font.regular(labelFontSize) let labelDisclosureFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) var updatedLabelBadgeImage: UIImage? var credibilityIcon: EmojiStatusComponent.Content? var verifiedIcon: EmojiStatusComponent.Content? if case .threatSelfAsSaved = item.aliasHandling, item.peer.id == item.context.accountPeerId { } else { if item.peer.isScam { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = item.peer.emojiStatus { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isPremium && !item.context.isPremiumDisabled { credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } if item.peer.isVerified { verifiedIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) } else if let verification = item.peer.verification { verifiedIcon = .animation(content: .customEmoji(fileId: verification.iconFileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(0)) } } var titleIconsWidth: CGFloat = 0.0 if let verifiedIcon = verifiedIcon { titleIconsWidth += 4.0 switch verifiedIcon { case let .text(_, string): let textString = NSAttributedString(string: string, font: Font.bold(10.0), textColor: .black, paragraphAlignment: .center) let stringRect = textString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil) titleIconsWidth += floor(stringRect.width) + 11.0 default: titleIconsWidth += 16.0 } } if let credibilityIcon = credibilityIcon { titleIconsWidth += 4.0 switch credibilityIcon { case let .text(_, string): let textString = NSAttributedString(string: string, font: Font.bold(10.0), textColor: .black, paragraphAlignment: .center) let stringRect = textString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil) titleIconsWidth += floor(stringRect.width) + 11.0 default: titleIconsWidth += 16.0 } } var badgeColor: UIColor? if case .badge = item.label { badgeColor = item.presentationData.theme.list.itemAccentColor } let badgeDiameter: CGFloat = 20.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) if let badgeColor = badgeColor { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } } else if let badgeColor = badgeColor, !currentHasBadge { updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor) } var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? var labelAttributedString: NSAttributedString? let peerRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { if let revealOptions = item.revealOptions { var mappedOptions: [ItemListRevealOption] = [] var index: Int32 = 0 for option in revealOptions.options { 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 } else { peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] } } else { peerRevealOptions = [] } var additionalLeftInset: CGFloat = 0.0 var leftInset: CGFloat = params.leftInset var rightInset: CGFloat = params.rightInset let switchSize = CGSize(width: 51.0, height: 31.0) var checkImage: UIImage? if let switchValue = item.switchValue { switch switchValue.style { case .standard: if currentSwitchNode == nil { currentSwitchNode = SwitchNode() } rightInset += switchSize.width currentCheckNode = nil case .check: checkImage = PresentationResourcesItemList.checkIconImage(item.presentationData.theme) if currentCheckNode == nil { currentCheckNode = ASImageNode() } rightInset += 24.0 currentSwitchNode = nil case .leftCheck: additionalLeftInset += 40.0 leftInset += additionalLeftInset currentSwitchNode = nil currentCheckNode = nil } } else { currentSwitchNode = nil currentCheckNode = nil } let titleColor: UIColor switch item.nameColor { case .primary: titleColor = item.presentationData.theme.list.itemPrimaryTextColor case .secret: titleColor = item.presentationData.theme.chatList.secretTitleColor } let currentBoldFont: UIFont switch item.nameStyle { case .distinctBold: currentBoldFont = titleBoldFont case .plain: currentBoldFont = titleFont } if let threadInfo = item.threadInfo { titleAttributedString = NSAttributedString(string: threadInfo.title, font: currentBoldFont, textColor: titleColor) } else if item.peer.id == item.context.accountPeerId, case .threatSelfAsSaved = item.aliasHandling { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: currentBoldFont, textColor: titleColor) } else if item.peer.id.isReplies { titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: currentBoldFont, textColor: titleColor) } else if item.peer.id.isAnonymousSavedMessages { titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_AuthorHidden, font: currentBoldFont, textColor: titleColor) } else if case let .user(user) = item.peer { 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 case let .legacyGroup(group) = item.peer { titleAttributedString = NSAttributedString(string: group.title, font: currentBoldFont, textColor: titleColor) } else if case let .channel(channel) = item.peer { titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor) } switch item.text { case .presence: if case let .user(user) = item.peer, 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 { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let (string, activity) = stringAndActivityForUserPresence(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : 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 .secondary: textColorValue = item.presentationData.theme.list.itemSecondaryTextColor case .accent: textColorValue = item.presentationData.theme.list.itemAccentColor case .constructive: textColorValue = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor } statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue) case .none: break } let verticalInset: CGFloat let verticalOffset: CGFloat let avatarSize: CGFloat let avatarFontSize: CGFloat switch item.height { case .generic: if case .none = item.text { verticalInset = 11.0 } else { verticalInset = 6.0 } verticalOffset = 0.0 avatarSize = 31.0 leftInset += 59.0 avatarFontSize = floor(31.0 * 16.0 / 37.0) case .peerList: if case .none = item.text { verticalInset = 14.0 } else { verticalInset = 8.0 } verticalOffset = 0.0 avatarSize = 40.0 leftInset += 65.0 avatarFontSize = floor(40.0 * 16.0 / 37.0) } var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? let editingOffset: CGFloat var reorderInset: CGFloat = 0.0 if item.editing.editing { let sizeAndApply = editableControlLayout(item.presentationData.theme, false) editableControlSizeAndApply = sizeAndApply editingOffset = sizeAndApply.0 if item.editing.canBeReordered { let reorderSizeAndApply = reorderControlLayout(item.presentationData.theme) reorderControlSizeAndApply = reorderSizeAndApply reorderInset = reorderSizeAndApply.0 } } else { editingOffset = 0.0 } var labelMaximumNumberOfLines = 1 var labelInset: CGFloat = 0.0 var labelAlignment: NSTextAlignment = .natural var updatedLabelArrowNode: ASImageNode? switch item.label { case .none: break case let .attributedText(text): labelAttributedString = text labelInset += 15.0 labelMaximumNumberOfLines = 2 labelAlignment = .right case let .text(text, font): let selectedFont: UIFont switch font { case .standard: selectedFont = labelFont case let .custom(value): selectedFont = value } labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) labelInset += 15.0 case let .disclosure(text): if let currentLabelArrowNode = currentLabelArrowNode { updatedLabelArrowNode = currentLabelArrowNode } else { let arrowNode = ASImageNode() arrowNode.isLayerBacked = true arrowNode.displayWithoutProcessing = true arrowNode.displaysAsynchronously = false arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) updatedLabelArrowNode = arrowNode } labelInset += 40.0 labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) case let .badge(text): labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) labelInset += 15.0 } labelInset += reorderInset let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: labelMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: labelAlignment, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let constrainedTitleSize = CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedTitleSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let constrainedStatusSize = CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - labelInset, height: CGFloat.greatestFiniteMagnitude) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedStatusSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var insets = itemListNeighborsGroupedInsets(neighbors, params) if !item.hasTopGroupInset { switch neighbors.top { case .none: insets.top = 0.0 default: break } } if item.noInsets { insets.top = 0.0 insets.bottom = 0.0 } if headerAtTop, let header = item.header { insets.top += header.height + 18.0 } 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 } return (layout, { [weak self] synchronousLoad, animated in if let strongSelf = self { strongSelf.layoutParams = (item, params, neighbors, headerAtTop) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.containerNode.isGestureEnabled = item.contextAction != nil strongSelf.avatarNode.font = avatarPlaceholderFont(size: avatarFontSize) strongSelf.accessibilityLabel = titleAttributedString?.string var combinedValueString = "" if let statusString = statusAttributedString?.string, !statusString.isEmpty { combinedValueString.append(statusString) } if let labelString = labelAttributedString?.string, !labelString.isEmpty { combinedValueString.append(", \(labelString)") } strongSelf.accessibilityValue = combinedValueString if let updateArrowImage = updateArrowImage { strongSelf.labelArrowNode?.image = updateArrowImage } let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor switch item.style { case .plain: itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor case .blocks: itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor } strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor let revealOffset = strongSelf.revealOffset 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 editableControlSizeAndApply = editableControlSizeAndApply { let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { let editableControlNode = editableControlSizeAndApply.1(layout.contentSize.height) editableControlNode.tapped = { if let strongSelf = self { strongSelf.setRevealOptionsOpened(true, animated: true) strongSelf.revealOptionsInteractivelyOpened() } } strongSelf.editableControlNode = editableControlNode strongSelf.addSubnode(editableControlNode) editableControlNode.frame = editableControlFrame transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) editableControlNode.alpha = 0.0 transition.updateAlpha(node: editableControlNode, alpha: 1.0) } else { strongSelf.editableControlNode?.frame = editableControlFrame } strongSelf.editableControlNode?.isHidden = !item.editing.editable } else if let editableControlNode = strongSelf.editableControlNode { var editableControlFrame = editableControlNode.frame editableControlFrame.origin.x = -editableControlFrame.size.width strongSelf.editableControlNode = nil transition.updateAlpha(node: editableControlNode, alpha: 0.0) transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in editableControlNode?.removeFromSupernode() }) } if let reorderControlSizeAndApply = reorderControlSizeAndApply { if strongSelf.reorderControlNode == nil { let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) strongSelf.reorderControlNode = reorderControlNode strongSelf.addSubnode(reorderControlNode) reorderControlNode.alpha = 0.0 transition.updateAlpha(node: reorderControlNode, alpha: 1.0) } let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) strongSelf.reorderControlNode?.frame = reorderControlFrame } else if let reorderControlNode = strongSelf.reorderControlNode { strongSelf.reorderControlNode = nil transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in reorderControlNode?.removeFromSupernode() }) } let _ = titleApply() let _ = statusApply() if case let .account(context) = item.context { let _ = labelApply(TextNodeWithEntities.Arguments(context: context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false)) } else { let _ = labelApply(nil) } strongSelf.labelNode.textNode.isHidden = labelAttributedString == nil if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } if strongSelf.topStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) } if strongSelf.bottomStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { strongSelf.addSubnode(strongSelf.maskNode) } let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners var hasTopCorners = false var hasBottomCorners = false switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight strongSelf.bottomStripeNode.isHidden = !item.displayDecorations default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners || !item.displayDecorations } strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) var titleFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size) var titleLeftOffset: CGFloat = 0.0 if let verifiedIcon = verifiedIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer let verifiedIconView: ComponentHostView if let current = strongSelf.verifiedIconView { verifiedIconView = current } else { verifiedIconView = ComponentHostView() strongSelf.containerNode.view.addSubview(verifiedIconView) strongSelf.verifiedIconView = verifiedIconView } let verifiedIconComponent = EmojiStatusComponent( postbox: item.context.postbox, energyUsageSettings: item.context.energyUsageSettings, resolveInlineStickers: item.context.resolveInlineStickers, animationCache: animationCache, animationRenderer: animationRenderer, content: verifiedIcon, isVisibleForAnimations: strongSelf.visibilityStatus, action: nil, emojiFileUpdated: nil ) strongSelf.verifiedIconComponent = verifiedIconComponent let iconSize = verifiedIconView.update( transition: .immediate, component: AnyComponent(verifiedIconComponent), environment: {}, containerSize: CGSize(width: 20.0, height: 20.0) ) transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) titleLeftOffset += iconSize.width + 4.0 } else if let verifiedIconView = strongSelf.verifiedIconView { strongSelf.verifiedIconView = nil verifiedIconView.removeFromSuperview() } titleFrame = titleFrame.offsetBy(dx: titleLeftOffset, dy: 0.0) transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size)) if let credibilityIcon = credibilityIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer let credibilityIconView: ComponentHostView if let current = strongSelf.credibilityIconView { credibilityIconView = current } else { credibilityIconView = ComponentHostView() strongSelf.containerNode.view.addSubview(credibilityIconView) strongSelf.credibilityIconView = credibilityIconView } let credibilityIconComponent = EmojiStatusComponent( postbox: item.context.postbox, energyUsageSettings: item.context.energyUsageSettings, resolveInlineStickers: item.context.resolveInlineStickers, animationCache: animationCache, animationRenderer: animationRenderer, content: credibilityIcon, isVisibleForAnimations: strongSelf.visibilityStatus, action: nil, emojiFileUpdated: nil ) strongSelf.credibilityIconComponent = credibilityIconComponent let iconSize = credibilityIconView.update( transition: .immediate, component: AnyComponent(credibilityIconComponent), environment: {}, containerSize: CGSize(width: 20.0, height: 20.0) ) transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) } else if let credibilityIconView = strongSelf.credibilityIconView { strongSelf.credibilityIconView = nil credibilityIconView.removeFromSuperview() } if let currentSwitchNode = currentSwitchNode { if currentSwitchNode !== strongSelf.switchNode { strongSelf.switchNode = currentSwitchNode strongSelf.containerNode.addSubnode(currentSwitchNode) currentSwitchNode.valueUpdated = { value in if let strongSelf = self { strongSelf.toggleUpdated(value) } } } currentSwitchNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize) if let switchValue = item.switchValue { currentSwitchNode.setOn(switchValue.value, animated: animated) } } else if let switchNode = strongSelf.switchNode { switchNode.removeFromSupernode() strongSelf.switchNode = nil } if let currentCheckNode = currentCheckNode { if currentCheckNode !== strongSelf.checkNode { strongSelf.checkNode = currentCheckNode strongSelf.containerNode.addSubnode(currentCheckNode) } if let checkImage = checkImage { currentCheckNode.image = checkImage currentCheckNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - checkImage.size.width - floor((44.0 - checkImage.size.width) / 2.0), y: floor((layout.contentSize.height - checkImage.size.height) / 2.0)), size: checkImage.size) } if let switchValue = item.switchValue { currentCheckNode.isHidden = !switchValue.value } } else if let checkNode = strongSelf.checkNode { checkNode.removeFromSupernode() strongSelf.checkNode = nil } var rightLabelInset: CGFloat = 15.0 + params.rightInset if let updatedLabelArrowNode = updatedLabelArrowNode { strongSelf.labelArrowNode = updatedLabelArrowNode strongSelf.containerNode.addSubnode(updatedLabelArrowNode) if let image = updatedLabelArrowNode.image { let labelArrowNodeFrame = CGRect(origin: CGPoint(x: params.width - rightLabelInset - image.size.width + 8.0, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) transition.updateFrame(node: updatedLabelArrowNode, frame: labelArrowNodeFrame) rightLabelInset += 19.0 } } else if let labelArrowNode = strongSelf.labelArrowNode { labelArrowNode.removeFromSupernode() strongSelf.labelArrowNode = nil } let badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0) let labelFrame: CGRect if case .badge = item.label { labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) strongSelf.labelNode.textNode.frame = labelFrame } else { labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) transition.updateFrame(node: strongSelf.labelNode.textNode, frame: labelFrame) } if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode.textNode) } strongSelf.labelBadgeNode.image = updateBadgeImage } if badgeColor == nil && strongSelf.labelBadgeNode.supernode != nil { strongSelf.labelBadgeNode.image = nil strongSelf.labelBadgeNode.removeFromSupernode() } strongSelf.labelBadgeNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth, y: labelFrame.minY - 1.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + additionalLeftInset + revealOffset + editingOffset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame) if item.storyStats != nil { let avatarButton: HighlightTrackingButton if let current = strongSelf.avatarButton { avatarButton = current } else { avatarButton = HighlightTrackingButton() strongSelf.avatarButton = avatarButton strongSelf.containerNode.view.addSubview(avatarButton) avatarButton.addTarget(strongSelf, action: #selector(strongSelf.avatarButtonPressed), for: .touchUpInside) } avatarButton.frame = avatarFrame } else if let avatarButton = strongSelf.avatarButton { strongSelf.avatarButton = nil avatarButton.removeFromSuperview() } if let switchValue = item.switchValue, case .leftCheck = switchValue.style { let leftCheckNode: CheckNode if let current = strongSelf.leftCheckNode { leftCheckNode = current } else { var checkTheme = CheckNodeTheme(theme: item.presentationData.theme, style: .plain) checkTheme.isDottedBorder = !switchValue.isEnabled leftCheckNode = CheckNode(theme: checkTheme) leftCheckNode.isUserInteractionEnabled = false strongSelf.leftCheckNode = leftCheckNode strongSelf.avatarNode.supernode?.addSubnode(leftCheckNode) } leftCheckNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: floor((layout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0)) leftCheckNode.setSelected(switchValue.value, animated: animated) } else { if let leftCheckNode = strongSelf.leftCheckNode { strongSelf.leftCheckNode = nil leftCheckNode.removeFromSupernode() } } if let threadInfo = item.threadInfo { let threadIconSize = floor(avatarSize * 0.9) let threadIconFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - threadIconSize) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - threadIconSize) / 2.0)), size: CGSize(width: threadIconSize, height: threadIconSize)) strongSelf.avatarNode.isHidden = true let avatarIconView: ComponentView if let current = strongSelf.avatarIconView { avatarIconView = current } else { avatarIconView = ComponentView() strongSelf.avatarIconView = avatarIconView } let avatarIconContent: EmojiStatusComponent.Content if let fileId = threadInfo.icon, fileId != 0 { avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .forever) } else { avatarIconContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: threadIconFrame.size) } let avatarIconComponent = EmojiStatusComponent( postbox: item.context.postbox, energyUsageSettings: item.context.energyUsageSettings, resolveInlineStickers: item.context.resolveInlineStickers, animationCache: item.context.animationCache, animationRenderer: item.context.animationRenderer, content: avatarIconContent, isVisibleForAnimations: strongSelf.visibilityStatus, action: nil, emojiFileUpdated: nil ) strongSelf.avatarIconComponent = avatarIconComponent let _ = avatarIconView.update( transition: .immediate, component: AnyComponent(avatarIconComponent), environment: {}, containerSize: threadIconFrame.size ) if let avatarIconComponentView = avatarIconView.view { if avatarIconComponentView.superview == nil { strongSelf.containerNode.view.addSubview(avatarIconComponentView) } transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame) } } else { if item.peer.id == item.context.accountPeerId, case .threatSelfAsSaved = item.aliasHandling { strongSelf.avatarNode.setPeer( accountPeerId: item.context.accountPeerId, postbox: item.context.postbox, network: item.context.network, contentSettings: item.context.contentSettings, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad ) } else if item.peer.id.isReplies { strongSelf.avatarNode.setPeer( accountPeerId: item.context.accountPeerId, postbox: item.context.postbox, network: item.context.network, contentSettings: item.context.contentSettings, theme: item.presentationData.theme, peer: item.peer, overrideImage: .repliesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad ) } else { var overrideImage: AvatarNodeImageOverride? if item.peer.isDeleted { overrideImage = .deletedIcon } strongSelf.avatarNode.imageNode.animateFirstTransition = item.animateFirstAvatarTransition var clipStyle: AvatarNodeClipStyle = .round if case let .channel(channel) = item.peer, channel.isForum { clipStyle = .roundedRect } strongSelf.avatarNode.setPeer( accountPeerId: item.context.accountPeerId, postbox: item.context.postbox, network: item.context.network, contentSettings: item.context.contentSettings, theme: item.presentationData.theme, peer: item.peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad ) strongSelf.avatarNode.setStoryStats(storyStats: item.storyStats.flatMap { storyStats in return AvatarNode.StoryStats( totalCount: storyStats.totalCount, unseenCount: storyStats.unseenCount, hasUnseenCloseFriendsItems: storyStats.hasUnseenCloseFriends ) }, presentationParams: AvatarNode.StoryPresentationParams( colors: AvatarNode.Colors(theme: item.presentationData.theme), lineWidth: 1.33, inactiveLineWidth: 1.33 ), transition: .immediate) } } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) if let presence = item.presence { strongSelf.peerPresenceManager?.reset(presence: presence) } if let shimmering = item.shimmering { strongSelf.avatarNode.isHidden = true strongSelf.titleNode.isHidden = true let shimmerNode: LoadingShimmerNode if let current = strongSelf.shimmerNode { shimmerNode = current } else { shimmerNode = LoadingShimmerNode() strongSelf.shimmerNode = shimmerNode strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode) } shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) if let (rect, size) = strongSelf.absoluteLocation { shimmerNode.updateAbsoluteRect(rect, within: size) } var shapes: [LoadingShimmerNode.Shape] = [] shapes.append(.circle(strongSelf.avatarNode.frame)) let possibleLines: [[CGFloat]] = [ [50.0, 40.0], [70.0, 45.0] ] let titleFrame = strongSelf.titleNode.frame let lineDiameter: CGFloat = 10.0 var lineStart = titleFrame.minX for lineWidth in possibleLines[shimmering.alternationIndex % possibleLines.count] { shapes.append(.roundedRectLine(startPoint: CGPoint(x: lineStart, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: lineWidth, diameter: lineDiameter)) lineStart += lineWidth + lineDiameter } shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize) } else if let shimmerNode = strongSelf.shimmerNode { strongSelf.avatarNode.isHidden = false strongSelf.titleNode.isHidden = false strongSelf.shimmerNode = nil shimmerNode.removeFromSupernode() } if let customAvatarIcon = item.customAvatarIcon { strongSelf.avatarNode.isHidden = true let customAvatarIconView: UIImageView if let current = strongSelf.customAvatarIconView { customAvatarIconView = current } else { customAvatarIconView = UIImageView() strongSelf.customAvatarIconView = customAvatarIconView strongSelf.containerNode.view.addSubview(customAvatarIconView) } customAvatarIconView.image = customAvatarIcon transition.updateFrame(view: customAvatarIconView, frame: strongSelf.avatarNode.frame) } else if let customAvatarIconView = strongSelf.customAvatarIconView { strongSelf.customAvatarIconView = nil customAvatarIconView.removeFromSuperview() } strongSelf.backgroundNode.isHidden = !item.displayDecorations strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations || !item.highlightable strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) if let revealed = item.editing.revealed { strongSelf.setRevealOptionsOpened(revealed, animated: animated) } strongSelf.updateIsHighlighted(transition: transition) } }) } } var isHighlighted = false var reallyHighlighted: Bool { var reallyHighlighted = self.isHighlighted if let (item, _, _, _) = self.layoutParams, item.highlighted { reallyHighlighted = true } return reallyHighlighted } func updateIsHighlighted(transition: ContainedViewLayoutTransition) { if self.reallyHighlighted { 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 } else if self.backgroundNode.supernode != nil { anchorNode = self.backgroundNode } 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 public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) if let avatarButton = self.avatarButton, avatarButton.bounds.contains(self.view.convert(point, to: avatarButton)) { self.isHighlighted = false } else { self.isHighlighted = highlighted self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) guard let item = self.layoutParams?.0, let params = self.layoutParams?.1 else { return } let leftInset: CGFloat switch item.height { case .generic: leftInset = 59.0 + params.leftInset case .peerList: leftInset = 65.0 + params.leftInset } let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width var editableControlFrame = editableControlNode.frame editableControlFrame.origin.x = params.leftInset + offset transition.updateFrame(node: editableControlNode, frame: editableControlFrame) } else { editingOffset = 0.0 } var titleLeftOffset: CGFloat = 0.0 if let verifiedIconView = self.verifiedIconView { transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verifiedIconView.frame.minY), size: verifiedIconView.bounds.size)) titleLeftOffset += verifiedIconView.bounds.size.width + 4.0 } transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset + titleLeftOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) if let credibilityIconView = self.credibilityIconView { transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 4.0, y: credibilityIconView.frame.minY), size: credibilityIconView.bounds.size)) } var rightLabelInset: CGFloat = 15.0 + params.rightInset if let labelArrowNode = self.labelArrowNode { if let image = labelArrowNode.image { let labelArrowNodeFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - image.size.width + 8.0, y: labelArrowNode.frame.minY), size: image.size) transition.updateFrame(node: labelArrowNode, frame: labelArrowNodeFrame) rightLabelInset += 19.0 } } let badgeDiameter: CGFloat = 20.0 let labelSize = self.labelNode.textNode.frame.size let badgeWidth = max(badgeDiameter, labelSize.width + 10.0) let labelFrame: CGRect if case .badge = item.label { labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.textNode.frame.minY), size: labelSize) } else { labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.textNode.bounds.size.width - rightLabelInset, y: self.labelNode.textNode.frame.minY), size: self.labelNode.textNode.bounds.size) } transition.updateFrame(node: self.labelNode.textNode, frame: labelFrame) transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth, y: self.labelBadgeNode.frame.minY), size: CGSize(width: badgeWidth, height: badgeDiameter))) let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size) transition.updateFrame(node: self.avatarNode, frame: avatarFrame) if let avatarButton = self.avatarButton { avatarButton.frame = avatarFrame } if let customAvatarIconView = self.customAvatarIconView { transition.updateFrame(view: customAvatarIconView, frame: avatarFrame) } if let avatarIconComponentView = self.avatarIconView?.view { let avatarFrame = self.avatarNode.frame let threadIconSize = floor(avatarFrame.width * 0.9) let threadIconFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - threadIconSize) / 2.0), y: avatarFrame.minY + floor((avatarFrame.height - threadIconSize) / 2.0)), size: CGSize(width: threadIconSize, height: threadIconSize)) transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame) } } override public func revealOptionsInteractivelyOpened() { if let (item, _, _, _) = self.layoutParams { item.setPeerIdWithRevealedOptions(item.peer.id, nil) } } override public func revealOptionsInteractivelyClosed() { if let (item, _, _, _) = self.layoutParams { item.setPeerIdWithRevealedOptions(nil, item.peer.id) } } override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { self.setRevealOptionsOpened(false, animated: true) self.revealOptionsInteractivelyClosed() if let (item, _, _, _) = self.layoutParams { if let revealOptions = item.revealOptions { if option.key >= 0 && option.key < Int32(revealOptions.options.count) { revealOptions.options[Int(option.key)].action() } } else { item.removePeer(item.peer.id) } } } private func toggleUpdated(_ value: Bool) { if let (item, _, _, _) = self.layoutParams { item.toggleUpdated?(value) } } override public func headers() -> [ListViewItemHeader]? { if let item = self.layoutParams?.0 { return item.header.flatMap { [$0] } } else { return nil } } override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { var rect = rect rect.origin.y += self.insets.top self.absoluteLocation = (rect, containerSize) if let shimmerNode = self.shimmerNode { shimmerNode.updateAbsoluteRect(rect, within: containerSize) } } override public func isReorderable(at point: CGPoint) -> Bool { if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { return true } return false } @objc private func avatarButtonPressed() { guard let item = self.layoutParams?.0 else { return } item.openStories?(self.avatarNode.view) } } public final class ItemListPeerItemHeader: ListViewItemHeader { public let id: ListViewItemNode.HeaderId public let context: AccountContext public let text: NSAttributedString public let additionalText: String public let stickDirection: ListViewItemHeaderStickDirection = .topEdge public let stickOverInsets: Bool = true public let theme: PresentationTheme public let strings: PresentationStrings public let actionTitle: String? public let action: (() -> Void)? public let height: CGFloat = 28.0 public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) { self.context = context self.text = text self.additionalText = additionalText self.id = ListViewItemNode.HeaderId(space: 0, id: id) self.theme = theme self.strings = strings self.actionTitle = actionTitle self.action = action } public func combinesWith(other: ListViewItemHeader) -> Bool { if let other = other as? ItemListPeerItemHeader, other.id == self.id { return true } else { return false } } public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, context: self.context, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) } public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { (node as? ItemListPeerItemHeaderNode)?.update(text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) } } public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListHeaderItemNode { private var theme: PresentationTheme private var strings: PresentationStrings private let context: AccountContext private var actionTitle: String? private var action: (() -> Void)? private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? private let backgroundNode: ASDisplayNode private let snappedBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let textNode: ImmediateTextNodeWithEntities private let additionalTextNode: ImmediateTextNode private let actionTextNode: ImmediateTextNode private let actionButton: HighlightableButtonNode private var stickDistanceFactor: CGFloat? public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) { self.context = context self.theme = theme self.strings = strings self.actionTitle = actionTitle self.action = action self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor self.snappedBackgroundNode = ASDisplayNode() self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor self.snappedBackgroundNode.alpha = 0.0 self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor self.separatorNode.alpha = 0.0 let titleFont = Font.regular(13.0) self.textNode = ImmediateTextNodeWithEntities() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 1 self.textNode.attributedText = text self.textNode.arguments = TextNodeWithEntities.Arguments( context: self.context, cache: self.context.animationCache, renderer: self.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, attemptSynchronous: true ) self.textNode.visibility = true self.additionalTextNode = ImmediateTextNode() self.additionalTextNode.displaysAsynchronously = false self.additionalTextNode.maximumNumberOfLines = 1 self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor) self.actionTextNode = ImmediateTextNode() self.actionTextNode.displaysAsynchronously = false self.actionTextNode.maximumNumberOfLines = 1 self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor) self.actionButton = HighlightableButtonNode() self.actionButton.isUserInteractionEnabled = self.action != nil super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.snappedBackgroundNode) self.addSubnode(self.separatorNode) self.addSubnode(self.textNode) self.addSubnode(self.additionalTextNode) self.addSubnode(self.actionTextNode) self.addSubnode(self.actionButton) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) self.actionButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.actionTextNode.layer.removeAnimation(forKey: "opacity") strongSelf.actionTextNode.alpha = 0.4 } else { strongSelf.actionTextNode.alpha = 1.0 strongSelf.actionTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } } @objc private func actionButtonPressed() { self.action?() } public func updateTheme(theme: PresentationTheme) { self.theme = theme self.backgroundNode.backgroundColor = theme.list.blocksBackgroundColor self.snappedBackgroundNode.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor self.separatorNode.backgroundColor = theme.list.itemBlocksSeparatorColor let titleFont = Font.regular(13.0) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) self.additionalTextNode.attributedText = NSAttributedString(string: self.additionalTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) } public func update(text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) { self.actionTitle = actionTitle self.action = action let titleFont = Font.regular(13.0) self.textNode.attributedText = text self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor) self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor) self.actionButton.isUserInteractionEnabled = self.action != nil if let (size, leftInset, rightInset) = self.validLayout { self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) } } override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { self.validLayout = (size, leftInset, rightInset) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.snappedBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)) let sideInset: CGFloat = 15.0 + leftInset let actionTextSize = self.actionTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: size.height)) let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - actionTextSize.width - 8.0, height: size.height)) let textSize = self.textNode.updateLayout(CGSize(width: max(1.0, size.width - sideInset * 2.0 - actionTextSize.width - 8.0 - additionalTextSize.width), height: size.height)) let textFrame = CGRect(origin: CGPoint(x: sideInset, y: 7.0), size: textSize) self.textNode.frame = textFrame self.additionalTextNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX, y: 7.0), size: additionalTextSize) self.actionTextNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 7.0), size: actionTextSize) self.actionButton.frame = CGRect(origin: CGPoint(x: size.width - sideInset - actionTextSize.width, y: 0.0), size: CGSize(width: actionTextSize.width, height: size.height)) } override public func animateRemoved(duration: Double) { self.alpha = 0.0 self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true) } override public func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { if self.stickDistanceFactor == factor { return } self.stickDistanceFactor = factor if let (size, leftInset, _) = self.validLayout { if leftInset.isZero { transition.updateAlpha(node: self.separatorNode, alpha: 1.0) transition.updateAlpha(node: self.snappedBackgroundNode, alpha: (1.0 - factor) * 0.0 + factor * 1.0) } else { let distance = factor * size.height let alpha = abs(distance) / 16.0 transition.updateAlpha(node: self.separatorNode, alpha: max(0.0, min(1.0, alpha))) transition.updateAlpha(node: self.snappedBackgroundNode, alpha: 0.0) } } } }