Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ShareController",
module_name = "ShareController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SaveToCameraRoll:SaveToCameraRoll",
"//submodules/StickerResources:StickerResources",
"//submodules/UrlEscaping:UrlEscaping",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/ActionSheetPeerItem:ActionSheetPeerItem",
"//submodules/ChatListSearchRecentPeersNode:ChatListSearchRecentPeersNode",
"//submodules/PeerPresenceStatusManager:PeerPresenceStatusManager",
"//submodules/SelectablePeerNode:SelectablePeerNode",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramIntents:TelegramIntents",
"//submodules/AccountContext:AccountContext",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/ContextUI:ContextUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/UndoUI",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/ChatPresentationInterfaceState",
"//submodules/CheckNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,186 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ContextUI
import CheckNode
public final class ShareActionButtonNode: HighlightTrackingButtonNode {
private let referenceNode: ContextReferenceContentNode
private let containerNode: ContextControllerSourceNode
private let badgeLabel: TextNode
private var badgeText: NSAttributedString?
private let badgeBackground: ASImageNode
public var badgeBackgroundColor: UIColor {
didSet {
self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: self.badgeBackgroundColor)
}
}
public var badgeTextColor: UIColor {
didSet {
self.setNeedsLayout()
}
}
public var badge: String? {
didSet {
if self.badge != oldValue {
if let badge = self.badge {
self.badgeText = NSAttributedString(string: badge, font: Font.regular(14.0), textColor: self.badgeTextColor, paragraphAlignment: .center)
self.badgeLabel.isHidden = false
self.badgeBackground.isHidden = false
} else {
self.badgeText = nil
self.badgeLabel.isHidden = true
self.badgeBackground.isHidden = true
}
self.setNeedsLayout()
}
}
}
var shouldBegin: (() -> Bool)?
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
public init(badgeBackgroundColor: UIColor, badgeTextColor: UIColor) {
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.badgeBackgroundColor = badgeBackgroundColor
self.badgeTextColor = badgeTextColor
self.badgeLabel = TextNode()
self.badgeLabel.isHidden = true
self.badgeLabel.isUserInteractionEnabled = false
self.badgeLabel.displaysAsynchronously = false
self.badgeBackground = ASImageNode()
self.badgeBackground.isHidden = true
self.badgeBackground.isLayerBacked = true
self.badgeBackground.displaysAsynchronously = false
self.badgeBackground.displayWithoutProcessing = true
self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: badgeBackgroundColor)
super.init()
self.containerNode.addSubnode(self.referenceNode)
self.addSubnode(self.containerNode)
self.addSubnode(self.badgeBackground)
self.addSubnode(self.badgeLabel)
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self, let _ = strongSelf.contextAction else {
return false
}
if let shouldBegin = strongSelf.shouldBegin {
return shouldBegin()
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.referenceNode, gesture)
}
}
override public func layout() {
super.layout()
if !self.badgeLabel.isHidden {
let (badgeLayout, badgeApply) = TextNode.asyncLayout(self.badgeLabel)(TextNodeLayoutArguments(attributedString: self.badgeText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let _ = badgeApply()
let backgroundSize = CGSize(width: max(22.0, badgeLayout.size.width + 10.0 + 1.0), height: 22.0)
let backgroundFrame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 6.0, y: self.bounds.size.height - 39.0), size: backgroundSize)
self.badgeBackground.frame = backgroundFrame
self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeLayout.size.width / 2.0), y: backgroundFrame.minY + 3.0), size: badgeLayout.size)
}
self.containerNode.frame = self.bounds
self.referenceNode.frame = self.bounds
}
}
public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode {
private let checkNode: CheckNode
private let titleTextNode: TextNode
public var titleTextColor: UIColor {
didSet {
self.setNeedsLayout()
}
}
public var checkNodeTheme: CheckNodeTheme {
didSet {
self.checkNode.theme = self.checkNodeTheme
}
}
private let titleText: String
public var value: Bool {
return self.checkNode.selected
}
public var updated: (() -> Void)?
public init(titleText: String, titleTextColor: UIColor, checkNodeTheme: CheckNodeTheme) {
self.titleText = titleText
self.titleTextColor = titleTextColor
self.checkNodeTheme = checkNodeTheme
self.checkNode = CheckNode(theme: checkNodeTheme, content: .check)
self.checkNode.isUserInteractionEnabled = false
self.titleTextNode = TextNode()
self.titleTextNode.isUserInteractionEnabled = false
self.titleTextNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.checkNode)
self.addSubnode(self.titleTextNode)
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.checkNode.setSelected(!self.checkNode.selected, animated: true)
self.updated?()
}
override public func layout() {
super.layout()
if self.bounds.width < 1.0 {
return
}
let checkSize: CGFloat = 18.0
let checkSpacing: CGFloat = 10.0
let (titleTextLayout, titleTextApply) = TextNode.asyncLayout(self.titleTextNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.titleText, font: Font.regular(13.0), textColor: self.titleTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: self.bounds.width - 8.0 * 2.0 - checkSpacing - checkSize, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let _ = titleTextApply()
let contentWidth = checkSize + checkSpacing + titleTextLayout.size.width
let checkFrame = CGRect(origin: CGPoint(x: floor((self.bounds.width - contentWidth) * 0.5), y: floor((self.bounds.height - checkSize) * 0.5)), size: CGSize(width: checkSize, height: checkSize))
let isFirstTime = self.checkNode.bounds.isEmpty
self.checkNode.frame = checkFrame
if isFirstTime {
self.checkNode.setSelected(false, animated: false)
}
self.titleTextNode.frame = CGRect(origin: CGPoint(x: checkFrame.maxX + checkSpacing, y: floor((self.bounds.height - titleTextLayout.size.height) * 0.5)), size: titleTextLayout.size)
}
}
@@ -0,0 +1,16 @@
import Foundation
import UIKit
import Display
import TelegramCore
import TelegramPresentationData
public protocol ShareContentContainerNode: AnyObject {
func activate()
func deactivate()
func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?)
func setDidBeginDragging(_ f: (() -> Void)?)
func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?)
func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition)
func updateTheme(_ theme: PresentationTheme)
func updateSelectedPeers(animated: Bool)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,351 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import TelegramPresentationData
import TelegramStringFormatting
import SelectablePeerNode
import PeerPresenceStatusManager
import AccountContext
import ShimmerEffect
final class ShareControllerInteraction {
var foundPeers: [(peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool)] = []
var selectedPeerIds = Set<EnginePeer.Id>()
var selectedPeers: [EngineRenderedPeer] = []
var selectedTopics: [EnginePeer.Id: (Int64, MessageHistoryThreadData?)] = [:]
let togglePeer: (EngineRenderedPeer, Bool) -> Void
let selectTopic: (EngineRenderedPeer, Int64, MessageHistoryThreadData?) -> Void
let shareStory: (() -> Void)?
let disabledPeerSelected: (EngineRenderedPeer) -> Void
init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void, selectTopic: @escaping (EngineRenderedPeer, Int64, MessageHistoryThreadData?) -> Void, shareStory: (() -> Void)?, disabledPeerSelected: @escaping (EngineRenderedPeer) -> Void) {
self.togglePeer = togglePeer
self.selectTopic = selectTopic
self.shareStory = shareStory
self.disabledPeerSelected = disabledPeerSelected
}
}
final class ShareControllerGridSection: GridSection {
let height: CGFloat = 33.0
private let title: String
private let theme: PresentationTheme
var hashValue: Int {
return 1
}
init(title: String, theme: PresentationTheme) {
self.title = title
self.theme = theme
}
func isEqual(to: GridSection) -> Bool {
if let to = to as? ShareControllerGridSection {
return self.title == to.title && self.theme === to.theme
} else {
return false
}
}
func node() -> ASDisplayNode {
return ShareControllerGridSectionNode(title: self.title, theme: self.theme)
}
}
private let sectionTitleFont = Font.bold(13.0)
final class ShareControllerGridSectionNode: ASDisplayNode {
let backgroundNode: ASDisplayNode
let titleNode: ASTextNode
init(title: String, theme: PresentationTheme) {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = theme.chatList.sectionHeaderFillColor
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.attributedText = NSAttributedString(string: title.uppercased(), font: sectionTitleFont, textColor: theme.chatList.sectionHeaderTextColor)
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
}
override func layout() {
super.layout()
let bounds = self.bounds
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: bounds.size.width, height: 27.0))
let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 16.0, y: 6.0 + UIScreenPixel), size: titleSize)
}
}
final class ShareControllerPeerGridItem: GridItem {
enum ShareItem: Equatable {
enum StoryMode {
case createStory
case repostStory
case repostMessage
}
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)
case story(mode: StoryMode)
var peerId: EnginePeer.Id? {
if case let .peer(peer, _, _, _, _, _) = self {
return peer.peerId
} else {
return nil
}
}
}
let environment: ShareControllerEnvironment
let context: ShareControllerAccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let item: ShareItem?
let controllerInteraction: ShareControllerInteraction
let search: Bool
let section: GridSection?
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, item: ShareItem?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) {
self.environment = environment
self.context = context
self.theme = theme
self.strings = strings
self.item = item
self.controllerInteraction = controllerInteraction
self.search = search
if let sectionTitle = sectionTitle {
self.section = ShareControllerGridSection(title: sectionTitle, theme: self.theme)
} else {
self.section = nil
}
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ShareControllerPeerGridItemNode()
node.controllerInteraction = self.controllerInteraction
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings, item: self.item, search: self.search, synchronousLoad: synchronousLoad, force: false)
return node
}
func update(node: GridItemNode) {
guard let node = node as? ShareControllerPeerGridItemNode else {
assertionFailure()
return
}
node.controllerInteraction = self.controllerInteraction
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings, item: self.item, search: self.search, synchronousLoad: false, force: false)
}
}
final class ShareControllerPeerGridItemNode: GridItemNode {
private var currentState: (environment: ShareControllerEnvironment, accountContext: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, item: ShareControllerPeerGridItem.ShareItem?, search: Bool)?
private let peerNode: SelectablePeerNode
private var presenceManager: PeerPresenceStatusManager?
var controllerInteraction: ShareControllerInteraction?
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
var peerId: EnginePeer.Id? {
if let item = self.currentState?.item, case let .peer(peer, _, _, _, _, _) = item {
return peer.peerId
} else {
return nil
}
}
override init() {
self.peerNode = SelectablePeerNode()
super.init()
self.peerNode.toggleSelection = { [weak self] isDisabled in
if let strongSelf = self {
if let (_, _, _, _, maybeItem, search) = strongSelf.currentState, let item = maybeItem {
if case let .peer(peer, _, _, _, _, _) = item, let _ = peer.peers[peer.peerId] {
if isDisabled {
strongSelf.controllerInteraction?.disabledPeerSelected(peer)
} else {
strongSelf.controllerInteraction?.togglePeer(peer, search)
}
} else if case .story = item {
strongSelf.controllerInteraction?.shareStory?()
}
}
}
}
self.addSubnode(self.peerNode)
self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in
guard let strongSelf = self, let currentState = strongSelf.currentState else {
return
}
strongSelf.setup(environment: currentState.0, context: currentState.1, theme: currentState.2, strings: currentState.3, item: currentState.4, search: currentState.5, synchronousLoad: false, force: true)
})
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
let rect = absoluteRect
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
func setup(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, item: ShareControllerPeerGridItem.ShareItem?, search: Bool, synchronousLoad: Bool, force: Bool) {
if force || self.currentState == nil || self.currentState!.1 !== context || self.currentState!.3 !== theme || self.currentState!.item != item {
let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: theme.chatList.secretTitleColor, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.checkContentColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor)
var effectivePresence: EnginePeer.Presence?
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
self.peerNode.theme = itemTheme
if let item, case let .peer(renderedPeer, presence, _, threadData, requiresPremiumForMessaging, requiresStars) = item, let peer = renderedPeer.peer {
effectivePresence = presence
var isOnline = false
var isSupport = false
if case let .user(user) = peer, user.flags.contains(.isSupport) {
isSupport = true
}
if let presence, !peer.isService && !isSupport && peer.id != context.accountPeerId {
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: timestamp)
if case .online = relativeStatus {
isOnline = true
}
}
let resolveInlineStickers = context.resolveInlineStickers
self.peerNode.setup(
accountPeerId: context.accountPeerId,
postbox: context.stateManager.postbox,
network: context.stateManager.network,
energyUsageSettings: environment.energyUsageSettings,
contentSettings: context.contentSettings,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
resolveInlineStickers: { fileIds in
return resolveInlineStickers(fileIds)
},
theme: theme,
strings: strings,
peer: renderedPeer,
requiresPremiumForMessaging: requiresPremiumForMessaging,
requiresStars: requiresStars,
customTitle: threadData?.info.title,
iconId: threadData?.info.icon,
iconColor: threadData?.info.iconColor ?? 0,
online: isOnline,
synchronousLoad: synchronousLoad
)
if let shimmerNode = self.placeholderNode {
self.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
} else if let item, case let .story(mode) = item {
let storyMode: SelectablePeerNode.StoryMode
switch mode {
case .createStory:
storyMode = .createStory
case .repostStory:
storyMode = .repostStory
case .repostMessage:
storyMode = .repostMessage
}
self.peerNode.setupStoryRepost(
accountPeerId: context.accountPeerId,
postbox: context.stateManager.postbox,
network: context.stateManager.network,
theme: theme,
strings: strings,
synchronousLoad: synchronousLoad,
storyMode: storyMode
)
} else {
let shimmerNode: ShimmerEffectNode
if let current = self.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
self.placeholderNode = shimmerNode
self.addSubnode(shimmerNode)
}
shimmerNode.frame = self.bounds
if let (rect, size) = self.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 56.0
let lineDiameter: CGFloat = 10.0
let iconFrame = CGRect(x: 13.0, y: 4.0, width: 60.0, height: 60.0)
shapes.append(.circle(iconFrame))
let titleFrame = CGRect(x: 15.0, y: 70.0, width: 56.0, height: 10.0)
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, horizontal: true, size: self.bounds.size)
}
self.currentState = (environment, context, theme, strings, item, search)
self.setNeedsLayout()
if let effectivePresence {
self.presenceManager?.reset(presence: effectivePresence)
}
}
self.updateSelection(animated: false)
}
func updateSelection(animated: Bool) {
var selected = false
if let controllerInteraction = self.controllerInteraction, let (_, _, _, _, maybeItem, _) = self.currentState, let item = maybeItem {
if case let .peer(peer, _, _, _, _, _) = item {
selected = controllerInteraction.selectedPeerIds.contains(peer.peerId)
}
}
self.peerNode.updateSelection(selected: selected, animated: animated)
}
override func layout() {
super.layout()
let bounds = self.bounds
self.peerNode.frame = bounds
self.placeholderNode?.frame = bounds
if let theme = self.currentState?.theme, let shimmerNode = self.placeholderNode {
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 56.0
let lineDiameter: CGFloat = 10.0
let iconFrame = CGRect(x: (bounds.width - 60.0) / 2.0, y: 4.0, width: 60.0, height: 60.0)
shapes.append(.circle(iconFrame))
let titleFrame = CGRect(x: (bounds.width - titleLineWidth) / 2.0, y: 70.0, width: titleLineWidth, height: 10.0)
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, horizontal: true, size: self.bounds.size)
}
}
}
@@ -0,0 +1,106 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import TelegramPresentationData
import ChatListSearchRecentPeersNode
import AccountContext
final class ShareControllerRecentPeersGridItem: GridItem {
let environment: ShareControllerEnvironment
let context: ShareControllerAccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let controllerInteraction: ShareControllerInteraction
let section: GridSection? = nil
let fillsRowWithHeight: (CGFloat, Bool)? = (102.0, true)
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction) {
self.environment = environment
self.context = context
self.theme = theme
self.strings = strings
self.controllerInteraction = controllerInteraction
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ShareControllerRecentPeersGridItemNode()
node.controllerInteraction = self.controllerInteraction
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings)
return node
}
func update(node: GridItemNode) {
guard let node = node as? ShareControllerRecentPeersGridItemNode else {
assertionFailure()
return
}
node.controllerInteraction = self.controllerInteraction
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings)
}
}
final class ShareControllerRecentPeersGridItemNode: GridItemNode {
private var currentState: (ShareControllerAccountContext, PresentationTheme, PresentationStrings)?
var controllerInteraction: ShareControllerInteraction?
private var peersNode: ChatListSearchRecentPeersNode?
override init() {
super.init()
}
func setup(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings) {
if self.currentState == nil || self.currentState!.0 !== context || self.currentState!.1 !== theme {
let peersNode: ChatListSearchRecentPeersNode
if let currentPeersNode = self.peersNode {
peersNode = currentPeersNode
peersNode.updateThemeAndStrings(theme: theme, strings: strings)
} else {
peersNode = ChatListSearchRecentPeersNode(
accountPeerId: context.accountPeerId,
postbox: context.stateManager.postbox,
network: context.stateManager.network,
energyUsageSettings: environment.energyUsageSettings,
contentSettings: context.contentSettings,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
resolveInlineStickers: context.resolveInlineStickers,
theme: theme,
mode: .actionSheet,
strings: strings,
peerSelected: { [weak self] peer in
self?.controllerInteraction?.togglePeer(EngineRenderedPeer(peer: peer), true)
},
peerContextAction: { _, _, gesture, _ in gesture?.cancel() },
isPeerSelected: { [weak self] peerId in
return self?.controllerInteraction?.selectedPeerIds.contains(peerId) ?? false
},
share: true
)
self.peersNode = peersNode
self.addSubnode(peersNode)
}
self.currentState = (context, theme, strings)
}
self.updateSelection(animated: false)
}
func updateSelection(animated: Bool) {
self.peersNode?.updateSelectedPeers(animated: animated)
}
override func layout() {
super.layout()
let bounds = self.bounds
self.peersNode?.frame = CGRect(origin: CGPoint(x: -8.0, y: 0.0), size: CGSize(width: bounds.width + 8.0, height: bounds.height))
self.peersNode?.updateLayout(size: bounds.size, leftInset: 0.0, rightInset: 0.0)
}
}
@@ -0,0 +1,399 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
import ComponentFlow
import MultilineTextComponent
import AnimatedTextComponent
private func generateClearIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
}
public final class ShareInputFieldNodeTheme: Equatable {
let backgroundColor: UIColor
let textColor: UIColor
let placeholderColor: UIColor
let clearButtonColor: UIColor
let accentColor: UIColor
let keyboard: PresentationThemeKeyboardColor
public init(backgroundColor: UIColor, textColor: UIColor, placeholderColor: UIColor, clearButtonColor: UIColor, accentColor: UIColor, keyboard: PresentationThemeKeyboardColor) {
self.backgroundColor = backgroundColor
self.textColor = textColor
self.placeholderColor = placeholderColor
self.clearButtonColor = clearButtonColor
self.accentColor = accentColor
self.keyboard = keyboard
}
public static func ==(lhs: ShareInputFieldNodeTheme, rhs: ShareInputFieldNodeTheme) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.placeholderColor != rhs.placeholderColor {
return false
}
if lhs.clearButtonColor != rhs.clearButtonColor {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.keyboard != rhs.keyboard {
return false
}
return true
}
}
public extension ShareInputFieldNodeTheme {
convenience init(presentationTheme theme: PresentationTheme) {
self.init(backgroundColor: theme.actionSheet.inputBackgroundColor, textColor: theme.actionSheet.inputTextColor, placeholderColor: theme.actionSheet.inputPlaceholderColor, clearButtonColor: theme.actionSheet.inputClearButtonColor, accentColor: theme.actionSheet.controlAccentColor, keyboard: theme.rootController.keyboardColor)
}
}
private final class ShareInputCopyComponent: Component {
let theme: ShareInputFieldNodeTheme
let strings: PresentationStrings
let text: String
let action: () -> Void
init(
theme: ShareInputFieldNodeTheme,
strings: PresentationStrings,
text: String,
action: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.text = text
self.action = action
}
static func ==(lhs: ShareInputCopyComponent, rhs: ShareInputCopyComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
final class View: UIView {
let text = ComponentView<Empty>()
let button = ComponentView<Empty>()
let textMask = UIImageView()
var component: ShareInputCopyComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ShareInputCopyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let textChanged = self.component != nil && self.component?.text != component.text
self.component = component
var textItems: [AnimatedTextComponent.Item] = []
if let range = component.text.range(of: "?", options: .backwards) {
textItems.append(AnimatedTextComponent.Item(id: 0, isUnbreakable: true, content: .text(String(component.text[component.text.startIndex ..< range.lowerBound]))))
textItems.append(AnimatedTextComponent.Item(id: 1, isUnbreakable: true, content: .text(String(component.text[range.lowerBound...]))))
} else {
textItems.append(AnimatedTextComponent.Item(id: 0, isUnbreakable: true, content: .text(component.text)))
}
let sideInset: CGFloat = 12.0
let textSize = self.text.update(
transition: textChanged ? .spring(duration: 0.4) : .immediate,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(17.0),
color: component.theme.textColor,
items: textItems,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((availableSize.height - textSize.height) * 0.5)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
textView.mask = self.textMask
}
textView.frame = textFrame
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.Conversation_LinkDialogCopy, font: Font.regular(17.0), textColor: component.theme.accentColor))
)),
action: { [weak self] in
guard let self else {
return
}
self.component?.action()
}
).minSize(CGSize(width: 0.0, height: availableSize.height))),
environment: {},
containerSize: CGSize(width: availableSize.width - 40.0, height: 1000.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - buttonSize.width, y: floor((availableSize.height - buttonSize.height) * 0.5)), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
if self.textMask.image == nil {
let gradientWidth: CGFloat = 26.0
self.textMask.image = generateGradientImage(size: CGSize(width: gradientWidth, height: 8.0), colors: [
UIColor(white: 1.0, alpha: 1.0),
UIColor(white: 1.0, alpha: 1.0),
UIColor(white: 1.0, alpha: 0.0)
], locations: [
0.0,
1.0 / gradientWidth,
1.0
], direction: .horizontal)?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: gradientWidth - 1.0), resizingMode: .stretch)
self.textMask.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, buttonFrame.minX - 4.0 - textFrame.minX), height: textFrame.height))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private let theme: ShareInputFieldNodeTheme
private let strings: PresentationStrings
private let backgroundNode: ASImageNode
private let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode
private let clearButton: HighlightableButtonNode
private var copyView: ComponentView<Empty>?
public var updateHeight: (() -> Void)?
public var updateText: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 1.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 22.0)
private let accessoryButtonsWidth: CGFloat = 10.0
private var inputCopyText: String?
public var onInputCopyText: (() -> Void)?
private var selectTextOnce: Bool = false
public var text: String {
get {
return self.textInputNode.attributedText?.string ?? ""
}
set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.textColor)
self.placeholderNode.isHidden = !newValue.isEmpty || self.inputCopyText != nil
self.clearButton.isHidden = newValue.isEmpty
}
}
public var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.placeholderColor)
}
}
public init(theme: ShareInputFieldNodeTheme, strings: PresentationStrings, placeholder: String) {
self.theme = theme
self.strings = strings
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.backgroundColor)
self.textInputNode = EditableTextNode()
let textColor: UIColor = theme.textColor
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
self.textInputNode.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
self.textInputNode.keyboardAppearance = theme.keyboard.keyboardAppearance
self.textInputNode.tintColor = theme.accentColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.placeholderColor)
self.clearButton = HighlightableButtonNode()
self.clearButton.imageNode.displaysAsynchronously = false
self.clearButton.imageNode.displayWithoutProcessing = true
self.clearButton.displaysAsynchronously = false
self.clearButton.setImage(generateClearIcon(color: theme.clearButtonColor), for: [])
self.clearButton.isHidden = true
super.init()
self.textInputNode.delegate = self
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.clearButton)
self.textInputNode.textView.showsVerticalScrollIndicator = false
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
}
public func preselectText() {
self.selectTextOnce = true
}
public func updateLayout(width: CGFloat, inputCopyText: String?, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let accessoryButtonsWidth = self.accessoryButtonsWidth
self.inputCopyText = inputCopyText
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.size.width - placeholderSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
if let image = self.clearButton.image(for: []) {
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size))
}
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: backgroundFrame.size.height)))
self.textInputNode.isUserInteractionEnabled = inputCopyText == nil
self.textInputNode.isHidden = inputCopyText != nil
self.placeholderNode.isHidden = !(self.textInputNode.textView.text ?? "").isEmpty || self.inputCopyText != nil
if let inputCopyText {
let copyView: ComponentView<Empty>
if let current = self.copyView {
copyView = current
} else {
copyView = ComponentView()
self.copyView = copyView
}
let copyViewSize = copyView.update(
transition: .immediate,
component: AnyComponent(ShareInputCopyComponent(
theme: self.theme,
strings: self.strings,
text: inputCopyText,
action: {
self.onInputCopyText?()
}
)),
environment: {},
containerSize: backgroundFrame.size
)
let copyViewFrame = CGRect(origin: backgroundFrame.origin, size: copyViewSize)
if let copyComponentView = copyView.view {
if copyComponentView.superview == nil {
self.view.addSubview(copyComponentView)
}
copyComponentView.frame = copyViewFrame
}
} else if let copyView = self.copyView {
self.copyView = nil
copyView.view?.removeFromSuperview()
}
return panelHeight
}
public func activateInput() {
self.textInputNode.becomeFirstResponder()
}
public func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
@objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.updateTextNodeText(animated: true)
self.updateText?(editableTextNode.attributedText?.string ?? "")
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil
}
public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.clearButton.isHidden = false
if self.selectTextOnce {
self.selectTextOnce = false
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
self.textInputNode.selectedRange = NSRange(self.text.startIndex ..< self.text.endIndex, in: self.text)
})
}
}
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil
self.clearButton.isHidden = true
}
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let accessoryButtonsWidth = self.accessoryButtonsWidth
if self.inputCopyText != nil {
return 41.0
} else {
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(41.0, unboundTextFieldHeight))
}
}
private func updateTextNodeText(animated: Bool) {
let backgroundInsets = self.backgroundInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight?()
}
}
@objc func clearPressed() {
self.textInputNode.attributedText = nil
self.deactivateInput()
self.updateHeight?()
}
}
@@ -0,0 +1,374 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramPresentationData
import ActivityIndicator
import RadialStatusNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AppBundle
import TelegramUniversalVideoContent
import TelegramCore
import Postbox
import AccountContext
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
if useTotalFileAllocatedSize {
let url = URL(fileURLWithPath: path)
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) {
if values.isRegularFile ?? false {
if let fileSize = values.fileAllocatedSize {
return Int64(fileSize)
}
}
}
}
var value = stat()
if stat(path, &value) == 0 {
return value.st_size
} else {
return nil
}
}
public enum ShareLoadingState {
case preparing
case progress(Float)
case done
}
protocol ShareLoadingContainer: ASDisplayNode {
var state: ShareLoadingState { get set }
}
public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContainerNode, ShareLoadingContainer {
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var theme: PresentationTheme
private let activityIndicator: ActivityIndicator
private let statusNode: RadialStatusNode
private let doneStatusNode: RadialStatusNode
public var state: ShareLoadingState = .preparing {
didSet {
switch self.state {
case .preparing:
self.activityIndicator.isHidden = false
self.statusNode.isHidden = true
case let .progress(value):
self.activityIndicator.isHidden = true
self.statusNode.isHidden = false
self.statusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: max(0.12, CGFloat(value)), cancelEnabled: false, animateRotation: true), completion: {})
case .done:
self.activityIndicator.isHidden = true
self.statusNode.isHidden = false
self.statusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: 1.0, cancelEnabled: false, animateRotation: true), completion: {})
self.doneStatusNode.transitionToState(.check(self.theme.actionSheet.controlAccentColor), completion: {})
}
}
}
public init(theme: PresentationTheme, forceNativeAppearance: Bool) {
self.theme = theme
self.activityIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, !forceNativeAppearance ? 22.0 : 50.0, 2.0, forceNativeAppearance))
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
super.init()
self.addSubnode(self.activityIndicator)
self.addSubnode(self.statusNode)
self.addSubnode(self.doneStatusNode)
self.doneStatusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: 0.0, cancelEnabled: false, animateRotation: true), completion: {})
}
public func activate() {
}
public func deactivate() {
}
public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) {
}
public func setDidBeginDragging(_ f: (() -> Void)?) {
}
public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
self.contentOffsetUpdated = f
}
public func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
}
public func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let nodeHeight: CGFloat = 125.0
let indicatorSize = self.activityIndicator.calculateSizeThatFits(size)
let indicatorFrame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: size.height - nodeHeight + floor((nodeHeight - indicatorSize.height) / 2.0)), size: indicatorSize)
transition.updateFrame(node: self.activityIndicator, frame: indicatorFrame)
let statusFrame = indicatorFrame
transition.updateFrame(node: self.statusNode, frame: statusFrame)
transition.updateFrame(node: self.doneStatusNode, frame: statusFrame)
self.contentOffsetUpdated?(-size.height + 64.0, transition)
}
public func updateSelectedPeers(animated: Bool) {
}
}
public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareContentContainerNode, ShareLoadingContainer {
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var theme: PresentationTheme
private let strings: PresentationStrings
private let animationNode: AnimatedStickerNode
private let doneAnimationNode: AnimatedStickerNode
private let progressTextNode: ImmediateTextNode
private let progressBackgroundNode: ASDisplayNode
private let progressForegroundNode: ASDisplayNode
private let animationStatusDisposable = MetaDisposable()
private var progressValue: CGFloat = 0.0
private var targetProgressValue: CGFloat = 0.0
private var animator: ConstantDisplayLinkAnimator?
private var randomCompletionStart: CGFloat = .random(in: 0.94...0.97)
private var isDone: Bool = false
private var startTimestamp: Double?
private var videoNode: UniversalVideoNode?
public var state: ShareLoadingState = .preparing {
didSet {
switch self.state {
case .preparing:
break
case let .progress(value):
let currentTimestamp = CACurrentMediaTime()
if self.startTimestamp == nil {
self.startTimestamp = currentTimestamp
} else if let startTimestamp = self.startTimestamp, currentTimestamp - startTimestamp < 1.0, value > 0.5 && value < 0.9 {
self.randomCompletionStart = 0.8
}
self.targetProgressValue = CGFloat(value) * self.randomCompletionStart
if self.animator == nil {
self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in
if let strongSelf = self, strongSelf.targetProgressValue > strongSelf.progressValue {
let updatedProgress = strongSelf.progressValue + 0.005
strongSelf.progressValue = min(1.0, updatedProgress)
if strongSelf.progressValue == 1.0 && !strongSelf.isDone {
strongSelf.isDone = true
strongSelf.animator?.invalidate()
if let snapshotView = strongSelf.progressTextNode.view.snapshotContentTree() {
snapshotView.frame = strongSelf.progressTextNode.frame
strongSelf.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.25, removeOnCompletion: false, additive: true)
strongSelf.progressTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
strongSelf.progressTextNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 20.0), to: CGPoint(), duration: 0.25, additive: true)
}
}
if let (size, isLandscape, bottomInset) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, isLandscape: isLandscape, bottomInset: bottomInset, transition: .immediate)
}
}
})
self.animator?.isPaused = false
}
case .done:
if let (size, isLandscape, bottomInset) = self.validLayout {
self.updateLayout(size: size, isLandscape: isLandscape, bottomInset: bottomInset, transition: .animated(duration: 0.2, curve: .easeInOut))
}
self.animationNode.stopAtNearestLoop = true
self.animationNode.completed = { [weak self] _ in
if let strongSelf = self {
strongSelf.animationNode.visibility = false
strongSelf.doneAnimationNode.visibility = true
strongSelf.doneAnimationNode.isHidden = false
}
}
self.animationNode.frameUpdated = { [weak self] index, total in
if let strongSelf = self {
let progress = min(1.0, CGFloat(index) / CGFloat(total))
let delta = 1.0 - strongSelf.randomCompletionStart
strongSelf.targetProgressValue = strongSelf.randomCompletionStart + delta * progress * 0.5
}
}
self.doneAnimationNode.frameUpdated = { [weak self] index, total in
if let strongSelf = self {
let progress = min(1.0, CGFloat(index) / CGFloat(total) * 2.1)
let delta = 1.0 - strongSelf.randomCompletionStart
strongSelf.targetProgressValue = strongSelf.randomCompletionStart + delta * 0.5 + delta * progress * 0.5
}
}
self.doneAnimationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animationNode.isHidden = true
}
}
}
}
private var elapsedTime: Double = 0.0
public var completionDuration: Double {
return self.elapsedTime + 3.0 + 0.15
}
public init(theme: PresentationTheme, strings: PresentationStrings, forceNativeAppearance: Bool, postbox: Postbox?, environment: ShareControllerEnvironment) {
self.theme = theme
self.strings = strings
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ShareProgress"), width: 384, height: 384, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.doneAnimationNode = DefaultAnimatedStickerNodeImpl()
self.doneAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ShareDone"), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.doneAnimationNode.visibility = false
self.doneAnimationNode.isHidden = true
self.progressTextNode = ImmediateTextNode()
self.progressTextNode.textAlignment = .center
self.progressBackgroundNode = ASDisplayNode()
self.progressBackgroundNode.backgroundColor = theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2)
self.progressBackgroundNode.cornerRadius = 3.0
self.progressForegroundNode = ASDisplayNode()
self.progressForegroundNode.backgroundColor = theme.actionSheet.controlAccentColor
self.progressForegroundNode.cornerRadius = 3.0
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.doneAnimationNode)
self.addSubnode(self.progressTextNode)
self.addSubnode(self.progressBackgroundNode)
self.addSubnode(self.progressForegroundNode)
self.animationStatusDisposable.set((self.animationNode.status
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
strongSelf.elapsedTime = status.duration - status.timestamp
}
}))
if let postbox, let mediaManager = environment.mediaManager, let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let _ = postbox
let _ = mediaManager
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let _ = decoration
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let _ = videoContent
/*let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
videoNode.alpha = 0.01
self.videoNode = videoNode
self.addSubnode(videoNode)
videoNode.canAttachContent = true
videoNode.play()*/
}
}
deinit {
self.animationStatusDisposable.dispose()
}
public func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
}
public func activate() {
}
public func deactivate() {
}
public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) {
}
public func setDidBeginDragging(_ f: (() -> Void)?) {
}
public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
self.contentOffsetUpdated = f
}
private var validLayout: (CGSize, Bool, CGFloat)?
public func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, isLandscape, bottomInset)
let nodeHeight: CGFloat = 450.0
let inset: CGFloat = 24.0
let progressHeight: CGFloat = 6.0
let spacing: CGFloat = 16.0
let progress = self.progressValue
let progressFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: size.width - inset * 2.0, height: progressHeight)
self.progressBackgroundNode.frame = progressFrame
let progressForegroundFrame = CGRect(x: progressFrame.minX, y: progressFrame.minY, width: floorToScreenPixels(progressFrame.width * progress), height: progressHeight)
if !self.progressForegroundNode.frame.origin.x.isZero {
transition.updateFrame(node: self.progressForegroundNode, frame: progressForegroundFrame, beginWithCurrentState: true)
} else {
self.progressForegroundNode.frame = progressForegroundFrame
}
let progressText: String
if self.isDone {
progressText = self.strings.Share_UploadDone
} else {
progressText = self.strings.Share_UploadProgress(Int(progress * 100.0)).string
}
self.progressTextNode.attributedText = NSAttributedString(string: progressText, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.theme.actionSheet.primaryTextColor)
let progressTextSize = self.progressTextNode.updateLayout(size)
let progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: progressFrame.minY - spacing - 9.0 - progressTextSize.height), size: progressTextSize)
self.progressTextNode.frame = progressTextFrame
let imageSide: CGFloat = 160.0
let imageSize = CGSize(width: imageSide, height: imageSide)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (progressTextFrame.minY - imageSize.height - 20.0)), size: imageSize)
self.animationNode.frame = animationFrame
self.animationNode.updateLayout(size: imageSize)
self.doneAnimationNode.frame = animationFrame
self.doneAnimationNode.updateLayout(size: imageSize)
self.contentOffsetUpdated?(-size.height + nodeHeight * 0.5, transition)
}
public func updateSelectedPeers(animated: Bool) {
}
}
@@ -0,0 +1,776 @@
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<PresentationTheme>
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, requiresStars: Int64?)]>()
private var _tick: Int = 0 {
didSet {
self.tick.set(self._tick)
}
}
private let tick = ValuePromise<Int>(0)
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, isMainApp: 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 {
let storyMode: ShareControllerPeerGridItem.ShareItem.StoryMode
if fromPublicChannel {
storyMode = .repostMessage
} else {
if !isMainApp {
storyMode = .createStory
} else {
storyMode = .repostStory
}
}
entries.append(SharePeerEntry(index: index, item: .story(mode: storyMode), theme: theme, strings: strings))
index += 1
}
var existingPeerIds: Set<EnginePeer.Id> = Set()
entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil, requiresPremiumForMessaging: false, requiresStars: nil), 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, requiresStars: nil), theme: theme, strings: strings))
existingPeerIds.insert(peer.peerId)
index += 1
}
}
for (peer, presence, requiresPremiumForMessaging, requiresStars) 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, requiresStars: requiresStars), 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)
}
@@ -0,0 +1,124 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
private func generateClearIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
}
final class ShareSearchBarNode: ASDisplayNode, UITextFieldDelegate {
private let backgroundNode: ASImageNode
private let searchIconNode: ASImageNode
private let textInputNode: TextFieldNode
private let clearButton: HighlightableButtonNode
private let inputInsets = UIEdgeInsets(top: 10.0, left: 26.0, bottom: 10.0, right: 10.0 + 16.0)
var textUpdated: ((String) -> Void)?
init(theme: PresentationTheme, placeholder: String) {
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputBackgroundColor)
self.searchIconNode = ASImageNode()
self.searchIconNode.isLayerBacked = true
self.searchIconNode.displaysAsynchronously = false
self.searchIconNode.displayWithoutProcessing = true
self.searchIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Share/SearchBarSearchIcon"), color: theme.actionSheet.inputPlaceholderColor)
self.clearButton = HighlightableButtonNode()
self.clearButton.imageNode.displaysAsynchronously = false
self.clearButton.imageNode.displayWithoutProcessing = true
self.clearButton.displaysAsynchronously = false
self.clearButton.setImage(generateClearIcon(color: theme.actionSheet.inputClearButtonColor), for: [])
self.clearButton.isHidden = true
self.textInputNode = TextFieldNode()
self.textInputNode.fixOffset = false
let textColor: UIColor = theme.actionSheet.inputTextColor
self.textInputNode.textField.font = Font.regular(16.0)
self.textInputNode.textField.textColor = textColor
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(16.0), NSAttributedString.Key.foregroundColor: textColor]
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textField.attributedPlaceholder = NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: theme.actionSheet.inputPlaceholderColor)
self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor
self.textInputNode.textField.returnKeyType = .search
self.textInputNode.textField.accessibilityTraits = .searchField
self.textInputNode.textField.spellCheckingType = .no
self.textInputNode.textField.autocorrectionType = .no
super.init()
self.textInputNode.textField.delegate = self
self.addSubnode(self.backgroundNode)
self.addSubnode(self.searchIconNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.clearButton)
self.textInputNode.textField.addTarget(self, action: #selector(self.textFieldDidChangeText), for: [.editingChanged])
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) {
let inputInsets = self.inputInsets
let textFieldHeight: CGFloat = 40.0
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: textFieldHeight))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
if let image = self.searchIconNode.image {
transition.updateFrame(node: self.searchIconNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + 8.0, y: backgroundFrame.minY + 13.0), size: image.size))
}
if let image = self.clearButton.image(for: []) {
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size))
}
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + UIScreenPixel), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
}
func updateTheme(_ theme: PresentationTheme) {
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputBackgroundColor)
self.searchIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Share/SearchBarSearchIcon"), color: theme.actionSheet.inputPlaceholderColor)
self.clearButton.setImage(generateClearIcon(color: theme.actionSheet.inputClearButtonColor), for: [])
let textColor: UIColor = theme.actionSheet.inputTextColor
self.textInputNode.textField.textColor = textColor
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(16.0), NSAttributedString.Key.foregroundColor: textColor]
self.textInputNode.textField.attributedPlaceholder = NSAttributedString(string: self.textInputNode.textField.attributedPlaceholder?.string ?? "", font: Font.regular(16.0), textColor: theme.actionSheet.inputPlaceholderColor)
self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor
}
func activateInput() {
self.textInputNode.textField.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.textField.resignFirstResponder()
}
@objc func textFieldDidChangeText() {
self.clearButton.isHidden = self.textInputNode.textField.text?.isEmpty ?? true
self.textUpdated?(self.textInputNode.textField.text ?? "")
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.deactivateInput()
return true
}
@objc func clearPressed() {
self.textInputNode.textField.text = ""
self.textFieldDidChangeText()
}
}
@@ -0,0 +1,895 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import SwiftSignalKit
import Display
import TelegramPresentationData
import MergeLists
import AccountContext
private let cancelFont = Font.regular(17.0)
private let subtitleFont = Font.regular(12.0)
private enum ShareSearchRecentEntryStableId: Hashable {
case topPeers
case peerId(EnginePeer.Id)
static func ==(lhs: ShareSearchRecentEntryStableId, rhs: ShareSearchRecentEntryStableId) -> Bool {
switch lhs {
case .topPeers:
if case .topPeers = rhs {
return true
} else {
return false
}
case let .peerId(peerId):
if case .peerId(peerId) = rhs {
return true
} else {
return false
}
}
}
}
private enum ShareSearchRecentEntry: Comparable, Identifiable {
case topPeers(PresentationTheme, PresentationStrings)
case peer(index: Int, theme: PresentationTheme, peer: EnginePeer, associatedPeer: EnginePeer?, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?, strings: PresentationStrings)
var stableId: ShareSearchRecentEntryStableId {
switch self {
case .topPeers:
return .topPeers
case let .peer(_, _, peer, _, _, _, _, _):
return .peerId(peer.id)
}
}
static func ==(lhs: ShareSearchRecentEntry, rhs: ShareSearchRecentEntry) -> Bool {
switch lhs {
case let .topPeers(lhsTheme, lhsStrings):
if case let .topPeers(rhsTheme, rhsStrings) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
return true
} else {
return false
}
case let .peer(lhsIndex, lhsTheme, lhsPeer, lhsAssociatedPeer, lhsPresence, lhsRequiresPremiumForMessaging, lhsRequiresStars, lhsStrings):
if case let .peer(rhsIndex, rhsTheme, rhsPeer, rhsAssociatedPeer, rhsPresence, rhsRequiresPremiumForMessaging, rhsRequiresStars, rhsStrings) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsStrings === rhsStrings && lhsTheme === rhsTheme && lhsPresence == rhsPresence && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging && lhsRequiresStars == rhsRequiresStars {
return true
} else {
return false
}
}
}
static func <(lhs: ShareSearchRecentEntry, rhs: ShareSearchRecentEntry) -> Bool {
switch lhs {
case .topPeers:
return true
case let .peer(lhsIndex, _, _, _, _, _, _, _):
switch rhs {
case .topPeers:
return false
case let .peer(rhsIndex, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
}
}
}
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
switch self {
case let .topPeers(theme, strings):
return ShareControllerRecentPeersGridItem(environment: environment, context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction)
case let .peer(_, theme, peer, associatedPeer, presence, requiresPremiumForMessaging, requiresStars, strings):
var peers: [EnginePeer.Id: EnginePeer] = [peer.id: peer]
if let associatedPeer = associatedPeer {
peers[associatedPeer.id] = associatedPeer
}
let peer = EngineRenderedPeer(peerId: peer.id, peers: peers, associatedMedia: [:])
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: requiresStars), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
}
}
}
private struct ShareSearchPeerEntry: Comparable, Identifiable {
let index: Int32
let peer: EngineRenderedPeer?
let presence: EnginePeer.Presence?
let requiresPremiumForMessaging: Bool
let requiresStars: Int64?
let theme: PresentationTheme
let strings: PresentationStrings
let isGlobal: Bool
var stableId: Int64 {
if let peer = self.peer {
return peer.peerId.toInt64()
} else {
return Int64(index)
}
}
static func ==(lhs: ShareSearchPeerEntry, rhs: ShareSearchPeerEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.presence != rhs.presence {
return false
}
if lhs.requiresPremiumForMessaging != rhs.requiresPremiumForMessaging {
return false
}
if lhs.requiresStars != rhs.requiresStars {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.isGlobal != rhs.isGlobal {
return false
}
return true
}
static func <(lhs: ShareSearchPeerEntry, rhs: ShareSearchPeerEntry) -> Bool {
return lhs.index < rhs.index
}
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
let sectionTitle: String?
if self.isGlobal {
sectionTitle = self.strings.Contacts_GlobalSearch.uppercased()
} else {
sectionTitle = nil
}
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil, requiresPremiumForMessaging: self.requiresPremiumForMessaging, requiresStars: self.requiresStars) }), controllerInteraction: interfaceInteraction, sectionTitle: sectionTitle, search: true)
}
}
private struct ShareSearchGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let animated: Bool
let crossFade: Bool
}
private func preparedGridEntryTransition(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, from fromEntries: [ShareSearchPeerEntry], to toEntries: [ShareSearchPeerEntry], interfaceInteraction: ShareControllerInteraction, crossFade: Bool) -> ShareSearchGridTransaction {
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 ShareSearchGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false, crossFade: crossFade)
}
private func preparedRecentEntryTransition(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, from fromEntries: [ShareSearchRecentEntry], to toEntries: [ShareSearchRecentEntry], interfaceInteraction: ShareControllerInteraction) -> ShareSearchGridTransaction {
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 ShareSearchGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false, crossFade: false)
}
final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
private let environment: ShareControllerEnvironment
private let context: ShareControllerAccountContext
private var theme: PresentationTheme
private let themePromise: Promise<PresentationTheme>
private let strings: PresentationStrings
private let controllerInteraction: ShareControllerInteraction
private var entries: [ShareSearchPeerEntry] = []
private var recentEntries: [ShareSearchRecentEntry] = []
private var enqueuedTransitions: [(ShareSearchGridTransaction, Bool)] = []
private var enqueuedRecentTransitions: [(ShareSearchGridTransaction, Bool)] = []
let contentGridNode: GridNode
private let recentGridNode: GridNode
var effectiveGridNode: GridNode {
if !self.recentGridNode.isHidden {
return self.recentGridNode
} else {
return self.contentGridNode
}
}
private let contentSeparatorNode: ASDisplayNode
private let searchNode: ShareSearchBarNode
private let cancelButtonNode: HighlightableButtonNode
private var contentDidBeginDragging: (() -> Void)?
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var cancel: (() -> Void)?
private var ensurePeerVisibleOnLayout: EnginePeer.Id?
private var validLayout: (CGSize, CGFloat)?
private var overrideGridOffsetTransition: ContainedViewLayoutTransition?
private let recentDisposable = MetaDisposable()
private let searchQuery = ValuePromise<String>("", ignoreRepeated: true)
private let searchDisposable = MetaDisposable()
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ShareControllerInteraction, recentPeers recentPeerList: [(peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool)]) {
self.environment = environment
self.context = context
self.theme = theme
self.themePromise = Promise<PresentationTheme>()
self.themePromise.set(.single(theme))
self.strings = strings
self.controllerInteraction = controllerInteraction
self.recentGridNode = GridNode()
self.contentGridNode = GridNode()
self.contentGridNode.isHidden = true
self.searchNode = ShareSearchBarNode(theme: theme, placeholder: strings.Common_Search)
self.cancelButtonNode = HighlightableButtonNode()
self.cancelButtonNode.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.actionSheet.controlAccentColor, for: [])
self.cancelButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.contentSeparatorNode = ASDisplayNode()
self.contentSeparatorNode.isLayerBacked = true
self.contentSeparatorNode.displaysAsynchronously = false
self.contentSeparatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
super.init()
self.addSubnode(self.recentGridNode)
self.addSubnode(self.contentGridNode)
self.addSubnode(self.searchNode)
self.addSubnode(self.cancelButtonNode)
self.addSubnode(self.contentSeparatorNode)
self.recentGridNode.scrollingInitiated = { [weak self] in
self?.contentDidBeginDragging?()
}
self.contentGridNode.scrollingInitiated = { [weak self] in
self?.contentDidBeginDragging?()
}
self.recentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
if let strongSelf = self, !strongSelf.recentGridNode.isHidden {
strongSelf.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
}
}
self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
if let strongSelf = self, !strongSelf.contentGridNode.isHidden {
strongSelf.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
}
}
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
let foundItems = combineLatest(self.searchQuery.get(), self.themePromise.get())
|> mapToSignal { query, theme -> Signal<([ShareSearchPeerEntry]?, Bool), NoError> in
if !query.isEmpty {
let accountPeer = context.stateManager.postbox.loadedPeerWithId(context.accountPeerId) |> take(1)
let foundLocalPeers = context.stateManager.postbox.searchPeers(query: query.lowercased())
let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> = .single(([], [], true))
|> then(
_internal_searchPeers(accountPeerId: context.accountPeerId, postbox: context.stateManager.postbox, network: context.stateManager.network, query: query, scope: .everywhere)
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
|> map { a, b -> ([FoundPeer], [FoundPeer], Bool) in
return (a, b, false)
}
)
struct FoundPeers {
var foundLocalPeers: [RenderedPeer]
var foundRemotePeers: ([FoundPeer], [FoundPeer], Bool)
}
let foundPeers = Promise<FoundPeers>()
foundPeers.set(combineLatest(
foundLocalPeers,
foundRemotePeers
)
|> map { foundLocalPeers, foundRemotePeers -> FoundPeers in
return FoundPeers(
foundLocalPeers: foundLocalPeers,
foundRemotePeers: foundRemotePeers
)
})
let peerRequiresPremiumForMessaging: Signal<[EnginePeer.Id: Bool], NoError>
peerRequiresPremiumForMessaging = foundPeers.get()
|> map { foundPeers -> Set<EnginePeer.Id> in
var result = Set<EnginePeer.Id>()
for peer in foundPeers.foundLocalPeers {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
for peer in foundPeers.foundRemotePeers.0 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
for peer in foundPeers.foundRemotePeers.1 {
if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
result.insert(user.id)
}
}
return result
}
|> distinctUntilChanged
|> mapToSignal { peerIds -> Signal<[EnginePeer.Id: Bool], NoError> in
if let context = context as? ShareControllerAppAccountContext {
context.context.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: Array(peerIds))
}
return context.engineData.subscribe(
EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging.init(id:))
)
)
}
return combineLatest(accountPeer, foundPeers.get(), peerRequiresPremiumForMessaging)
|> map { accountPeer, foundPeers, peerRequiresPremiumForMessaging -> ([ShareSearchPeerEntry]?, Bool) in
let foundLocalPeers = foundPeers.foundLocalPeers
let foundRemotePeers = foundPeers.foundRemotePeers
var entries: [ShareSearchPeerEntry] = []
var index: Int32 = 0
var existingPeerIds = Set<EnginePeer.Id>()
let lowercasedQuery = query.lowercased()
if strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) {
if !existingPeerIds.contains(accountPeer.id) {
existingPeerIds.insert(accountPeer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(accountPeer)), presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
for renderedPeer in foundLocalPeers {
if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id {
if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(renderedPeer.peerId)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
}
var isPlaceholder = false
if foundRemotePeers.2 {
isPlaceholder = true
for _ in 0 ..< 4 {
entries.append(ShareSearchPeerEntry(index: index, peer: nil, presence: nil, requiresPremiumForMessaging: false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
} else {
for foundPeer in foundRemotePeers.0 {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
}
}
for foundPeer in foundRemotePeers.1 {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true))
index += 1
}
}
}
return (entries, isPlaceholder)
}
} else {
return .single((nil, false))
}
}
let previousSearchItemsAndIsPlaceholder = Atomic<([ShareSearchPeerEntry]?, Bool)>(value: (nil, false))
self.searchDisposable.set((foundItems
|> deliverOnMainQueue).start(next: { [weak self] entriesAndIsPlaceholder in
if let strongSelf = self {
let (entries, isPlaceholder) = entriesAndIsPlaceholder
let previousEntries = previousSearchItemsAndIsPlaceholder.swap(entriesAndIsPlaceholder)
strongSelf.entries = entries ?? []
let firstTime = previousEntries.0 == nil
let crossFade = !firstTime && previousEntries.1 && !isPlaceholder
let transition = preparedGridEntryTransition(environment: environment, context: context, from: previousEntries.0 ?? [], to: entries ?? [], interfaceInteraction: controllerInteraction, crossFade: crossFade)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
if (previousEntries.0 == nil) != (entries == nil) {
if previousEntries.0 == nil {
strongSelf.recentGridNode.isHidden = true
strongSelf.contentGridNode.isHidden = false
strongSelf.transitionToContentGridLayout()
} else {
strongSelf.recentGridNode.isHidden = false
strongSelf.contentGridNode.isHidden = true
strongSelf.transitionToRecentGridLayout()
}
}
}
}))
self.searchNode.textUpdated = { [weak self] text in
self?.searchQuery.set(text)
}
let hasRecentPeers = _internal_recentPeers(accountPeerId: context.accountPeerId, postbox: context.stateManager.postbox)
|> map { value -> Bool in
switch value {
case let .peers(peers):
return !peers.isEmpty
case .disabled:
return false
}
}
|> distinctUntilChanged
let recentItems: Signal<[ShareSearchRecentEntry], NoError> = combineLatest(hasRecentPeers, self.themePromise.get())
|> map { hasRecentPeers, theme -> [ShareSearchRecentEntry] in
var recentItemList: [ShareSearchRecentEntry] = []
if hasRecentPeers {
recentItemList.append(.topPeers(theme, strings))
}
var index = 0
for (peer, requiresPremiumForMessaging) in recentPeerList {
if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer._asPeer()) {
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings))
index += 1
}
}
return recentItemList
}
let previousRecentItems = Atomic<[ShareSearchRecentEntry]?>(value: nil)
self.recentDisposable.set((recentItems
|> deliverOnMainQueue).start(next: { [weak self] entries in
if let strongSelf = self {
let previousEntries = previousRecentItems.swap(entries)
strongSelf.recentEntries = entries
let firstTime = previousEntries == nil
let transition = preparedRecentEntryTransition(environment: environment, context: context, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction)
strongSelf.enqueueRecentTransition(transition, firstTime: firstTime)
}
}))
}
deinit {
self.searchDisposable.dispose()
self.recentDisposable.dispose()
}
func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) {
self.ensurePeerVisibleOnLayout = peerId
}
func setDidBeginDragging(_ f: (() -> Void)?) {
self.contentDidBeginDragging = f
}
func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
self.contentOffsetUpdated = f
}
func activate() {
self.searchNode.activateInput()
}
func deactivate() {
self.searchNode.deactivateInput()
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.themePromise.set(.single(theme))
self.searchNode.updateTheme(theme)
self.contentSeparatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
self.cancelButtonNode.setTitle(self.strings.Common_Cancel, with: cancelFont, with: self.theme.actionSheet.controlAccentColor, for: [])
}
private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) {
let itemCount: Int
if self.contentGridNode.isHidden {
itemCount = self.recentEntries.count
} else {
itemCount = self.entries.count
}
let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
let minimalItemWidth: CGFloat = 70.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 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)
var scrollToItem: GridNodeScrollToItem?
if !self.contentGridNode.isHidden, let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
self.ensurePeerVisibleOnLayout = nil
if let index = self.entries.firstIndex(where: { $0.peer?.peerId == ensurePeerVisibleOnLayout }) {
scrollToItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false)
}
}
var scrollToRecentItem: GridNodeScrollToItem?
if !self.recentGridNode.isHidden, let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
self.ensurePeerVisibleOnLayout = nil
if let index = self.recentEntries.firstIndex(where: {
switch $0 {
case .topPeers:
return false
case let .peer(_, _, peer, _, _, _, _, _):
return peer.id == ensurePeerVisibleOnLayout
}
}) {
scrollToRecentItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false)
}
}
let gridSize = CGSize(width: size.width, height: size.height - 5.0)
self.recentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToRecentItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.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.recentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 5.0), size: gridSize))
self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.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: 5.0), size: gridSize))
if firstLayout {
self.animateIn()
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
}
}
private func transitionToRecentGridLayout(_ transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)) {
if let (size, bottomInset) = self.validLayout {
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size)
let offset = self.recentGridNode.scrollView.contentOffset.y - self.contentGridNode.scrollView.contentOffset.y
let gridSize = CGSize(width: size.width, height: size.height - 5.0)
self.recentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
transition.animatePositionAdditive(node: self.recentGridNode, offset: CGPoint(x: 0.0, y: offset))
}
}
private func transitionToContentGridLayout(_ transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)) {
if let (size, bottomInset) = self.validLayout {
let (gridTopInset, itemWidth) = self.calculateMetrics(size: size)
let offset = self.recentGridNode.scrollView.contentOffset.y - self.contentGridNode.scrollView.contentOffset.y
let gridSize = CGSize(width: size.width, height: size.height - 5.0)
self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 6.0, bottom: bottomInset, right: 6.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
transition.animatePositionAdditive(node: self.contentGridNode, offset: CGPoint(x: 0.0, y: -offset))
}
}
private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) {
let actualTransition = self.overrideGridOffsetTransition ?? transition
self.overrideGridOffsetTransition = nil
let titleAreaHeight: CGFloat = 64.0
let size = self.bounds.size
let rawTitleOffset = -titleAreaHeight - presentationLayout.contentOffset.y
let titleOffset = max(-titleAreaHeight, rawTitleOffset)
let cancelButtonSize = self.cancelButtonNode.measure(CGSize(width: 320.0, height: 100.0))
let cancelButtonFrame = CGRect(origin: CGPoint(x: size.width - cancelButtonSize.width - 12.0, y: titleOffset + 25.0), size: cancelButtonSize)
transition.updateFrame(node: self.cancelButtonNode, frame: cancelButtonFrame)
let searchNodeFrame = CGRect(origin: CGPoint(x: 16.0, y: titleOffset + 16.0), size: CGSize(width: cancelButtonFrame.minX - 16.0 - 10.0, height: 40.0))
transition.updateFrame(node: self.searchNode, frame: searchNodeFrame)
self.searchNode.updateLayout(width: searchNodeFrame.size.width, transition: transition)
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleOffset + titleAreaHeight + 5.0), 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 animateIn() {
}
func updateSelectedPeers(animated: Bool) {
self.contentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
itemNode.updateSelection(animated: true)
}
}
self.recentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
itemNode.updateSelection(animated: true)
} else if let itemNode = itemNode as? ShareControllerRecentPeersGridItemNode {
itemNode.updateSelection(animated: true)
}
}
}
@objc func cancelPressed() {
self.cancel?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let nodes: [ASDisplayNode] = [self.searchNode, self.cancelButtonNode]
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)
}
private func enqueueTransition(_ transition: ShareSearchGridTransaction, 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)
}
if transition.crossFade {
if let snapshotView = self.contentGridNode.view.snapshotView(afterScreenUpdates: false) {
self.contentGridNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentGridNode.view)
snapshotView.frame = self.contentGridNode.frame
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
}
self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: true), completion: { _ in })
}
}
private func enqueueRecentTransition(_ transition: ShareSearchGridTransaction, firstTime: Bool) {
self.enqueuedRecentTransitions.append((transition, firstTime))
if self.validLayout != nil {
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
}
}
private func dequeueRecentTransition() {
if let (transition, _) = self.enqueuedRecentTransitions.first {
self.enqueuedRecentTransitions.remove(at: 0)
var itemTransition: ContainedViewLayoutTransition = .immediate
if transition.animated {
itemTransition = .animated(duration: 0.3, curve: .spring)
}
self.recentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
}
}
func frameForPeerId(_ peerId: EnginePeer.Id) -> CGRect? {
var node: ASDisplayNode?
if !self.recentGridNode.isHidden {
self.recentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId {
node = itemNode
}
}
} else {
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 animateIn(peerId: EnginePeer.Id, scrollDelta: CGFloat) -> CGRect? {
self.searchNode.alpha = 1.0
self.searchNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.searchNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.cancelButtonNode.alpha = 1.0
self.cancelButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.cancelButtonNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
let gridNode = self.effectiveGridNode
gridNode.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.searchNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset))
gridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: gridNode.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
gridNode.alpha = 1.0
gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false)
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.searchNode.alpha = 0.0
self.searchNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.searchNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.cancelButtonNode.alpha = 0.0
self.cancelButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.cancelButtonNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
let gridNode = self.effectiveGridNode
gridNode.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.searchNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset))
gridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: gridNode.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
gridNode.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()
})
gridNode.alpha = 0.0
return sourceFrame
} else {
return nil
}
}
}
@@ -0,0 +1,170 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import TelegramPresentationData
import TelegramStringFormatting
import SelectablePeerNode
import PeerPresenceStatusManager
import AccountContext
import ShimmerEffect
import ComponentFlow
import EmojiStatusComponent
import AvatarNode
final class ShareTopicGridItem: GridItem {
let environment: ShareControllerEnvironment
let context: ShareControllerAccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let basePeer: EnginePeer
let peer: EngineRenderedPeer?
let id: Int64
let threadInfo: MessageHistoryThreadData?
let controllerInteraction: ShareControllerInteraction
let section: GridSection?
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, basePeer: EnginePeer, peer: EngineRenderedPeer?, id: Int64, threadInfo: MessageHistoryThreadData?, controllerInteraction: ShareControllerInteraction) {
self.environment = environment
self.context = context
self.basePeer = basePeer
self.theme = theme
self.strings = strings
self.peer = peer
self.id = id
self.threadInfo = threadInfo
self.controllerInteraction = controllerInteraction
self.section = nil
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
return ShareTopicGridItemNode()
}
func update(node: GridItemNode) {
}
}
final class ShareTopicGridItemNode: GridItemNode {
private var currentState: (ShareControllerAccountContext, PresentationTheme, PresentationStrings, EngineRenderedPeer?, MessageHistoryThreadData?)?
private let iconView: ComponentView<Empty>
private let textNode: ImmediateTextNode
private var avatarNode: AvatarNode?
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var currentItem: ShareTopicGridItem?
var id: Int64? {
return self.currentItem?.id
}
override init() {
self.iconView = ComponentView<Empty>()
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 2
self.textNode.textAlignment = .center
super.init()
self.addSubnode(self.textNode)
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
}
@objc private func tapped() {
if let item = self.currentItem, let peer = item.peer {
if let threadInfo = item.threadInfo {
item.controllerInteraction.selectTopic(peer, item.id, threadInfo)
} else {
item.controllerInteraction.selectTopic(EngineRenderedPeer(peer: item.basePeer), item.id, nil)
}
}
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
let rect = absoluteRect
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
super.updateLayout(item: item, size: size, isVisible: isVisible, synchronousLoads: synchronousLoads)
guard let item = item as? ShareTopicGridItem else {
return
}
self.currentItem = item
if let threadInfo = item.threadInfo {
self.textNode.attributedText = NSAttributedString(string: threadInfo.info.title, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor)
let iconContent: EmojiStatusComponent.Content
if let fileId = threadInfo.info.icon {
iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 96.0, height: 96.0), placeholderColor: item.theme.actionSheet.disabledActionTextColor, themeColor: item.theme.actionSheet.primaryTextColor, loopMode: .count(0))
} else {
iconContent = .topic(title: String(threadInfo.info.title.prefix(1)), color: threadInfo.info.iconColor, size: CGSize(width: 64.0, height: 64.0))
}
let iconSize = self.iconView.update(
transition: .easeInOut(duration: 0.2),
component: AnyComponent(EmojiStatusComponent(
postbox: item.context.stateManager.postbox,
energyUsageSettings: item.environment.energyUsageSettings,
resolveInlineStickers: item.context.resolveInlineStickers,
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
content: iconContent,
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: CGSize(width: 54.0, height: 54.0)
)
if let iconComponentView = self.iconView.view {
if iconComponentView.superview == nil {
self.view.addSubview(iconComponentView)
}
iconComponentView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize)
}
} else if let peer = item.peer, let mainPeer = peer.chatMainPeer {
self.textNode.attributedText = NSAttributedString(string: mainPeer.compactDisplayTitle, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor)
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0))
self.avatarNode = avatarNode
self.addSubnode(avatarNode)
}
let iconSize = CGSize(width: 54.0, height: 54.0)
avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize)
avatarNode.updateSize(size: iconSize)
avatarNode.setPeer(accountPeerId: item.context.accountPeerId, postbox: item.context.stateManager.postbox, network: item.context.stateManager.network, contentSettings: ContentSettings.default, theme: item.theme, peer: mainPeer, overrideImage: nil, emptyColor: item.theme.list.mediaPlaceholderColor, clipStyle: .round, synchronousLoad: false)
}
let textSize = self.textNode.updateLayout(size)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: 4.0 + 60.0 + 4.0), size: textSize)
self.textNode.frame = textFrame
}
override func layout() {
super.layout()
}
}
@@ -0,0 +1,479 @@
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)
private struct ShareTopicEntry: Comparable, Identifiable {
let index: Int32
let basePeer: EnginePeer
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.basePeer != rhs.basePeer {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.id != rhs.id {
return false
}
if lhs.threadData != rhs.threadData {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
static func <(lhs: ShareTopicEntry, rhs: ShareTopicEntry) -> Bool {
return lhs.index < rhs.index
}
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
return ShareTopicGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, basePeer: self.basePeer, 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(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, 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(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)
}
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 + 16.0, 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: self.buttonNode.frame.width, height: 56.0)
}
}
final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode {
func setEnsurePeerVisibleOnLayout(_ peerId: TelegramCore.EnginePeer.Id?) {
}
func updateSelectedPeers(animated: Bool) {
}
private let environment: ShareControllerEnvironment
private let context: ShareControllerAccountContext
private var theme: PresentationTheme
private let themePromise: Promise<PresentationTheme>
private let strings: PresentationStrings
private let controllerInteraction: ShareControllerInteraction
private let disposable = MetaDisposable()
private var entries: [ShareTopicEntry] = []
private var enqueuedTransitions: [(ShareGridTransaction, Bool)] = []
let contentGridNode: GridNode
private let headerNode: ASDisplayNode
private let contentTitleNode: ASTextNode
private let contentSubtitleNode: ASTextNode
private let backNode: CancelButtonNode
private var contentDidBeginDragging: (() -> Void)?
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var validLayout: (CGSize, CGFloat)?
private var overrideGridOffsetTransition: ContainedViewLayoutTransition?
let topicsValue = Promise<[EngineChatList.Item]>()
var backPressed: () -> Void = {}
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, topics: Signal<EngineChatList, NoError>, controllerInteraction: ShareControllerInteraction) {
self.environment = environment
self.context = context
self.theme = theme
self.themePromise = Promise()
self.themePromise.set(.single(theme))
self.strings = strings
self.controllerInteraction = controllerInteraction
self.topicsValue.set(topics
|> map {
return $0.items
})
let items: Signal<[ShareTopicEntry], NoError> = (combineLatest(self.topicsValue.get(), self.themePromise.get()))
|> map { topics, theme -> [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, basePeer: peer, peer: EngineRenderedPeer(peer: peer), id: threadId, threadData: threadData, theme: theme, strings: strings))
index += 1
} else if case .chatList = topic.index {
entries.append(ShareTopicEntry(index: index, basePeer: peer, peer: topic.renderedPeer, id: topic.renderedPeer.peerId.toInt64(), threadData: nil, theme: theme, strings: strings))
index += 1
}
}
return entries
}
self.contentGridNode = GridNode()
self.headerNode = ASDisplayNode()
self.contentTitleNode = ASTextNode()
self.contentTitleNode.maximumNumberOfLines = 1
self.contentTitleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor)
self.contentTitleNode.textAlignment = .center
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.headerNode)
self.headerNode.addSubnode(self.contentTitleNode)
self.headerNode.addSubnode(self.contentSubtitleNode)
self.headerNode.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(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.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 setDidBeginDragging(_ f: (() -> Void)?) {
self.contentDidBeginDragging = f
}
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, scrollDelta: CGFloat) {
self.headerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
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.animatePosition(from: CGPoint(x: 0.0, y: scrollDelta), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
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, scrollDelta: CGFloat, completion: @escaping () -> Void = {}) {
self.headerNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
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.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: scrollDelta), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.contentGridNode.alpha = 0.0
self.contentGridNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, 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)
}
}
public func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.themePromise.set(.single(theme))
self.contentTitleNode.attributedText = NSAttributedString(string: self.contentTitleNode.attributedText?.string ?? "", font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor)
self.contentSubtitleNode.attributedText = NSAttributedString(string: self.contentSubtitleNode.attributedText?.string ?? "", font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor)
}
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 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 backSize = self.backNode.measure(CGSize(width: size.width, height: 56.0))
let backFrame = CGRect(origin: CGPoint(x: 20.0, y: 6.0), size: CGSize(width: backSize.width, height: 56.0))
transition.updateFrame(node: self.backNode, frame: backFrame)
let titleSize = self.contentTitleNode.measure(CGSize(width: size.width - (backSize.width * 2.0 + 40.0), height: size.height))
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.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: 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)
self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition)
}
}