mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Various improvements
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
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)
|
||||
|
||||
private struct ShareTopicEntry: Comparable, Identifiable {
|
||||
let index: Int32
|
||||
let peer: EngineRenderedPeer
|
||||
let id: Int64
|
||||
let threadData: MessageHistoryThreadData
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
var stableId: Int64 {
|
||||
return self.id
|
||||
}
|
||||
|
||||
static func ==(lhs: ShareTopicEntry, rhs: ShareTopicEntry) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.threadData != rhs.threadData {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
static func <(lhs: ShareTopicEntry, rhs: ShareTopicEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
|
||||
return ShareTopicGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, id: self.id, threadInfo: self.threadData, controllerInteraction: interfaceInteraction)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ShareGridTransaction {
|
||||
let deletions: [Int]
|
||||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
let animated: Bool
|
||||
}
|
||||
|
||||
private func preparedGridEntryTransition(context: AccountContext, from fromEntries: [ShareTopicEntry], to toEntries: [ShareTopicEntry], 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(context: context, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) }
|
||||
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction)) }
|
||||
|
||||
return ShareGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false)
|
||||
}
|
||||
|
||||
private class CancelButtonNode: ASDisplayNode {
|
||||
let buttonNode: HighlightTrackingButtonNode
|
||||
private let arrowNode: ASImageNode
|
||||
private let labelNode: ImmediateTextNode
|
||||
|
||||
var theme: PresentationTheme {
|
||||
didSet {
|
||||
self.updateThemeAndStrings()
|
||||
}
|
||||
}
|
||||
private let strings: PresentationStrings
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.displaysAsynchronously = false
|
||||
self.arrowNode.isUserInteractionEnabled = false
|
||||
|
||||
self.labelNode = ImmediateTextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.buttonNode.addSubnode(self.arrowNode)
|
||||
self.buttonNode.addSubnode(self.labelNode)
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.arrowNode.alpha = 0.4
|
||||
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.labelNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.arrowNode.alpha = 1.0
|
||||
strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.labelNode.alpha = 1.0
|
||||
strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings()
|
||||
}
|
||||
|
||||
func updateThemeAndStrings() {
|
||||
self.labelNode.attributedText = NSAttributedString(string: self.strings.Common_Back, font: Font.regular(17.0), textColor: self.theme.rootController.navigationBar.accentTextColor)
|
||||
|
||||
let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0))
|
||||
|
||||
self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height))
|
||||
self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: self.theme.rootController.navigationBar.accentTextColor)
|
||||
if let image = self.arrowNode.image {
|
||||
self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size)
|
||||
}
|
||||
self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize)
|
||||
self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(11.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height))
|
||||
self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size)
|
||||
self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size)
|
||||
|
||||
return CGSize(width: 70.0, height: 56.0)
|
||||
}
|
||||
}
|
||||
|
||||
final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
func setEnsurePeerVisibleOnLayout(_ peerId: TelegramCore.EnginePeer.Id?) {
|
||||
|
||||
}
|
||||
|
||||
func updateSelectedPeers(animated: Bool) {
|
||||
|
||||
}
|
||||
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let context: AccountContext
|
||||
private let theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
private let controllerInteraction: ShareControllerInteraction
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
private var entries: [ShareTopicEntry] = []
|
||||
private var enqueuedTransitions: [(ShareGridTransaction, Bool)] = []
|
||||
|
||||
private let contentGridNode: GridNode
|
||||
private let contentTitleNode: ASTextNode
|
||||
private let contentSubtitleNode: ASTextNode
|
||||
private let backNode: CancelButtonNode
|
||||
|
||||
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
private var validLayout: (CGSize, CGFloat)?
|
||||
private var overrideGridOffsetTransition: ContainedViewLayoutTransition?
|
||||
|
||||
let peersValue = Promise<[EngineChatList.Item]>()
|
||||
|
||||
var backPressed: () -> Void = {}
|
||||
|
||||
init(sharedContext: SharedAccountContext, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, topics: [EngineChatList.Item], controllerInteraction: ShareControllerInteraction) {
|
||||
self.sharedContext = sharedContext
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
||||
self.peersValue.set(.single(topics))
|
||||
|
||||
let items: Signal<[ShareTopicEntry], NoError> = self.peersValue.get()
|
||||
|> map { topics -> [ShareTopicEntry] in
|
||||
var entries: [ShareTopicEntry] = []
|
||||
var index: Int32 = 0
|
||||
|
||||
for topic in topics {
|
||||
if case let .forum(_, _, threadId, _, _) = topic.index, let threadData = topic.threadData {
|
||||
entries.append(ShareTopicEntry(index: index, peer: EngineRenderedPeer(peer: peer), id: threadId, threadData: threadData, theme: theme, strings: strings))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
self.contentGridNode = GridNode()
|
||||
|
||||
self.contentTitleNode = ASTextNode()
|
||||
self.contentTitleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, 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_SelectTopic, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor)
|
||||
|
||||
self.backNode = CancelButtonNode(theme: theme, strings: strings)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentGridNode)
|
||||
|
||||
self.addSubnode(self.contentTitleNode)
|
||||
self.addSubnode(self.contentSubtitleNode)
|
||||
self.addSubnode(self.backNode)
|
||||
|
||||
let previousItems = Atomic<[ShareTopicEntry]?>(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(context: context, 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.backNode.buttonNode.addTarget(self, action: #selector(self.backButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
@objc private func backButtonPressed() {
|
||||
self.backPressed()
|
||||
}
|
||||
|
||||
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 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 animateIn(sourceFrame: CGRect) {
|
||||
self.backNode.alpha = 1.0
|
||||
self.backNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.backNode.layer.animatePosition(from: CGPoint(x: 20.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.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
self.contentGridNode.forEachItemNode { itemNode in
|
||||
itemNode.layer.animatePosition(from: sourceFrame.center, to: itemNode.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
itemNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(targetFrame: CGRect, completion: @escaping () -> Void = {}) {
|
||||
self.backNode.alpha = 0.0
|
||||
self.backNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.backNode.layer.animatePosition(from: .zero, to: CGPoint(x: 20.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.2)
|
||||
|
||||
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.2)
|
||||
|
||||
self.contentGridNode.alpha = 0.0
|
||||
self.contentGridNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
||||
self.contentGridNode.forEachItemNode { itemNode in
|
||||
itemNode.layer.animatePosition(from: itemNode.position, to: targetFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
itemNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
let scrollToItem: GridNodeScrollToItem? = nil
|
||||
|
||||
let delta = bottomInset
|
||||
var gridSize = CGSize(width: size.width - 12.0, height: size.height)
|
||||
gridSize.height -= delta
|
||||
|
||||
self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 0.0, bottom: 0.0, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let nodes: [ASDisplayNode] = [self.backNode]
|
||||
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)
|
||||
}
|
||||
|
||||
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 backFrame = CGRect(origin: CGPoint(x: 30.0, y: titleOffset + 6.0), size: CGSize(width: 90.0, height: 56.0))
|
||||
transition.updateFrame(node: self.backNode, frame: backFrame)
|
||||
|
||||
self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user