import Foundation import UIKit import AsyncDisplayKit import Postbox import Display import SwiftSignalKit import TelegramCore private let titleFont = Font.regular(17.0) private let titleBoldFont = Font.medium(17.0) private let statusFont = Font.regular(13.0) private let badgeFont = Font.regular(14.0) enum ContactsPeerItemStatus { case none case presence(PeerPresence, PresentationDateTimeFormat) case addressName(String) case custom(String) } enum ContactsPeerItemSelection: Equatable { case none case selectable(selected: Bool) static func ==(lhs: ContactsPeerItemSelection, rhs: ContactsPeerItemSelection) -> Bool { switch lhs { case .none: if case .none = rhs { return true } else { return false } case let .selectable(selected): if case .selectable(selected) = rhs { return true } else { return false } } } } struct ContactsPeerItemEditing: Equatable { let editable: Bool let editing: Bool let revealed: Bool static func ==(lhs: ContactsPeerItemEditing, rhs: ContactsPeerItemEditing) -> Bool { if lhs.editable != rhs.editable { return false } if lhs.editing != rhs.editing { return false } if lhs.revealed != rhs.revealed { return false } return true } } enum ContactsPeerItemPeerMode { case generalSearch case peer } enum ContactsPeerItemBadgeType { case active case inactive } struct ContactsPeerItemBadge { let count: Int32 let type: ContactsPeerItemBadgeType } enum ContactsPeerItemPeer: Equatable { case peer(peer: Peer?, chatPeer: Peer?) case deviceContact(stableId: DeviceContactStableId, contact: DeviceContactBasicData) static func ==(lhs: ContactsPeerItemPeer, rhs: ContactsPeerItemPeer) -> Bool { switch lhs { case let .peer(lhsPeer, lhsChatPeer): if case let .peer(rhsPeer, rhsChatPeer) = rhs { if !arePeersEqual(lhsPeer, rhsPeer) { return false } if !arePeersEqual(lhsChatPeer, rhsChatPeer) { return false } return true } else { return false } case let .deviceContact(stableId, contact): if case .deviceContact(stableId, contact) = rhs { return true } else { return false } } } } class ContactsPeerItem: ListViewItem { let theme: PresentationTheme let strings: PresentationStrings let sortOrder: PresentationPersonNameOrder let displayOrder: PresentationPersonNameOrder let account: Account let peerMode: ContactsPeerItemPeerMode let peer: ContactsPeerItemPeer let status: ContactsPeerItemStatus let badge: ContactsPeerItemBadge? let enabled: Bool let selection: ContactsPeerItemSelection let editing: ContactsPeerItemEditing let action: (ContactsPeerItemPeer) -> Void let setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? let deletePeer: ((PeerId) -> Void)? let selectable: Bool let headerAccessoryItem: ListViewAccessoryItem? let header: ListViewItemHeader? init(theme: PresentationTheme, strings: PresentationStrings, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, account: Account, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil) { self.theme = theme self.strings = strings self.sortOrder = sortOrder self.displayOrder = displayOrder self.account = account self.peerMode = peerMode self.peer = peer self.status = status self.badge = badge self.enabled = enabled self.selection = selection self.editing = editing self.action = action self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.deletePeer = deletePeer self.header = header self.selectable = self.enabled if let index = index { var letter: String = "#" switch peer { case let .peer(peer, _): if let user = peer as? TelegramUser { switch index { case .firstNameFirst: if let firstName = user.firstName, !firstName.isEmpty { letter = String(firstName.prefix(1)).uppercased() } else if let lastName = user.lastName, !lastName.isEmpty { letter = String(lastName.prefix(1)).uppercased() } case .lastNameFirst: if let lastName = user.lastName, !lastName.isEmpty { letter = String(lastName.prefix(1)).uppercased() } else if let firstName = user.firstName, !firstName.isEmpty { letter = String(firstName.prefix(1)).uppercased() } } } else if let group = peer as? TelegramGroup { if !group.title.isEmpty { letter = String(group.title.prefix(1)).uppercased() } } else if let channel = peer as? TelegramChannel { if !channel.title.isEmpty { letter = String(channel.title.prefix(1)).uppercased() } } case let .deviceContact(_, contact): switch index { case .firstNameFirst: if !contact.firstName.isEmpty { letter = String(contact.firstName.prefix(1)).uppercased() } else if !contact.lastName.isEmpty { letter = String(contact.lastName.prefix(1)).uppercased() } case .lastNameFirst: if !contact.lastName.isEmpty { letter = String(contact.lastName.prefix(1)).uppercased() } else if !contact.firstName.isEmpty { letter = String(contact.firstName.prefix(1)).uppercased() } } } self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter), theme: theme) } else { self.headerAccessoryItem = nil } } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ContactsPeerItemNode() let makeLayout = node.asyncLayout() let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets Queue.mainQueue().async { completion(node, { let (signal, apply) = nodeApply() return (signal, { apply(false) }) }) } } } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ContactsPeerItemNode { let layout = nodeValue.asyncLayout() async { let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader) Queue.mainQueue().async { completion(nodeLayout, { apply().1(animation.isAnimated) }) } } } } } func selected(listView: ListView) { listView.clearHighlightAnimated(true) self.action(self.peer) } static func mergeType(item: ContactsPeerItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) { var first = false var last = false var firstWithHeader = false if let previousItem = previousItem { if let header = item.header { if let previousItem = previousItem as? ContactsPeerItem { firstWithHeader = header.id != previousItem.header?.id } else { firstWithHeader = true } } } else { first = true firstWithHeader = item.header != nil } if let nextItem = nextItem { if let header = item.header { if let nextItem = nextItem as? ContactsPeerItem { last = header.id != nextItem.header?.id } else { last = true } } } else { last = true } return (first, last, firstWithHeader) } } private let separatorHeight = 1.0 / UIScreen.main.scale private let avatarFont: UIFont = UIFont(name: ".SFCompactRounded-Semibold", size: 16.0)! class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private let avatarNode: AvatarNode private let titleNode: TextNode private var verificationIconNode: ASImageNode? private let statusNode: TextNode private var badgeBackgroundNode: ASImageNode? private var badgeTextNode: TextNode? private var selectionNode: CheckNode? private var avatarState: (Account, Peer?)? private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool)? var chatPeer: Peer? { if let peer = self.layoutParams?.0.peer { switch peer { case let .peer(peer, chatPeer): return chatPeer ?? peer case .deviceContact: return nil } } else { return nil } } private var item: ContactsPeerItem? { return self.layoutParams?.0 } required init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = true self.titleNode = TextNode() self.statusNode = TextNode() super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) self.addSubnode(self.titleNode) self.addSubnode(self.statusNode) 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, layoutParams.4) let _ = apply() } }) } override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let (item, _, _, _, _) = self.layoutParams { let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem) self.layoutParams = (item, params, first, last, firstWithHeader) let makeLayout = self.asyncLayout() let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets let _ = nodeApply() } } override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) if highlighted && self.selectionNode == nil { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) } } else { if self.highlightedBackgroundNode.supernode != nil { if animated { 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() } } } } func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let currentItem = self.layoutParams?.0 return { [weak self] item, params, first, last, firstWithHeader in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { updatedTheme = item.theme } var leftInset: CGFloat = 65.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset let updatedSelectionNode: CheckNode? var isSelected = false switch item.selection { case .none: updatedSelectionNode = nil case let .selectable(selected): leftInset += 28.0 isSelected = selected let selectionNode: CheckNode if let current = currentSelectionNode { selectionNode = current updatedSelectionNode = selectionNode } else { selectionNode = CheckNode(strokeColor: item.theme.list.itemCheckColors.strokeColor, fillColor: item.theme.list.itemCheckColors.fillColor, foregroundColor: item.theme.list.itemCheckColors.foregroundColor, style: .plain) selectionNode.isUserInteractionEnabled = false updatedSelectionNode = selectionNode } } var isVerified = false switch item.peer { case let .peer(peer, _): if let peer = peer as? TelegramUser { isVerified = peer.flags.contains(.isVerified) } else if let peer = peer as? TelegramChannel { isVerified = peer.flags.contains(.isVerified) } case .deviceContact: break } var verificationIconImage: UIImage? if isVerified { verificationIconImage = PresentationResourcesChatList.verifiedIcon(item.theme) } var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? var userPresence: TelegramUserPresence? switch item.peer { case let .peer(peer, chatPeer): if let peer = peer { let textColor: UIColor if let _ = chatPeer as? TelegramSecretChat { textColor = item.theme.chatList.secretTitleColor } else { textColor = item.theme.list.itemPrimaryTextColor } if let user = peer as? TelegramUser { if peer.id == item.account.peerId, case .generalSearch = item.peerMode { titleAttributedString = NSAttributedString(string: item.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor) } else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() string.append(NSAttributedString(string: firstName, font: item.sortOrder == .firstLast ? titleBoldFont : titleFont, textColor: textColor)) string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) string.append(NSAttributedString(string: lastName, font: item.sortOrder == .firstLast ? titleFont : titleBoldFont, textColor: textColor)) titleAttributedString = string } else if let firstName = user.firstName, !firstName.isEmpty { titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: textColor) } else if let lastName = user.lastName, !lastName.isEmpty { titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: textColor) } else { titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) } } else if let group = peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else if let channel = peer as? TelegramChannel { titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } switch item.status { case .none: break case let .presence(presence, dateTimeFormat): let presence = (presence as? TelegramUserPresence) ?? TelegramUserPresence(status: .none, lastActivity: 0) userPresence = presence let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) case let .addressName(suffix): if let addressName = peer.addressName { let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemAccentColor) if !suffix.isEmpty { let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) let finalString = NSMutableAttributedString() finalString.append(addressNameString) finalString.append(suffixString) statusAttributedString = finalString } else { statusAttributedString = addressNameString } } else if !suffix.isEmpty { statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } case let .custom(text): statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } } case let .deviceContact(_, contact): let textColor: UIColor = item.theme.list.itemPrimaryTextColor if !contact.firstName.isEmpty, !contact.lastName.isEmpty { let string = NSMutableAttributedString() string.append(NSAttributedString(string: contact.firstName, font: titleFont, textColor: textColor)) string.append(NSAttributedString(string: " ", font: titleFont, textColor: textColor)) string.append(NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor)) titleAttributedString = string } else if !contact.firstName.isEmpty { titleAttributedString = NSAttributedString(string: contact.firstName, font: titleBoldFont, textColor: textColor) } else if !contact.lastName.isEmpty { titleAttributedString = NSAttributedString(string: contact.lastName, font: titleBoldFont, textColor: textColor) } else { titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: textColor) } switch item.status { case let .custom(text): statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) default: break } } var badgeTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? var currentBadgeBackgroundImage: UIImage? if let badge = item.badge { let badgeTextColor: UIColor switch badge.type { case .inactive: currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) badgeTextColor = item.theme.chatList.unreadBadgeInactiveTextColor case .active: currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) badgeTextColor = item.theme.chatList.unreadBadgeActiveTextColor } let badgeAttributedString = NSAttributedString(string: badge.count > 0 ? "\(badge.count)" : " ", font: badgeFont, textColor: badgeTextColor) badgeTextLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) } var badgeSize: CGFloat = 0.0 if let currentBadgeBackgroundImage = currentBadgeBackgroundImage, let (badgeTextLayout, _) = badgeTextLayoutAndApply { badgeSize += max(currentBadgeBackgroundImage.size.width, badgeTextLayout.size.width + 10.0) + 5.0 } var additionalTitleInset: CGFloat = 0.0 if let verificationIconImage = verificationIconImage { additionalTitleInset += 3.0 + verificationIconImage.size.width } additionalTitleInset += badgeSize let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - badgeSize), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let titleFrame: CGRect if statusAttributedString != nil { titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 4.0), size: titleLayout.size) } else { titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 13.0), size: titleLayout.size) } return (nodeLayout, { [weak self] in if let strongSelf = self { switch item.peer { case let .peer(peer, _): if let peer = peer { var overrideImage: AvatarNodeImageOverride? if peer.id == item.account.peerId, case .generalSearch = item.peerMode { overrideImage = .savedMessagesIcon } strongSelf.avatarNode.setPeer(account: item.account, peer: peer, overrideImage: overrideImage) } case let .deviceContact(_, contact): let letters: [String] if !contact.firstName.isEmpty && !contact.lastName.isEmpty { letters = [contact.firstName.substring(to: contact.firstName.index(after: contact.firstName.startIndex)).uppercased(), contact.lastName.substring(to: contact.lastName.index(after: contact.lastName.startIndex)).uppercased()] } else if !contact.firstName.isEmpty { letters = [contact.firstName.substring(to: contact.firstName.index(after: contact.firstName.startIndex)).uppercased()] } else if !contact.lastName.isEmpty { letters = [contact.lastName.substring(to: contact.lastName.index(after: contact.lastName.startIndex)).uppercased()] } else { letters = [" "] } strongSelf.avatarNode.setCustomLetters(letters) } return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in if let strongSelf = strongSelf { strongSelf.layoutParams = (item, params, first, last, firstWithHeader) let transition: ContainedViewLayoutTransition if animated { transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) } else { transition = .immediate } let revealOffset = strongSelf.revealOffset if let _ = updatedTheme { strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor } transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 51.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0))) let _ = titleApply() transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0)) strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4 strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0 let _ = statusApply() let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 25.0), size: statusLayout.size) let previousStatusFrame = strongSelf.statusNode.frame strongSelf.statusNode.frame = statusFrame transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) if let verificationIconImage = verificationIconImage { if strongSelf.verificationIconNode == nil { let verificationIconNode = ASImageNode() verificationIconNode.isLayerBacked = true verificationIconNode.displayWithoutProcessing = true verificationIconNode.displaysAsynchronously = false strongSelf.verificationIconNode = verificationIconNode strongSelf.addSubnode(verificationIconNode) } if let verificationIconNode = strongSelf.verificationIconNode { verificationIconNode.image = verificationIconImage transition.updateFrame(node: verificationIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + titleFrame.maxX + 3.0, y: titleFrame.minY + 3.0 + UIScreenPixel), size: verificationIconImage.size)) } } else if let verificationIconNode = strongSelf.verificationIconNode { strongSelf.verificationIconNode = nil verificationIconNode.removeFromSupernode() } let badgeBackgroundWidth: CGFloat if let currentBadgeBackgroundImage = currentBadgeBackgroundImage, let (badgeTextLayout, badgeTextApply) = badgeTextLayoutAndApply { let badgeBackgroundNode: ASImageNode let badgeTransition: ContainedViewLayoutTransition if let current = strongSelf.badgeBackgroundNode { badgeBackgroundNode = current badgeTransition = transition } else { badgeBackgroundNode = ASImageNode() badgeBackgroundNode.isLayerBacked = true badgeBackgroundNode.displaysAsynchronously = false badgeBackgroundNode.displayWithoutProcessing = true strongSelf.addSubnode(badgeBackgroundNode) strongSelf.badgeBackgroundNode = badgeBackgroundNode badgeTransition = .immediate } badgeBackgroundNode.image = currentBadgeBackgroundImage badgeBackgroundWidth = max(badgeTextLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) let badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeTextLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeTextLayout.size) let badgeTextNode = badgeTextApply() if badgeTextNode !== strongSelf.badgeTextNode { strongSelf.badgeTextNode?.removeFromSupernode() strongSelf.addSubnode(badgeTextNode) strongSelf.badgeTextNode = badgeTextNode } badgeTransition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame) badgeTransition.updateFrame(node: badgeTextNode, frame: badgeTextFrame) } else { badgeBackgroundWidth = 0.0 if let badgeBackgroundNode = strongSelf.badgeBackgroundNode { badgeBackgroundNode.removeFromSupernode() strongSelf.badgeBackgroundNode = nil } if let badgeTextNode = strongSelf.badgeTextNode { badgeTextNode.removeFromSupernode() strongSelf.badgeTextNode = badgeTextNode } } if let updatedSelectionNode = updatedSelectionNode { if strongSelf.selectionNode !== updatedSelectionNode { strongSelf.selectionNode?.removeFromSupernode() strongSelf.selectionNode = updatedSelectionNode strongSelf.addSubnode(updatedSelectionNode) } updatedSelectionNode.setIsChecked(isSelected, animated: animated) updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 6.0, y: floor((nodeLayout.contentSize.height - 32.0) / 2.0)), size: CGSize(width: 32.0, height: 32.0)) } else if let selectionNode = strongSelf.selectionNode { selectionNode.removeFromSupernode() strongSelf.selectionNode = nil } let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight)) strongSelf.separatorNode.isHidden = last if let userPresence = userPresence { strongSelf.peerPresenceManager?.reset(presence: userPresence) } strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.editing.editable { strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } else { strongSelf.setRevealOptions((left: [], right: [])) } } }) } else { return (nil, { _ in }) } }) } } override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) if let item = self.item, let params = self.layoutParams?.1 { var leftInset: CGFloat = 65.0 switch item.selection { case .none: break case .selectable: leftInset += 28.0 } var avatarFrame = self.avatarNode.frame avatarFrame.origin.x = offset + leftInset - 51.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)) if let verificationIconNode = self.verificationIconNode { var iconFrame = verificationIconNode.frame iconFrame.origin.x = offset + titleFrame.maxX + 3.0 transition.updateFrame(node: verificationIconNode, frame: iconFrame) } if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode { var badgeBackgroundFrame = badgeBackgroundNode.frame badgeBackgroundFrame.origin.x = offset + params.width - params.rightInset - badgeBackgroundFrame.width - 6.0 var badgeTextFrame = badgeTextNode.frame badgeTextFrame.origin.x = badgeBackgroundFrame.midX - badgeTextFrame.width / 2.0 transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame) transition.updateFrame(node: badgeTextNode, frame: badgeTextFrame) } } } override func revealOptionsInteractivelyOpened() { if let item = self.item { switch item.peer { case let .peer(peer, chatPeer): if let peer = chatPeer ?? peer { item.setPeerIdWithRevealedOptions?(peer.id, nil) } case .deviceContact: break } } } override func revealOptionsInteractivelyClosed() { if let item = self.item { switch item.peer { case let .peer(peer, chatPeer): if let peer = chatPeer ?? peer { item.setPeerIdWithRevealedOptions?(nil, peer.id) } case .deviceContact: break } } } override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { if let item = self.item { switch item.peer { case let .peer(peer, chatPeer): if let peer = chatPeer ?? peer { item.deletePeer?(peer.id) } case .deviceContact: break } } self.setRevealOptionsOpened(false, animated: true) self.revealOptionsInteractivelyClosed() } override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { let bounds = self.bounds accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } override public func header() -> ListViewItemHeader? { if let (item, _, _, _, _) = self.layoutParams { return item.header } else { return nil } } }