import Display import UIKit import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import MergeLists import AccountContext import ContactListUI import ChatListUI import AnimationCache import MultiAnimationRenderer import EditableTokenListNode import SolidRoundedButtonNode import ContextUI import ComponentFlow import MultilineTextComponent import CheckComponent private struct SearchResultEntry: Identifiable { let index: Int let peer: Peer var stableId: Int64 { return self.peer.id.toInt64() } static func ==(lhs: SearchResultEntry, rhs: SearchResultEntry) -> Bool { return lhs.index == rhs.index && lhs.peer.isEqual(rhs.peer) } static func <(lhs: SearchResultEntry, rhs: SearchResultEntry) -> Bool { return lhs.index < rhs.index } } enum ContactMultiselectionContentNode { case contacts(ContactListNode) case chats(ChatListNode) var node: ASDisplayNode { switch self { case let .contacts(contacts): return contacts case let .chats(chats): return chats } } } final class ContactMultiselectionControllerNode: ASDisplayNode { private let navigationBar: NavigationBar? let contentNode: ContactMultiselectionContentNode let tokenListNode: EditableTokenListNode var searchResultsNode: ContactListNode? private let context: AccountContext private let mode: ContactMultiselectionControllerMode private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeerId) -> Void)? var openPeer: ((ContactListPeer) -> Void)? var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? var openDisabledPeer: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? var removeSelectedPeer: ((ContactListPeerId) -> Void)? var removeSelectedCategory: ((Int) -> Void)? var additionalCategorySelected: ((Int) -> Void)? var complete: (() -> Void)? var editableTokens: [EditableTokenListToken] = [] private let searchResultsReadyDisposable = MetaDisposable() var dismiss: (() -> Void)? private var presentationData: PresentationData private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer private let footerPanelNode: FooterPanelNode? private let isPeerEnabled: ((EnginePeer) -> Bool)? private let onlyWriteable: Bool private let isGroupInvitation: Bool var isCallVideoOptionSelected: Bool { return self.footerPanelNode?.isCheckOptionSelected ?? false } init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { self.navigationBar = navigationBar self.context = context self.presentationData = presentationData self.mode = mode self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer self.isPeerEnabled = isPeerEnabled self.onlyWriteable = onlyWriteable self.isGroupInvitation = isGroupInvitation var proceedImpl: (() -> Void)? var placeholder: String var shortPlaceholder: String? var includeChatList = false switch mode { case let .peerSelection(_, searchGroups, searchChannels): includeChatList = searchGroups || searchChannels if searchGroups { placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel } else { placeholder = self.presentationData.strings.Contacts_SearchLabel } self.footerPanelNode = nil case .premiumGifting: placeholder = self.presentationData.strings.Premium_Gift_ContactSelection_Placeholder shortPlaceholder = self.presentationData.strings.Common_Search self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() }, checkOptionTitle: nil) case .requestedUsersSelection: placeholder = self.presentationData.strings.RequestPeer_SelectUsers_SearchPlaceholder self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() }, checkOptionTitle: nil) case let .groupCreation(isCall): if isCall { placeholder = self.presentationData.strings.NewCall_SearchPlaceholder self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { proceedImpl?() }, checkOptionTitle: self.presentationData.strings.NewCall_VideoOption) } else { placeholder = self.presentationData.strings.Compose_TokenListPlaceholder self.footerPanelNode = nil } default: placeholder = self.presentationData.strings.Compose_TokenListPlaceholder self.footerPanelNode = nil } if case let .chatSelection(chatSelection) = mode { let placeholderValue = chatSelection.searchPlaceholder let selectedChats = chatSelection.selectedChats let additionalCategories = chatSelection.additionalCategories let chatListFilters = chatSelection.chatListFilters var chatListFilter: ChatListFilter? if chatSelection.onlyUsers { chatListFilter = .filter(id: Int32.max, title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), emoticon: nil, data: ChatListFilterData( isShared: false, hasSharedLinks: false, categories: [.contacts, .nonContacts], excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: ChatListFilterIncludePeers(), excludePeers: [], color: nil )) } else if chatSelection.disableChannels || chatSelection.disableBots { var categories: ChatListFilterPeerCategories = [.contacts, .nonContacts, .groups, .bots, .channels] if chatSelection.disableContacts { categories.remove(.contacts) } if chatSelection.disableChannels { categories.remove(.channels) } if chatSelection.disableChannels { categories.remove(.bots) } chatListFilter = .filter(id: Int32.max, title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), emoticon: nil, data: ChatListFilterData( isShared: false, hasSharedLinks: false, categories: categories, excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: ChatListFilterIncludePeers(), excludePeers: [], color: nil )) } placeholder = placeholderValue let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) chatListNode.passthroughPeerSelection = true chatListNode.disabledPeerSelected = { peer, _, reason in attemptDisabledItemSelection?(peer, reason) } if let limit = limit { chatListNode.selectionLimit = limit chatListNode.reachedSelectionLimit = reachedSelectionLimit } chatListNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } chatListNode.updateState { state in var state = state for peerId in selectedChats { state.selectedPeerIds.insert(peerId) } if let additionalCategories = additionalCategories { for id in additionalCategories.selectedCategories { state.selectedAdditionalCategoryIds.insert(id) } } return state } self.contentNode = .chats(chatListNode) } else { let displayTopPeers: ContactListPresentation.TopPeers var selectedPeers: [EnginePeer.Id] = [] if case let .premiumGifting(birthdays, selectToday, hasActions) = mode { if let birthdays { let today = Calendar(identifier: .gregorian).component(.day, from: Date()) var sections: [(String, [EnginePeer.Id], Bool)] = [] var todayPeers: [EnginePeer.Id] = [] var yesterdayPeers: [EnginePeer.Id] = [] var tomorrowPeers: [EnginePeer.Id] = [] for (peerId, birthday) in birthdays { if birthday.day == today { todayPeers.append(peerId) if selectToday { selectedPeers.append(peerId) } } else if birthday.day == today - 1 || birthday.day > today + 5 { yesterdayPeers.append(peerId) } else if birthday.day == today + 1 || birthday.day < today + 5 { tomorrowPeers.append(peerId) } } if !todayPeers.isEmpty { sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers, hasActions)) } if !yesterdayPeers.isEmpty { sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers, hasActions)) } if !tomorrowPeers.isEmpty { sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) } displayTopPeers = .custom(showSelf: false, selfSubtitle: nil, sections: sections) } else { displayTopPeers = .recent } } else if case .requestedUsersSelection = mode { displayTopPeers = .recent } else { displayTopPeers = .none } let presentation: Signal = options |> map { options in return .natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers) } let contactListNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, isPeerEnabled: isPeerEnabled, selectionState: ContactListNodeGroupSelectionState()) self.contentNode = .contacts(contactListNode) if !selectedPeers.isEmpty { contactListNode.updateSelectionState { state in var state = state ?? ContactListNodeGroupSelectionState() for peerId in selectedPeers { state = state.withToggledPeerId(.peer(peerId)) } return state } } } self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder, shortPlaceholder: shortPlaceholder) super.init() self.setViewBlock({ return UITracingLayerView() }) self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.contentNode.node) self.navigationBar?.additionalContentNode.addSubnode(self.tokenListNode) switch self.contentNode { case let .contacts(contactsNode): contactsNode.openPeer = { [weak self] peer, action, sourceNode, gesture in if case .more = action { self?.openPeerMore?(peer, sourceNode, gesture) } else { self?.openPeer?(peer) } } contactsNode.openDisabledPeer = { [weak self] peer, reason in guard let self else { return } self.openDisabledPeer?(peer, reason) } contactsNode.suppressPermissionWarning = { [weak self] in if let strongSelf = self { strongSelf.context.sharedContext.presentContactsWarningSuppression(context: strongSelf.context, present: { c, a in present(c, a) }) } } case let .chats(chatsNode): chatsNode.peerSelected = { [weak self] peer, _, _, _, _ in self?.openPeer?(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) } chatsNode.additionalCategorySelected = { [weak self] id in guard let strongSelf = self else { return } strongSelf.additionalCategorySelected?(id) } } let searchText = ValuePromise() self.tokenListNode.deleteToken = { [weak self] id in if let id = id as? PeerId { self?.removeSelectedPeer?(ContactListPeerId.peer(id)) } else if let id = id as? Int { self?.removeSelectedCategory?(id) } } self.tokenListNode.textUpdated = { [weak self] text in if let strongSelf = self { searchText.set(text) if text.isEmpty { if let searchResultsNode = strongSelf.searchResultsNode { searchResultsNode.removeFromSupernode() strongSelf.searchResultsNode = nil } } else { if strongSelf.searchResultsNode == nil { var selectionState: ContactListNodeGroupSelectionState? switch strongSelf.contentNode { case let .contacts(contactsNode): contactsNode.updateSelectionState { state in selectionState = state return state } case let .chats(chatsNode): selectionState = ContactListNodeGroupSelectionState() for peerId in chatsNode.currentState.selectedPeerIds { selectionState = selectionState?.withToggledPeerId(.peer(peerId)) } } var searchChatList = false var searchGroups = false var searchChannels = false var globalSearch = false var displaySavedMessages = true var filters = filters switch mode { case .groupCreation, .channelCreation: globalSearch = true case let .peerSelection(searchChatListValue, searchGroupsValue, searchChannelsValue): searchChatList = searchChatListValue searchGroups = searchGroupsValue searchChannels = searchChannelsValue globalSearch = true case let .chatSelection(chatSelection): if chatSelection.onlyUsers { searchChatList = true searchGroups = false searchChannels = false displaySavedMessages = false filters.append(.excludeSelf) } else { searchChatList = true searchGroups = true searchChannels = !chatSelection.disableChannels } globalSearch = false case .premiumGifting, .requestedUsersSelection: searchChatList = true } let searchResultsNode = ContactListNode(context: context, updatedPresentationData: updatedPresentationData, presentation: .single(.search(ContactListPresentation.Search( signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels, globalSearch: globalSearch, displaySavedMessages: displaySavedMessages ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) searchResultsNode.openPeer = { peer, _, _, _ in self?.tokenListNode.setText("") self?.openPeer?(peer) } searchResultsNode.openDisabledPeer = { peer, reason in guard let self else { return } self.openDisabledPeer?(peer, reason) } strongSelf.searchResultsNode = searchResultsNode searchResultsNode.enableUpdates = true searchResultsNode.backgroundColor = strongSelf.presentationData.theme.chatList.backgroundColor if let (layout, navigationBarHeight, actualNavigationBarHeight) = strongSelf.containerLayout { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight insets.top += strongSelf.tokenListNode.bounds.size.height var headerInsets = layout.insets(options: [.input]) headerInsets.top += actualNavigationBarHeight headerInsets.top += strongSelf.tokenListNode.bounds.size.height searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: .immediate) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } strongSelf.searchResultsReadyDisposable.set((searchResultsNode.ready |> deliverOnMainQueue).startStrict(next: { _ in if let strongSelf = self, let searchResultsNode = strongSelf.searchResultsNode { strongSelf.insertSubnode(searchResultsNode, aboveSubnode: strongSelf.contentNode.node) } })) } } } } self.tokenListNode.textReturned = { [weak self] in self?.complete?() } if let footerPanelNode = self.footerPanelNode { proceedImpl = { [weak self] in self?.complete?() } self.addSubnode(footerPanelNode) } } deinit { self.searchResultsReadyDisposable.dispose() } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.backgroundColor = presentationData.theme.chatList.backgroundColor } func scrollToTop() { switch self.contentNode { case let .contacts(contactsNode): contactsNode.scrollToTop() case let .chats(chatsNode): chatsNode.scrollToPosition(.top(adjustForTempInset: false)) } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight let tokenListHeight = self.tokenListNode.updateLayout(tokens: self.editableTokens, width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) transition.updateFrame(node: self.tokenListNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: tokenListHeight))) var headerInsets = layout.insets(options: [.input]) headerInsets.top += actualNavigationBarHeight insets.top += tokenListHeight headerInsets.top += tokenListHeight if let footerPanelNode = self.footerPanelNode { var count = 0 if case let .contacts(contactListNode) = self.contentNode { count = contactListNode.selectionState?.selectedPeerIndices.count ?? 0 } if case let .groupCreation(isCall) = self.mode, isCall { if count == 0 { // Don't set anything to prevent state update } else if count <= 1 { let callTitle: String if case let .contacts(contactListNode) = self.contentNode, let peer = contactListNode.selectedPeers.first, case let .peer(peer, _, _) = peer { callTitle = self.presentationData.strings.NewCall_ActionCallSingle(EnginePeer(peer).compactDisplayTitle).string } else { callTitle = self.presentationData.strings.NewCall_ActionCallMultiple } footerPanelNode.content = FooterPanelNode.Content(title: callTitle, badge: "") } else { footerPanelNode.content = FooterPanelNode.Content(title: "Call", badge: "\(count)") } } else { footerPanelNode.content = FooterPanelNode.Content(title: self.presentationData.strings.Premium_Gift_ContactSelection_Proceed, badge: count == 0 ? "" : "\(count)") } let panelHeight = footerPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: headerInsets.bottom, transition: transition) if count == 0 { transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight))) } else { insets.bottom += panelHeight transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) } } switch self.contentNode { case let .contacts(contactsNode): contactsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) case let .chats(chatsNode): var combinedInsets = insets combinedInsets.left += layout.safeInsets.left combinedInsets.right += layout.safeInsets.right let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: combinedInsets, headerInsets: headerInsets, duration: duration, curve: curve) chatsNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: updateSizeAndInsets.insets.top, originalTopInset: updateSizeAndInsets.insets.top, storiesInset: 0.0, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0) } self.contentNode.node.frame = CGRect(origin: CGPoint(), size: layout.size) if let searchResultsNode = self.searchResultsNode { searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } return tokenListHeight } func animateIn() { self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } func animateOut(completion: (() -> Void)?) { self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { [weak self] _ in if let strongSelf = self { strongSelf.dismiss?() completion?() } }) } } private final class FooterPanelNode: ASDisplayNode { struct Content: Equatable { let title: String let badge: String init(title: String, badge: String) { self.title = title self.badge = badge } } private let theme: PresentationTheme private let strings: PresentationStrings private let checkOptionTitle: String? private var checkOptionButton: HighlightTrackingButton? private var checkOptionText: ComponentView? private var checkOptionControl: ComponentView? private let separatorNode: ASDisplayNode private let button: SolidRoundedButtonView private(set) var isCheckOptionSelected: Bool = false private var validLayout: (CGFloat, CGFloat, CGFloat)? var content: Content { didSet { if self.content != oldValue { self.button.title = content.title self.button.badge = content.badge.isEmpty ? nil : content.badge if let (width, sideInset, bottomInset) = self.validLayout { let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) } } } } init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, checkOptionTitle: String?) { self.theme = theme self.strings = strings self.checkOptionTitle = checkOptionTitle self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 10.0) self.content = Content(title: self.strings.Premium_Gift_ContactSelection_Proceed, badge: "") super.init() self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor self.addSubnode(self.separatorNode) self.button.pressed = { action() } } override func didLoad() { super.didLoad() self.view.addSubview(self.button) } @objc private func checkOptionButtonPressed() { self.isCheckOptionSelected = !self.isCheckOptionSelected if let validLayout = self.validLayout { let _ = self.updateLayout(width: validLayout.0, sideInset: validLayout.1, bottomInset: validLayout.2, transition: .animated(duration: 0.2, curve: .easeInOut)) } } func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, sideInset, bottomInset) let topInset: CGFloat = 9.0 var bottomInset = bottomInset bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) let buttonInset: CGFloat = 16.0 + sideInset let buttonWidth = width - buttonInset * 2.0 let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) var height = topInset + buttonHeight + bottomInset var buttonOffset: CGFloat = 0.0 if let checkOptionTitle = self.checkOptionTitle { let checkSpacing: CGFloat = 10.0 let checkOptionButton: HighlightTrackingButton if let current = self.checkOptionButton { checkOptionButton = current } else { checkOptionButton = HighlightTrackingButton() self.checkOptionButton = checkOptionButton self.view.addSubview(checkOptionButton) checkOptionButton.addTarget(self, action: #selector(self.checkOptionButtonPressed), for: .touchUpInside) } let checkOptionText: ComponentView if let current = self.checkOptionText { checkOptionText = current } else { checkOptionText = ComponentView() self.checkOptionText = checkOptionText } let checkOptionControl: ComponentView if let current = self.checkOptionControl { checkOptionControl = current } else { checkOptionControl = ComponentView() self.checkOptionControl = checkOptionControl } let checkOptionTextSize = checkOptionText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: checkOptionTitle, font: Font.regular(13.0), textColor: theme.rootController.navigationBar.primaryTextColor)) )), environment: {}, containerSize: CGSize(width: width - sideInset * 2.0 - checkSpacing - 20.0, height: 100.0) ) let checkTheme = CheckComponent.Theme( backgroundColor: self.theme.list.itemCheckColors.fillColor, strokeColor: self.theme.list.itemCheckColors.foregroundColor, borderColor: self.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false ) let checkOptionControlSize = checkOptionControl.update( transition: transition.isAnimated ? .easeInOut(duration: 0.2) : .immediate, component: AnyComponent(CheckComponent( theme: checkTheme, size: CGSize(width: 18.0, height: 18.0), selected: self.isCheckOptionSelected )), environment: {}, containerSize: CGSize(width: 18.0, height: 18.0) ) let checkContentWidth = checkOptionControlSize.width + checkSpacing + checkOptionTextSize.width let checkContentHeight = 49.0 let checkOptionControlFrame = CGRect(origin: CGPoint(x: floor((width - checkContentWidth) * 0.5), y: floor(checkContentHeight - checkOptionControlSize.height) * 0.5), size: checkOptionControlSize) let checkOptionTextFrame = CGRect(origin: CGPoint(x: checkOptionControlFrame.maxX + checkSpacing, y: floor((checkContentHeight - checkOptionTextSize.height) * 0.5)), size: checkOptionTextSize) if let checkOptionControlView = checkOptionControl.view { if checkOptionControlView.superview == nil { checkOptionControlView.isUserInteractionEnabled = false checkOptionButton.addSubview(checkOptionControlView) } checkOptionControlView.frame = checkOptionControlFrame } if let checkOptionTextView = checkOptionText.view { if checkOptionTextView.superview == nil { checkOptionTextView.isUserInteractionEnabled = false checkOptionButton.addSubview(checkOptionTextView) } checkOptionTextView.frame = checkOptionTextFrame } checkOptionButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: checkContentHeight)) height += checkContentHeight buttonOffset += checkContentHeight } else { if let checkOptionButton = self.checkOptionButton { self.checkOptionButton = nil checkOptionButton.removeFromSuperview() } } transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset + buttonOffset, width: buttonWidth, height: buttonHeight)) return height } }