mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Added recent stickers clearing Added sending logs via email Added forward recipient change on forward acccessory panel tap Tweaked undo panel design Various UI fixes
410 lines
19 KiB
Swift
410 lines
19 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 presence: PeerPresence?
|
|
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
|
|
}
|
|
if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence {
|
|
if !lhsPresence.isEqual(to: rhsPresence) {
|
|
return false
|
|
}
|
|
} else if (lhs.presence != nil) != (rhs.presence != nil) {
|
|
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, presence: self.presence, controllerInteraction: interfaceInteraction, search: false)
|
|
}
|
|
}
|
|
|
|
private struct ShareGridTransaction {
|
|
let deletions: [Int]
|
|
let insertions: [GridNodeInsertItem]
|
|
let updates: [GridNodeUpdateItem]
|
|
let animated: Bool
|
|
}
|
|
|
|
private let avatarFont: UIFont = UIFont(name: ".SFCompactRounded-Semibold", size: 17.0)!
|
|
|
|
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 sharedContext: SharedAccountContext
|
|
private let account: Account
|
|
private let theme: PresentationTheme
|
|
private let strings: PresentationStrings
|
|
private let controllerInteraction: ShareControllerInteraction
|
|
private let switchToAnotherAccount: () -> Void
|
|
|
|
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 contentTitleAccountNode: AvatarNode
|
|
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(sharedContext: SharedAccountContext, account: Account, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void) {
|
|
self.sharedContext = sharedContext
|
|
self.account = account
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.controllerInteraction = controllerInteraction
|
|
self.accountPeer = accountPeer
|
|
self.switchToAnotherAccount = switchToAnotherAccount
|
|
|
|
let items: Signal<[SharePeerEntry], NoError> = combineLatest(.single(peers), self.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), presence: nil, theme: theme, strings: strings))
|
|
index += 1
|
|
|
|
for peer in foundPeers.reversed() {
|
|
entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings))
|
|
existingPeerIds.insert(peer.peerId)
|
|
index += 1
|
|
}
|
|
|
|
for (peer, presence) in initialPeers {
|
|
if !existingPeerIds.contains(peer.peerId) {
|
|
entries.append(SharePeerEntry(index: index, peer: peer, presence: presence, 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.contentTitleAccountNode = AvatarNode(font: avatarFont)
|
|
var hasOtherAccounts = false
|
|
if switchableAccounts.count > 1, let info = switchableAccounts.first(where: { $0.account.id == account.id }) {
|
|
hasOtherAccounts = true
|
|
self.contentTitleAccountNode.setPeer(account: account, theme: theme, peer: 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.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.contentTitleNode)
|
|
self.addSubnode(self.contentSubtitleNode)
|
|
self.addSubnode(self.contentTitleAccountNode)
|
|
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)
|
|
self.contentTitleAccountNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.accountTapGesture(_:))))
|
|
}
|
|
|
|
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), 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 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)
|
|
|
|
let avatarButtonSize = CGSize(width: 36.0, height: 36.0)
|
|
let avatarButtonFrame = CGRect(origin: CGPoint(x: size.width - avatarButtonSize.width - 20.0, y: titleOffset + 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 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, self.contentTitleAccountNode]
|
|
for node in nodes {
|
|
let nodeFrame = node.frame
|
|
if node.isHidden {
|
|
continue
|
|
}
|
|
if let result = node.hitTest(point.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()
|
|
}
|
|
}
|