import Foundation import UIKit import AsyncDisplayKit import TelegramCore import SwiftSignalKit import Display import TelegramPresentationData import TelegramUIPreferences import MergeLists import AvatarNode import AccountContext import PeerPresenceStatusManager import AppBundle import SegmentedControlNode import ContextUI private let subtitleFont = Font.regular(12.0) extension CGPoint { func angle(to other: CGPoint) -> CGFloat { let originX = other.x - self.x let originY = other.y - self.y let bearingRadians = atan2f(Float(originY), Float(originX)) return CGFloat(bearingRadians) } func distance(to other: CGPoint) -> CGFloat { return sqrt((self.x - other.x) * (self.x - other.x) + (self.y - other.y) * (self.y - other.y)) } func offsetBy(distance: CGFloat, inDirection radians: CGFloat) -> CGPoint { let vertical = sin(radians) * distance let horizontal = cos(radians) * distance return self.offsetBy(dx: horizontal, dy: vertical) } } private struct SharePeerEntry: Comparable, Identifiable { let index: Int32 let item: ShareControllerPeerGridItem.ShareItem let theme: PresentationTheme let strings: PresentationStrings var stableId: Int64 { switch self.item { case let .peer(peer, _, _, _, _): return peer.peerId.toInt64() case .story: return 0 } } static func ==(lhs: SharePeerEntry, rhs: SharePeerEntry) -> Bool { if lhs.index != rhs.index { return false } if lhs.item != rhs.item { return false } if lhs.theme !== rhs.theme { return false } return true } static func <(lhs: SharePeerEntry, rhs: SharePeerEntry) -> Bool { return lhs.index < rhs.index } func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.item, controllerInteraction: interfaceInteraction, search: false) } } private struct ShareGridTransaction { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] let animated: Bool } private let avatarFont = avatarPlaceholderFont(size: 17.0) private func preparedGridEntryTransition(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, from fromEntries: [SharePeerEntry], to toEntries: [SharePeerEntry], interfaceInteraction: ShareControllerInteraction) -> ShareGridTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(environment: environment, context: context, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(environment: environment, context: context, interfaceInteraction: interfaceInteraction)) } return ShareGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) } final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let environment: ShareControllerEnvironment private let context: ShareControllerAccountContext private var theme: PresentationTheme private let themePromise: Promise private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder private let controllerInteraction: ShareControllerInteraction private let switchToAnotherAccount: () -> Void private let debugAction: () -> Void private let extendedInitialReveal: Bool let accountPeer: EnginePeer private let foundPeers = Promise<[(peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool)]>([]) private let disposable = MetaDisposable() private var entries: [SharePeerEntry] = [] private var enqueuedTransitions: [(ShareGridTransaction, Bool)] = [] let contentGridNode: GridNode private let headerNode: ASDisplayNode private let contentTitleNode: ASTextNode private let contentSubtitleNode: ImmediateTextNode private let contentTitleAccountNode: AvatarNode private let contentSeparatorNode: ASDisplayNode private let searchButtonNode: HighlightableButtonNode private let shareButtonNode: HighlightableButtonNode private let shareReferenceNode: ContextReferenceContentNode private let shareContainerNode: ContextControllerSourceNode private let segmentedNode: SegmentedControlNode private let segmentedValues: [ShareControllerSegmentedValue]? private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? var openSearch: (() -> Void)? var openShare: ((ASDisplayNode, ContextGesture?) -> Void)? var segmentedSelectedIndexUpdated: ((Int) -> Void)? private var ensurePeerVisibleOnLayout: EnginePeer.Id? private var validLayout: (CGSize, CGFloat)? private var overrideGridOffsetTransition: ContainedViewLayoutTransition? let peersValue = Promise<[(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)]>() private var _tick: Int = 0 { didSet { self.tick.set(self._tick) } } private let tick = ValuePromise(0) init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) { self.environment = environment self.context = context self.theme = theme self.themePromise = Promise() self.themePromise.set(.single(theme)) self.strings = strings self.nameDisplayOrder = nameDisplayOrder self.controllerInteraction = controllerInteraction self.accountPeer = accountPeer self.switchToAnotherAccount = switchToAnotherAccount self.debugAction = debugAction self.extendedInitialReveal = extendedInitialReveal self.segmentedValues = segmentedValues self.peersValue.set(.single(peers)) let canShareStory = controllerInteraction.shareStory != nil let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get(), self.tick.get(), self.themePromise.get()) |> map { [weak controllerInteraction] initialPeers, foundPeers, _, theme -> [SharePeerEntry] in var entries: [SharePeerEntry] = [] var index: Int32 = 0 if canShareStory { entries.append(SharePeerEntry(index: index, item: .story(isMessage: fromPublicChannel), theme: theme, strings: strings)) index += 1 } var existingPeerIds: Set = Set() entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false), theme: theme, strings: strings)) existingPeerIds.insert(accountPeer.id) index += 1 for (peer, requiresPremiumForMessaging) in foundPeers.reversed() { if !existingPeerIds.contains(peer.peerId) { entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings)) existingPeerIds.insert(peer.peerId) index += 1 } } for (peer, presence, requiresPremiumForMessaging) in initialPeers { if !existingPeerIds.contains(peer.peerId) { let thread = controllerInteraction?.selectedTopics[peer.peerId] entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1, requiresPremiumForMessaging: requiresPremiumForMessaging), theme: theme, strings: strings)) existingPeerIds.insert(peer.peerId) index += 1 } } return entries } self.contentGridNode = GridNode() self.headerNode = ASDisplayNode() self.contentTitleNode = ASTextNode() self.contentTitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor) self.contentSubtitleNode = ImmediateTextNode() self.contentSubtitleNode.maximumNumberOfLines = 1 self.contentSubtitleNode.isUserInteractionEnabled = false self.contentSubtitleNode.displaysAsynchronously = false self.contentSubtitleNode.truncationMode = .byTruncatingTail self.contentSubtitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_SelectChats, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor) self.contentTitleAccountNode = AvatarNode(font: avatarFont) var hasOtherAccounts = false if switchableAccounts.count > 1, let info = switchableAccounts.first(where: { $0.account.accountId == context.accountId }) { hasOtherAccounts = true self.contentTitleAccountNode.setPeer( accountPeerId: context.accountPeerId, postbox: context.stateManager.postbox, network: context.stateManager.network, contentSettings: context.contentSettings, theme: theme, peer: EnginePeer(info.peer), emptyColor: nil, synchronousLoad: false ) } else { self.contentTitleAccountNode.isHidden = true } self.searchButtonNode = HighlightableButtonNode() self.searchButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/SearchIcon"), color: self.theme.actionSheet.controlAccentColor), for: []) self.shareButtonNode = HighlightableButtonNode() self.shareButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Share/ShareIcon"), color: self.theme.actionSheet.controlAccentColor), for: []) self.shareReferenceNode = ContextReferenceContentNode() self.shareContainerNode = ContextControllerSourceNode() self.shareContainerNode.animateScale = false let segmentedItems: [SegmentedControlItem] if let segmentedValues = segmentedValues { segmentedItems = segmentedValues.map { SegmentedControlItem(title: $0.title) } } else { segmentedItems = [] } self.segmentedNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segmentedItems, selectedIndex: 0) self.segmentedNode.isHidden = segmentedValues == nil self.contentTitleNode.isHidden = self.segmentedValues != nil self.contentSubtitleNode.isHidden = self.segmentedValues != nil self.contentSeparatorNode = ASDisplayNode() self.contentSeparatorNode.isLayerBacked = true self.contentSeparatorNode.displaysAsynchronously = false self.contentSeparatorNode.backgroundColor = self.theme.actionSheet.opaqueItemSeparatorColor if !externalShare || hasOtherAccounts { self.shareButtonNode.isHidden = true } super.init() self.addSubnode(self.contentGridNode) self.addSubnode(self.headerNode) self.headerNode.addSubnode(self.contentTitleNode) self.headerNode.addSubnode(self.contentSubtitleNode) self.headerNode.addSubnode(self.contentTitleAccountNode) self.headerNode.addSubnode(self.segmentedNode) self.headerNode.addSubnode(self.searchButtonNode) self.shareContainerNode.addSubnode(self.shareReferenceNode) self.shareButtonNode.addSubnode(self.shareContainerNode) self.headerNode.addSubnode(self.shareButtonNode) self.addSubnode(self.contentSeparatorNode) self.shareContainerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self else { return } strongSelf.openShare?(strongSelf.shareReferenceNode, gesture) } let previousItems = Atomic<[SharePeerEntry]?>(value: []) self.disposable.set((items |> deliverOnMainQueue).start(next: { [weak self] entries in if let strongSelf = self { let previousEntries = previousItems.swap(entries) strongSelf.entries = entries let firstTime = previousEntries == nil let transition = preparedGridEntryTransition(environment: environment, context: context, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) self.contentGridNode.scrollingInitiated = { [weak self] in self?.contentDidBeginDragging?() } self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) } self.searchButtonNode.addTarget(self, action: #selector(self.searchPressed), forControlEvents: .touchUpInside) self.shareButtonNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) self.contentTitleAccountNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.accountTapGesture(_:)))) self.segmentedNode.selectedIndexChanged = { [weak self] index in self?.segmentedSelectedIndexUpdated?(index) } self.contentTitleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTapGesture(_:)))) } deinit { self.disposable.dispose() } func updateTheme(_ theme: PresentationTheme) { self.theme = theme self.themePromise.set(.single(theme)) self.contentTitleNode.attributedText = NSAttributedString(string: self.strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor) self.updateSelectedPeers(animated: false) } private func enqueueTransition(_ transition: ShareGridTransaction, firstTime: Bool) { self.enqueuedTransitions.append((transition, firstTime)) if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func dequeueTransition() { if let (transition, _) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) var itemTransition: ContainedViewLayoutTransition = .immediate if transition.animated { itemTransition = .animated(duration: 0.3, curve: .spring) } self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) } } func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { self.ensurePeerVisibleOnLayout = peerId } func setDidBeginDragging(_ f: (() -> Void)?) { self.contentDidBeginDragging = f } func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) { let itemCount = self.entries.count let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) let minimalItemWidth: CGFloat = size.width > 301.0 ? 70.0 : 60.0 let effectiveWidth = size.width - itemInsets.left - itemInsets.right let itemsPerRow = Int(effectiveWidth / minimalItemWidth) let itemWidth = floor(effectiveWidth / CGFloat(itemsPerRow)) var rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) rowCount = max(rowCount, 4) let minimallyRevealedRowCount: CGFloat if self.extendedInitialReveal { minimallyRevealedRowCount = 4.6 } else { minimallyRevealedRowCount = 3.7 } let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset) return (gridTopInset, itemWidth) } func activate() { } func deactivate() { } func frameForPeerId(_ peerId: EnginePeer.Id) -> CGRect? { var node: ASDisplayNode? self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { node = itemNode } } if let node = node { return node.frame.offsetBy(dx: 0.0, dy: -10.0) } else { return nil } } func prepareForAnimateIn() { self.searchButtonNode.alpha = 0.0 self.shareButtonNode.alpha = 0.0 self.contentTitleNode.alpha = 0.0 self.contentSubtitleNode.alpha = 0.0 self.contentGridNode.alpha = 0.0 } func animateIn(peerId: EnginePeer.Id, scrollDelta: CGFloat) -> CGRect? { self.headerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.searchButtonNode.alpha = 1.0 self.searchButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.searchButtonNode.layer.animatePosition(from: CGPoint(x: -20.0, y: 0.0), to: .zero, duration: 0.2, additive: true) self.shareButtonNode.alpha = 1.0 self.shareButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.shareButtonNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: .zero, duration: 0.2, additive: true) self.contentTitleNode.alpha = 1.0 self.contentTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.contentTitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -10.0), to: .zero, duration: 0.2, additive: true) self.contentTitleNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.2) self.contentSubtitleNode.alpha = 1.0 self.contentSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.contentSubtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -10.0), to: .zero, duration: 0.2, additive: true) self.contentSubtitleNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.2) self.contentGridNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let targetFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout { let clippedNode = ASDisplayNode() clippedNode.clipsToBounds = true clippedNode.cornerRadius = 16.0 clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.headerNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset + 15.0)) self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view) clippedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) let maskView = UIView() maskView.frame = clippedNode.bounds let maskImageView = UIImageView() maskImageView.image = generatePeersMaskImage() maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0) maskView.addSubview(maskImageView) clippedNode.view.mask = maskView self.contentGridNode.alpha = 1.0 self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) itemNode.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak clippedNode] _ in clippedNode?.view.removeFromSuperview() }) } else if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view) clippedNode.view.addSubview(snapshotView) itemNode.alpha = 0.0 let angle = targetFrame.center.angle(to: itemNode.position) let distance = targetFrame.center.distance(to: itemNode.position) let newDistance = distance * 2.8 let newPosition = snapshotView.center.offsetBy(distance: newDistance, inDirection: angle) snapshotView.layer.animatePosition(from: newPosition, to: snapshotView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) snapshotView.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak itemNode] _ in itemNode?.alpha = 1.0 }) snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false) } } return targetFrame } else { return nil } } func animateOut(peerId: EnginePeer.Id, scrollDelta: CGFloat) -> CGRect? { self.headerNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.searchButtonNode.alpha = 0.0 self.searchButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.searchButtonNode.layer.animatePosition(from: .zero, to: CGPoint(x: -20.0, y: 0.0), duration: 0.2, additive: true) self.shareButtonNode.alpha = 0.0 self.shareButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.shareButtonNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 0.0), duration: 0.2, additive: true) self.contentTitleNode.alpha = 0.0 self.contentTitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.contentTitleNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, additive: true) self.contentTitleNode.layer.animateScale(from: 1.0, to: 0.85, duration: 0.3) self.contentSubtitleNode.alpha = 0.0 self.contentSubtitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.contentSubtitleNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, additive: true) self.contentSubtitleNode.layer.animateScale(from: 1.0, to: 0.85, duration: 0.3) self.contentGridNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let sourceFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout { let clippedNode = ASDisplayNode() clippedNode.clipsToBounds = true clippedNode.cornerRadius = 16.0 clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.headerNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset + 15.0)) self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view) clippedNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) let maskView = UIView() maskView.frame = clippedNode.bounds let maskImageView = UIImageView() maskImageView.image = generatePeersMaskImage() maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0) maskView.addSubview(maskImageView) clippedNode.view.mask = maskView self.contentGridNode.forEachItemNode { itemNode in if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view) clippedNode.view.addSubview(snapshotView) if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { } else { let angle = sourceFrame.center.angle(to: itemNode.position) let distance = sourceFrame.center.distance(to: itemNode.position) let newDistance = distance * 2.8 let newPosition = snapshotView.center.offsetBy(distance: newDistance, inDirection: angle) snapshotView.layer.animatePosition(from: snapshotView.center, to: newPosition, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } snapshotView.layer.animateScale(from: 1.0, to: 1.35, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } } clippedNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak clippedNode] _ in clippedNode?.view.removeFromSuperview() }) self.contentGridNode.alpha = 0.0 return sourceFrame } else { return nil } } func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { let firstLayout = self.validLayout == nil self.validLayout = (size, bottomInset) let gridLayoutTransition: ContainedViewLayoutTransition if firstLayout { gridLayoutTransition = .immediate self.overrideGridOffsetTransition = transition } else { gridLayoutTransition = transition self.overrideGridOffsetTransition = nil } let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset) var scrollToItem: GridNodeScrollToItem? if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { self.ensurePeerVisibleOnLayout = nil if let index = self.entries.firstIndex(where: { $0.item.peerId == ensurePeerVisibleOnLayout }) { scrollToItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false) } } let gridSize = CGSize(width: size.width - 10.0, height: size.height) self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: gridLayoutTransition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) gridLayoutTransition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 0.0), size: gridSize)) if firstLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { guard let (size, _) = self.validLayout else { return } let actualTransition = self.overrideGridOffsetTransition ?? transition self.overrideGridOffsetTransition = nil let titleAreaHeight: CGFloat = 64.0 let rawTitleOffset = -titleAreaHeight - presentationLayout.contentOffset.y let titleOffset = max(-titleAreaHeight, rawTitleOffset) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: titleOffset), size: CGSize(width: size.width, height: 64.0)) transition.updateFrame(node: self.headerNode, frame: headerFrame) let titleSize = self.contentTitleNode.measure(size) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: 15.0), size: titleSize) transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) let subtitleSize = self.contentSubtitleNode.updateLayout(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0, height: titleAreaHeight)) let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: 40.0), size: subtitleSize) var originalSubtitleFrame = self.contentSubtitleNode.frame originalSubtitleFrame.origin.x = subtitleFrame.origin.x originalSubtitleFrame.size = subtitleFrame.size self.contentSubtitleNode.frame = originalSubtitleFrame transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame) let titleButtonSize = CGSize(width: 44.0, height: 44.0) let searchButtonFrame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: titleButtonSize) transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame) let shareButtonFrame = CGRect(origin: CGPoint(x: size.width - titleButtonSize.width - 12.0, y: 12.0), size: titleButtonSize) transition.updateFrame(node: self.shareButtonNode, frame: shareButtonFrame) transition.updateFrame(node: self.shareContainerNode, frame: CGRect(origin: CGPoint(), size: titleButtonSize)) transition.updateFrame(node: self.shareReferenceNode, frame: CGRect(origin: CGPoint(), size: titleButtonSize)) let segmentedSize = self.segmentedNode.updateLayout(.sizeToFit(maximumWidth: size.width - titleButtonSize.width * 2.0, minimumWidth: 160.0, height: 32.0), transition: transition) transition.updateFrame(node: self.segmentedNode, frame: CGRect(origin: CGPoint(x: floor((size.width - segmentedSize.width) / 2.0), y: 18.0), size: segmentedSize)) let avatarButtonSize = CGSize(width: 36.0, height: 36.0) let avatarButtonFrame = CGRect(origin: CGPoint(x: size.width - avatarButtonSize.width - 20.0, y: 15.0), size: avatarButtonSize) transition.updateFrame(node: self.contentTitleAccountNode, frame: avatarButtonFrame) transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleOffset + titleAreaHeight), size: CGSize(width: size.width, height: UIScreenPixel))) if rawTitleOffset.isLess(than: -titleAreaHeight) { self.contentSeparatorNode.alpha = 1.0 } else { self.contentSeparatorNode.alpha = 0.0 } self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition) } func updateVisibleItemsSelection(animated: Bool) { self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode { itemNode.updateSelection(animated: animated) } } } func updateFoundPeers() { self.foundPeers.set(.single(self.controllerInteraction.foundPeers)) } func update() { self._tick += 1 } func updateSelectedPeers(animated: Bool = true) { if self.segmentedValues != nil { self.contentTitleNode.isHidden = true self.contentSubtitleNode.isHidden = true } else { self.contentTitleNode.isHidden = false self.contentSubtitleNode.isHidden = false var subtitleText = self.strings.ShareMenu_SelectChats if !self.controllerInteraction.selectedPeers.isEmpty { subtitleText = self.controllerInteraction.selectedPeers.reduce("", { string, peer in let text: String if peer.peerId == self.accountPeer.id { text = self.strings.DialogList_SavedMessages } else { text = peer.chatMainPeer?.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder) ?? "" } if !string.isEmpty { return string + ", " + text } else { return string + text } }) } self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor) } self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode { itemNode.updateSelection(animated: animated) } } } @objc func searchPressed() { self.openSearch?() } @objc func sharePressed() { self.openShare?(self.shareReferenceNode, nil) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode, self.contentTitleAccountNode] for node in nodes { let nodeFrame = node.frame if node.isHidden { continue } if let result = node.hitTest(point.offsetBy(dx: -self.headerNode.frame.minX, dy: -self.headerNode.frame.minY).offsetBy(dx: -nodeFrame.minX, dy: -nodeFrame.minY), with: event) { return result } } return super.hitTest(point, with: event) } @objc private func accountTapGesture(_ recognizer: UITapGestureRecognizer) { self.switchToAnotherAccount() } private var debugTapCounter: (Double, Int) = (0.0, 0) @objc private func debugTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let timestamp = CACurrentMediaTime() if self.debugTapCounter.0 < timestamp - 0.4 { self.debugTapCounter.0 = timestamp self.debugTapCounter.1 = 0 } if self.debugTapCounter.0 >= timestamp - 0.4 { self.debugTapCounter.0 = timestamp self.debugTapCounter.1 += 1 } if self.debugTapCounter.1 >= 10 { self.debugTapCounter.1 = 0 self.debugAction() } } } } func generatePeersMaskImage() -> UIImage? { return generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 16.0, dy: 16.0), cornerRadius: 16.0) context.setFillColor(UIColor.white.cgColor) context.setShadow(offset: .zero, blur: 40.0, color: UIColor.white.cgColor) for _ in 0 ..< 10 { context.addPath(path.cgPath) context.fillPath() } })?.stretchableImage(withLeftCapWidth: 49, topCapHeight: 49) }