Swiftgram/TelegramUI/SharePeersContainerNode.swift
2018-11-21 01:04:04 +04:00

370 lines
17 KiB
Swift

import Foundation
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
private let subtitleFont = Font.regular(12.0)
private struct SharePeerEntry: Comparable, Identifiable {
let index: Int32
let peer: RenderedPeer
let theme: PresentationTheme
let strings: PresentationStrings
var stableId: Int64 {
return self.peer.peerId.toInt64()
}
static func ==(lhs: SharePeerEntry, rhs: SharePeerEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
static func <(lhs: SharePeerEntry, rhs: SharePeerEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, interfaceInteraction: ShareControllerInteraction) -> GridItem {
return ShareControllerPeerGridItem(account: account, theme: self.theme, strings: self.strings, peer: self.peer, controllerInteraction: interfaceInteraction, search: false)
}
}
private struct ShareGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let animated: Bool
}
private func preparedGridEntryTransition(account: Account, 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(account: account, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) }
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction)) }
return ShareGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false)
}
final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
private let account: Account
private let theme: PresentationTheme
private let strings: PresentationStrings
private let controllerInteraction: ShareControllerInteraction
private let accountPeer: Peer
private let foundPeers = Promise<[RenderedPeer]>([])
private let disposable = MetaDisposable()
private var entries: [SharePeerEntry] = []
private var enqueuedTransitions: [(ShareGridTransaction, Bool)] = []
private let contentGridNode: GridNode
private let contentTitleNode: ASTextNode
private let contentSubtitleNode: ASTextNode
private let contentSeparatorNode: ASDisplayNode
private let searchButtonNode: HighlightableButtonNode
private let shareButtonNode: HighlightableButtonNode
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var openSearch: (() -> Void)?
var openShare: (() -> Void)?
private var ensurePeerVisibleOnLayout: PeerId?
private var validLayout: (CGSize, CGFloat)?
private var overrideGridOffsetTransition: ContainedViewLayoutTransition?
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peers: [RenderedPeer], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool) {
self.account = account
self.theme = theme
self.strings = strings
self.controllerInteraction = controllerInteraction
self.accountPeer = accountPeer
let items: Signal<[SharePeerEntry], NoError> = combineLatest(.single(peers), foundPeers.get())
|> map { initialPeers, foundPeers -> [SharePeerEntry] in
var entries: [SharePeerEntry] = []
var index: Int32 = 0
var existingPeerIds: Set<PeerId> = Set()
entries.append(SharePeerEntry(index: index, peer: RenderedPeer(peer: accountPeer), theme: theme, strings: strings))
index += 1
for peer in foundPeers.reversed() {
entries.append(SharePeerEntry(index: index, peer: peer, theme: theme, strings: strings))
existingPeerIds.insert(peer.peerId)
index += 1
}
for peer in initialPeers {
if !existingPeerIds.contains(peer.peerId) {
entries.append(SharePeerEntry(index: index, peer: peer, theme: theme, strings: strings))
existingPeerIds.insert(peer.peerId)
index += 1
}
}
return entries
}
self.contentGridNode = GridNode()
self.contentTitleNode = ASTextNode()
self.contentTitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor)
self.contentSubtitleNode = ASTextNode()
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.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.shareButtonNode.isHidden = !externalShare
self.contentSeparatorNode = ASDisplayNode()
self.contentSeparatorNode.isLayerBacked = true
self.contentSeparatorNode.displaysAsynchronously = false
self.contentSeparatorNode.backgroundColor = self.theme.actionSheet.opaqueItemSeparatorColor
super.init()
self.addSubnode(self.contentGridNode)
self.addSubnode(self.contentTitleNode)
self.addSubnode(self.contentSubtitleNode)
self.addSubnode(self.searchButtonNode)
self.addSubnode(self.shareButtonNode)
self.addSubnode(self.contentSeparatorNode)
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(account: account, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
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)
}
deinit {
self.disposable.dispose()
}
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: PeerId?) {
self.ensurePeerVisibleOnLayout = peerId
}
func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
self.contentOffsetUpdated = f
}
private func calculateMetrics(size: CGSize) -> (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 = 3.7
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0)
return (gridTopInset, itemWidth)
}
func activate() {
}
func deactivate() {
}
func updateLayout(size: CGSize, 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)
var scrollToItem: GridNodeScrollToItem?
if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
self.ensurePeerVisibleOnLayout = nil
if let index = self.entries.index(where: { $0.peer.peerId == ensurePeerVisibleOnLayout }) {
scrollToItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false)
}
}
let gridSize = CGSize(width: size.width - 12.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), lineSpacing: 0.0)), 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 titleSize = self.contentTitleNode.measure(size)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: titleOffset + 15.0), size: titleSize)
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
let subtitleSize = self.contentSubtitleNode.measure(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: titleOffset + 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: titleOffset + 12.0), size: titleButtonSize)
transition.updateFrame(node: self.searchButtonNode, frame: searchButtonFrame)
let shareButtonFrame = CGRect(origin: CGPoint(x: size.width - titleButtonSize.width - 12.0, y: titleOffset + 12.0), size: titleButtonSize)
transition.updateFrame(node: self.shareButtonNode, frame: shareButtonFrame)
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 updateSelectedPeers() {
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 ?? ""
}
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: true)
}
}
}
@objc func searchPressed() {
self.openSearch?()
}
@objc func sharePressed() {
self.openShare?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let nodes: [ASDisplayNode] = [self.searchButtonNode, self.shareButtonNode]
for node in nodes {
let nodeFrame = node.frame
if let result = node.hitTest(point.offsetBy(dx: -nodeFrame.minX, dy: -nodeFrame.minY), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
}