mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-06-08 19:13:56 +02:00
Update Ghostgram features
This commit is contained in:
+445
@@ -0,0 +1,445 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import SwiftSignalKit
|
||||
import Markdown
|
||||
import ContextUI
|
||||
|
||||
public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
||||
private var presentationData: PresentationData
|
||||
private(set) var action: ContextMenuActionItem
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let actionSelected: (ContextMenuActionResult) -> Void
|
||||
private let requestLayout: () -> Void
|
||||
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let statusNode: ImmediateTextNode?
|
||||
private let iconNode: ASImageNode
|
||||
private let titleIconNode: ASImageNode
|
||||
private let badgeBackgroundNode: ASImageNode
|
||||
private let badgeTextNode: ImmediateTextNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var iconDisposable: Disposable?
|
||||
|
||||
private var pointerInteraction: PointerInteraction?
|
||||
|
||||
public var isActionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.action = action
|
||||
self.getController = getController
|
||||
self.actionSelected = actionSelected
|
||||
self.requestLayout = requestLayout
|
||||
self.requestUpdateAction = requestUpdateAction
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
|
||||
let boldTextFont = Font.semibold(presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallBoldTextFont = Font.semibold(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isAccessibilityElement = false
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isAccessibilityElement = false
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.isAccessibilityElement = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
let textColor: UIColor
|
||||
switch action.textColor {
|
||||
case .primary:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
textColor = presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
let titleFont: UIFont
|
||||
let titleBoldFont: UIFont
|
||||
switch action.textFont {
|
||||
case .regular:
|
||||
titleFont = textFont
|
||||
titleBoldFont = boldTextFont
|
||||
case .small:
|
||||
titleFont = smallTextFont
|
||||
titleBoldFont = smallBoldTextFont
|
||||
case let .custom(customFont, _, _):
|
||||
titleFont = customFont
|
||||
titleBoldFont = customFont
|
||||
}
|
||||
|
||||
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
|
||||
if action.parseMarkdown {
|
||||
let attributedText = parseMarkdownIntoAttributedString(action.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: textColor), linkAttribute: { _ in
|
||||
return nil
|
||||
}))
|
||||
self.textNode.attributedText = attributedText
|
||||
} else {
|
||||
self.textNode.attributedText = NSAttributedString(string: action.text, font: titleFont, textColor: textColor)
|
||||
}
|
||||
|
||||
switch action.textLayout {
|
||||
case .singleLine:
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
self.statusNode = nil
|
||||
case .twoLinesMax:
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.statusNode = nil
|
||||
case let .secondLineWithValue(value):
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
let statusNode = ImmediateTextNode()
|
||||
statusNode.isAccessibilityElement = false
|
||||
statusNode.isUserInteractionEnabled = false
|
||||
statusNode.displaysAsynchronously = false
|
||||
statusNode.attributedText = NSAttributedString(string: value, font: subtitleFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||
statusNode.maximumNumberOfLines = 1
|
||||
self.statusNode = statusNode
|
||||
case let .secondLineWithAttributedValue(value):
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
let statusNode = ImmediateTextNode()
|
||||
statusNode.isAccessibilityElement = false
|
||||
statusNode.isUserInteractionEnabled = false
|
||||
statusNode.displaysAsynchronously = false
|
||||
|
||||
let mutableString = value.mutableCopy() as! NSMutableAttributedString
|
||||
mutableString.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.secondaryColor, range: NSRange(location: 0, length: mutableString.length))
|
||||
mutableString.addAttribute(.font, value: subtitleFont, range: NSRange(location: 0, length: mutableString.length))
|
||||
statusNode.attributedText = mutableString
|
||||
statusNode.maximumNumberOfLines = 1
|
||||
self.statusNode = statusNode
|
||||
case .multiline:
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.statusNode = nil
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isAccessibilityElement = false
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
if let iconSource = action.iconSource {
|
||||
self.iconNode.clipsToBounds = true
|
||||
self.iconNode.contentMode = iconSource.contentMode
|
||||
self.iconNode.cornerRadius = iconSource.cornerRadius
|
||||
} else {
|
||||
self.iconNode.image = action.icon(presentationData.theme)
|
||||
}
|
||||
|
||||
self.titleIconNode = ASImageNode()
|
||||
self.titleIconNode.isAccessibilityElement = false
|
||||
self.titleIconNode.displaysAsynchronously = false
|
||||
self.titleIconNode.displayWithoutProcessing = true
|
||||
self.titleIconNode.isUserInteractionEnabled = false
|
||||
self.titleIconNode.image = action.textIcon(presentationData.theme)
|
||||
|
||||
self.badgeBackgroundNode = ASImageNode()
|
||||
self.badgeBackgroundNode.isAccessibilityElement = false
|
||||
self.badgeBackgroundNode.displaysAsynchronously = false
|
||||
self.badgeBackgroundNode.displayWithoutProcessing = true
|
||||
self.badgeBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.badgeTextNode = ImmediateTextNode()
|
||||
if let badge = action.badge {
|
||||
let badgeFillColor: UIColor
|
||||
let badgeForegroundColor: UIColor
|
||||
switch badge.color {
|
||||
case .accent:
|
||||
badgeForegroundColor = presentationData.theme.contextMenu.badgeForegroundColor
|
||||
badgeFillColor = presentationData.theme.contextMenu.badgeFillColor
|
||||
case .inactive:
|
||||
badgeForegroundColor = presentationData.theme.contextMenu.badgeInactiveForegroundColor
|
||||
badgeFillColor = presentationData.theme.contextMenu.badgeInactiveFillColor
|
||||
}
|
||||
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: badgeFillColor)
|
||||
self.badgeTextNode.attributedText = NSAttributedString(string: badge.value, font: Font.regular(14.0), textColor: badgeForegroundColor)
|
||||
}
|
||||
self.badgeTextNode.isAccessibilityElement = false
|
||||
self.badgeTextNode.isUserInteractionEnabled = false
|
||||
self.badgeTextNode.displaysAsynchronously = false
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.buttonNode.isAccessibilityElement = true
|
||||
self.buttonNode.accessibilityLabel = action.text
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.statusNode.flatMap(self.addSubnode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.badgeBackgroundNode)
|
||||
self.addSubnode(self.badgeTextNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
if let _ = self.titleIconNode.image {
|
||||
self.addSubnode(self.titleIconNode)
|
||||
}
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highligted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highligted {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.isUserInteractionEnabled = self.action.action != nil
|
||||
|
||||
if let iconSource = action.iconSource {
|
||||
self.iconDisposable = (iconSource.signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.iconNode.image = image
|
||||
}).strict()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconDisposable?.dispose()
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.75
|
||||
}
|
||||
}, willExit: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func updateLayout(constrainedWidth: CGFloat, previous: ContextActionSibling, next: ContextActionSibling) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||
let sideInset: CGFloat = 16.0
|
||||
let iconSideInset: CGFloat = 12.0
|
||||
let verticalInset: CGFloat = 12.0
|
||||
|
||||
let iconSize: CGSize
|
||||
if let iconSource = self.action.iconSource {
|
||||
iconSize = iconSource.size
|
||||
} else {
|
||||
iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize()
|
||||
}
|
||||
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
var rightTextInset: CGFloat = sideInset
|
||||
if !iconSize.width.isZero {
|
||||
rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset
|
||||
}
|
||||
if let iconSize = self.titleIconNode.image?.size {
|
||||
rightTextInset += iconSize.width + 10.0
|
||||
}
|
||||
|
||||
let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude))
|
||||
let badgeInset: CGFloat = 4.0
|
||||
|
||||
let badgeSize: CGSize
|
||||
let badgeWidthSpace: CGFloat
|
||||
let badgeSpacing: CGFloat = 10.0
|
||||
if badgeTextSize.width.isZero {
|
||||
badgeSize = CGSize()
|
||||
badgeWidthSpace = 0.0
|
||||
} else {
|
||||
badgeSize = CGSize(width: max(18.0, badgeTextSize.width + badgeInset * 2.0), height: 18.0)
|
||||
badgeWidthSpace = badgeSize.width + badgeSpacing
|
||||
}
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset - badgeWidthSpace, height: .greatestFiniteMagnitude))
|
||||
let statusSize = self.statusNode?.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset - badgeWidthSpace, height: .greatestFiniteMagnitude)) ?? CGSize()
|
||||
|
||||
if !statusSize.width.isZero, let statusNode = self.statusNode {
|
||||
let verticalSpacing: CGFloat = 2.0
|
||||
let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height
|
||||
return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset + badgeWidthSpace, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
|
||||
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||
transition.updateFrameAdditive(node: statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: statusSize))
|
||||
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
|
||||
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)
|
||||
transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeFrame.width - badgeTextSize.width) / 2.0), y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0)), size: badgeTextSize))
|
||||
|
||||
if !iconSize.width.isZero {
|
||||
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if let iconSize = self.titleIconNode.image?.size {
|
||||
transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: self.textNode.frame.maxX + 7.0, y: floorToScreenPixels(self.textNode.frame.midY - iconSize.height / 2.0)), size: iconSize))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return (CGSize(width: textSize.width + sideInset + rightTextInset + badgeWidthSpace, height: verticalInset * 2.0 + textSize.height), { size, transition in
|
||||
let verticalOrigin = floor((size.height - textSize.height) / 2.0)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||
|
||||
if !iconSize.width.isZero {
|
||||
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
|
||||
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)
|
||||
transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeFrame.width - badgeTextSize.width) / 2.0), y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0)), size: badgeTextSize))
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if let iconSize = self.titleIconNode.image?.size {
|
||||
transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: self.textNode.frame.maxX + 7.0, y: floorToScreenPixels(self.textNode.frame.midY - iconSize.height / 2.0)), size: iconSize))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTheme(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
|
||||
let textColor: UIColor
|
||||
switch action.textColor {
|
||||
case .primary:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
textColor = presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let titleFont: UIFont
|
||||
switch self.action.textFont {
|
||||
case .regular:
|
||||
titleFont = textFont
|
||||
case .small:
|
||||
titleFont = smallTextFont
|
||||
case let .custom(customFont, _, _):
|
||||
titleFont = customFont
|
||||
}
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.action.text, font: titleFont, textColor: textColor)
|
||||
|
||||
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
|
||||
switch self.action.textLayout {
|
||||
case let .secondLineWithValue(value):
|
||||
self.statusNode?.attributedText = NSAttributedString(string: value, font: subtitleFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||
case let .secondLineWithAttributedValue(value):
|
||||
let mutableString = value.mutableCopy() as! NSMutableAttributedString
|
||||
mutableString.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.secondaryColor, range: NSRange(location: 0, length: mutableString.length))
|
||||
mutableString.addAttribute(.font, value: subtitleFont, range: NSRange(location: 0, length: mutableString.length))
|
||||
self.statusNode?.attributedText = mutableString
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.action.iconSource == nil {
|
||||
self.iconNode.image = self.action.icon(presentationData.theme)
|
||||
}
|
||||
|
||||
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.contextMenu.badgeFillColor)
|
||||
self.badgeTextNode.attributedText = NSAttributedString(string: self.badgeTextNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: presentationData.theme.contextMenu.badgeForegroundColor)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.performAction()
|
||||
}
|
||||
|
||||
func updateAction(item: ContextMenuActionItem) {
|
||||
self.action = item
|
||||
|
||||
let textColor: UIColor
|
||||
switch self.action.textColor {
|
||||
case .primary:
|
||||
textColor = self.presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
textColor = self.presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
textColor = self.presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let titleFont: UIFont
|
||||
switch self.action.textFont {
|
||||
case .regular:
|
||||
titleFont = textFont
|
||||
case .small:
|
||||
titleFont = smallTextFont
|
||||
case let .custom(customFont, _, _):
|
||||
titleFont = customFont
|
||||
}
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.action.text, font: titleFont, textColor: textColor)
|
||||
|
||||
if self.action.iconSource == nil {
|
||||
self.iconNode.image = self.action.icon(self.presentationData.theme)
|
||||
}
|
||||
|
||||
self.requestLayout()
|
||||
}
|
||||
|
||||
private var performedAction = false
|
||||
public func performAction() {
|
||||
guard let controller = self.getController(), !self.performedAction else {
|
||||
return
|
||||
}
|
||||
self.action.action?(ContextMenuActionItem.Action(
|
||||
controller: controller,
|
||||
dismissWithResult: { [weak self] result in
|
||||
self?.performedAction = true
|
||||
self?.actionSelected(result)
|
||||
},
|
||||
updateAction: { [weak self] id, updatedAction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestUpdateAction(id, updatedAction)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
public func setIsHighlighted(_ value: Bool) {
|
||||
if value && self.buttonNode.isUserInteractionEnabled {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
public func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
|
||||
return self
|
||||
}
|
||||
}
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import Markdown
|
||||
import AppBundle
|
||||
import TextFormat
|
||||
import TextNodeWithEntities
|
||||
import SwiftSignalKit
|
||||
import ContextUI
|
||||
import GlassBackgroundComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecognizer {
|
||||
var updateLocation: ((CGPoint, Bool) -> Void)?
|
||||
var completed: ((Bool) -> Void)?
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
self.updateLocation?(touches.first!.location(in: self.view), false)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
self.updateLocation?(touches.first!.location(in: self.view), true)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.completed?(true)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.completed?(false)
|
||||
}
|
||||
}
|
||||
|
||||
private enum ContextItemNode {
|
||||
case action(ContextActionNode)
|
||||
case custom(ContextMenuCustomNode)
|
||||
case itemSeparator(ASDisplayNode)
|
||||
case separator(ASDisplayNode)
|
||||
}
|
||||
|
||||
private final class InnerActionsContainerNode: ASDisplayNode {
|
||||
private let blurBackground: Bool
|
||||
private let presentationData: PresentationData
|
||||
private let containerNode: ASDisplayNode
|
||||
private var effectView: UIVisualEffectView?
|
||||
private var itemNodes: [ContextItemNode]
|
||||
private let feedbackTap: () -> Void
|
||||
|
||||
private(set) var gesture: UIGestureRecognizer?
|
||||
private var currentHighlightedActionNode: ContextActionNodeProtocol?
|
||||
|
||||
var panSelectionGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
if self.panSelectionGestureEnabled != oldValue, let gesture = self.gesture {
|
||||
gesture.isEnabled = self.panSelectionGestureEnabled
|
||||
|
||||
self.itemNodes.forEach({ itemNode in
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
actionNode.isUserInteractionEnabled = !self.panSelectionGestureEnabled
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) {
|
||||
self.presentationData = presentationData
|
||||
self.feedbackTap = feedbackTap
|
||||
self.blurBackground = blurBackground
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.clipsToBounds = true
|
||||
self.containerNode.cornerRadius = 14.0
|
||||
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
||||
|
||||
var requestUpdateAction: ((AnyHashable, ContextMenuActionItem) -> Void)?
|
||||
|
||||
var itemNodes: [ContextItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
switch items[i] {
|
||||
case let .action(action):
|
||||
itemNodes.append(.action(ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, requestUpdateAction: { id, action in
|
||||
requestUpdateAction?(id, action)
|
||||
})))
|
||||
case let .custom(item, _):
|
||||
let itemNode = item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected)
|
||||
itemNodes.append(.custom(itemNode))
|
||||
case .separator:
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
itemNodes.append(.separator(separatorNode))
|
||||
}
|
||||
}
|
||||
|
||||
self.itemNodes = itemNodes
|
||||
|
||||
super.init()
|
||||
|
||||
requestUpdateAction = { [weak self] id, action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
loop: for itemNode in strongSelf.itemNodes {
|
||||
switch itemNode {
|
||||
case let .action(contextActionNode):
|
||||
if contextActionNode.action.id == id {
|
||||
contextActionNode.updateAction(item: action)
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.itemNodes.forEach({ itemNode in
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
actionNode.isUserInteractionEnabled = false
|
||||
self.containerNode.addSubnode(actionNode)
|
||||
case let .custom(itemNode):
|
||||
self.containerNode.addSubnode(itemNode)
|
||||
case let .itemSeparator(separatorNode):
|
||||
self.containerNode.addSubnode(separatorNode)
|
||||
case let .separator(separatorNode):
|
||||
self.containerNode.addSubnode(separatorNode)
|
||||
}
|
||||
})
|
||||
|
||||
let gesture = ContextActionsSelectionGestureRecognizer(target: nil, action: nil)
|
||||
self.gesture = gesture
|
||||
gesture.updateLocation = { [weak self] point, moved in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var actionNode = strongSelf.actionNode(at: point)
|
||||
if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled {
|
||||
actionNode = nil
|
||||
}
|
||||
if actionNode !== strongSelf.currentHighlightedActionNode {
|
||||
if actionNode != nil, moved {
|
||||
strongSelf.feedbackTap()
|
||||
}
|
||||
strongSelf.currentHighlightedActionNode?.setIsHighlighted(false)
|
||||
}
|
||||
strongSelf.currentHighlightedActionNode = actionNode
|
||||
actionNode?.setIsHighlighted(true)
|
||||
}
|
||||
gesture.completed = { [weak self] performAction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let currentHighlightedActionNode = strongSelf.currentHighlightedActionNode {
|
||||
strongSelf.currentHighlightedActionNode = nil
|
||||
currentHighlightedActionNode.setIsHighlighted(false)
|
||||
if performAction {
|
||||
currentHighlightedActionNode.performAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(gesture)
|
||||
gesture.isEnabled = self.panSelectionGestureEnabled
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var minActionsWidth: CGFloat = 250.0
|
||||
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
|
||||
minActionsWidth = minimalWidth
|
||||
}
|
||||
|
||||
switch widthClass {
|
||||
case .compact:
|
||||
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
|
||||
if let effectView = self.effectView {
|
||||
self.effectView = nil
|
||||
effectView.removeFromSuperview()
|
||||
}
|
||||
case .regular:
|
||||
if self.effectView == nil {
|
||||
let effectView: UIVisualEffectView
|
||||
if #available(iOS 13.0, *) {
|
||||
if self.presentationData.theme.rootController.keyboardColor == .dark {
|
||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialDark))
|
||||
} else {
|
||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialLight))
|
||||
}
|
||||
} else {
|
||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
||||
}
|
||||
self.effectView = effectView
|
||||
self.containerNode.view.insertSubview(effectView, at: 0)
|
||||
}
|
||||
}
|
||||
minActionsWidth = min(minActionsWidth, constrainedWidth)
|
||||
let separatorHeight: CGFloat = 8.0
|
||||
|
||||
var maxWidth: CGFloat = 0.0
|
||||
var contentHeight: CGFloat = 0.0
|
||||
var heightsAndCompletions: [(CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)?] = []
|
||||
for i in 0 ..< self.itemNodes.count {
|
||||
switch self.itemNodes[i] {
|
||||
case let .action(itemNode):
|
||||
let previous: ContextActionSibling
|
||||
let next: ContextActionSibling
|
||||
if i == 0 {
|
||||
previous = .none
|
||||
} else if case .separator = self.itemNodes[i - 1] {
|
||||
previous = .separator
|
||||
} else {
|
||||
previous = .item
|
||||
}
|
||||
if i == self.itemNodes.count - 1 {
|
||||
next = .none
|
||||
} else if case .separator = self.itemNodes[i + 1] {
|
||||
next = .separator
|
||||
} else {
|
||||
next = .item
|
||||
}
|
||||
let (minSize, complete) = itemNode.updateLayout(constrainedWidth: constrainedWidth, previous: previous, next: next)
|
||||
maxWidth = max(maxWidth, minSize.width)
|
||||
heightsAndCompletions.append((minSize.height, complete))
|
||||
contentHeight += minSize.height
|
||||
case let .custom(itemNode):
|
||||
let (minSize, complete) = itemNode.updateLayout(constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight)
|
||||
maxWidth = max(maxWidth, minSize.width)
|
||||
heightsAndCompletions.append((minSize.height, complete))
|
||||
contentHeight += minSize.height
|
||||
case .itemSeparator:
|
||||
heightsAndCompletions.append(nil)
|
||||
contentHeight += UIScreenPixel
|
||||
case .separator:
|
||||
heightsAndCompletions.append(nil)
|
||||
contentHeight += separatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = max(maxWidth, minActionsWidth)
|
||||
|
||||
var verticalOffset: CGFloat = 0.0
|
||||
for i in 0 ..< heightsAndCompletions.count {
|
||||
switch self.itemNodes[i] {
|
||||
case let .action(itemNode):
|
||||
if let (itemHeight, itemCompletion) = heightsAndCompletions[i] {
|
||||
let itemSize = CGSize(width: maxWidth, height: itemHeight)
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
|
||||
itemCompletion(itemSize, transition)
|
||||
verticalOffset += itemHeight
|
||||
}
|
||||
case let .custom(itemNode):
|
||||
if let (itemHeight, itemCompletion) = heightsAndCompletions[i] {
|
||||
let itemSize = CGSize(width: maxWidth, height: itemHeight)
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
|
||||
itemCompletion(itemSize, transition)
|
||||
verticalOffset += itemHeight
|
||||
}
|
||||
case let .itemSeparator(separatorNode):
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: maxWidth, height: UIScreenPixel)))
|
||||
verticalOffset += UIScreenPixel
|
||||
case let .separator(separatorNode):
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + floorToScreenPixels((separatorHeight - UIScreenPixel) * 0.5)), size: CGSize(width: maxWidth, height: UIScreenPixel)))
|
||||
verticalOffset += separatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
let size = CGSize(width: maxWidth, height: verticalOffset)
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: bounds)
|
||||
if let effectView = self.effectView {
|
||||
transition.updateFrame(view: effectView, frame: bounds)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
for itemNode in self.itemNodes {
|
||||
switch itemNode {
|
||||
case let .action(action):
|
||||
action.updateTheme(presentationData: presentationData)
|
||||
case let .custom(item):
|
||||
item.updateTheme(presentationData: presentationData)
|
||||
case let .separator(separator):
|
||||
separator.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
||||
case let .itemSeparator(itemSeparator):
|
||||
itemSeparator.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
}
|
||||
}
|
||||
|
||||
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
|
||||
for itemNode in self.itemNodes {
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
if actionNode.frame.contains(point) {
|
||||
return actionNode
|
||||
}
|
||||
case let .custom(node):
|
||||
if let node = node as? ContextActionNodeProtocol, node.frame.contains(point) {
|
||||
return node.actionNode(at: self.convert(point, to: node))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
||||
private let presentationData: PresentationData
|
||||
private var background: (container: GlassBackgroundContainerView, background: GlassBackgroundView)?
|
||||
private let textNode: TextNodeWithEntities
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
private let iconNode: ASImageNode
|
||||
private let placeholderNode: ASDisplayNode
|
||||
|
||||
var tip: ContextController.Tip
|
||||
|
||||
private let text: String
|
||||
private var arguments: TextNodeWithEntities.Arguments?
|
||||
private var file: TelegramMediaFile?
|
||||
private let targetSelectionIndex: Int?
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
private var action: (() -> Void)?
|
||||
var requestDismiss: (@escaping () -> Void) -> Void = { _ in }
|
||||
|
||||
init(presentationData: PresentationData, tip: ContextController.Tip, isInline: Bool) {
|
||||
self.tip = tip
|
||||
self.presentationData = presentationData
|
||||
|
||||
if !isInline {
|
||||
self.background = (GlassBackgroundContainerView(), GlassBackgroundView())
|
||||
} else {
|
||||
self.background = nil
|
||||
}
|
||||
|
||||
self.textNode = TextNodeWithEntities()
|
||||
self.textNode.textNode.displaysAsynchronously = false
|
||||
self.textNode.textNode.isUserInteractionEnabled = false
|
||||
|
||||
var isUserInteractionEnabled = false
|
||||
var icon: UIImage?
|
||||
switch tip {
|
||||
case .textSelection:
|
||||
var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip2
|
||||
if let range = rawText.range(of: "|") {
|
||||
rawText.removeSubrange(range)
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound
|
||||
} else {
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = 1
|
||||
}
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case .quoteSelection:
|
||||
var rawText = presentationData.strings.ChatContextMenu_QuoteSelectionTip
|
||||
if let range = rawText.range(of: "|") {
|
||||
rawText.removeSubrange(range)
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound
|
||||
} else {
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = 1
|
||||
}
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case .messageViewsPrivacy:
|
||||
self.text = self.presentationData.strings.ChatContextMenu_MessageViewsPrivacyTip
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case let .messageCopyProtection(isChannel):
|
||||
self.text = isChannel ? self.presentationData.strings.Conversation_CopyProtectionInfoChannel : self.presentationData.strings.Conversation_CopyProtectionInfoGroup
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportCopyright")
|
||||
case let .animatedEmoji(text, arguments, file, action):
|
||||
self.action = action
|
||||
self.text = text ?? ""
|
||||
self.arguments = arguments
|
||||
self.file = file
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = text != nil
|
||||
case let .notificationTopicExceptions(text, action):
|
||||
self.action = action
|
||||
self.text = text
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case let .starsReactions(topCount):
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Chat_SendStarsToBecomeTopInfo("\(topCount)").string
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case .videoProcessing:
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Chat_VideoProcessingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case .collageReordering:
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Camera_CollageReorderingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.image = generateTintedImage(image: icon, color: presentationData.theme.contextMenu.primaryColor)
|
||||
|
||||
self.placeholderNode = ASDisplayNode()
|
||||
self.placeholderNode.clipsToBounds = true
|
||||
self.placeholderNode.cornerRadius = 4.0
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { _ in
|
||||
}, present: { _, _ in
|
||||
}, rootNode: { [weak self] in
|
||||
return self
|
||||
}, performAction: { _, _ in
|
||||
})
|
||||
self.textSelectionNode = textSelectionNode
|
||||
|
||||
let parentView: UIView
|
||||
if let background = self.background {
|
||||
self.view.addSubview(background.container)
|
||||
background.container.contentView.addSubview(background.background)
|
||||
parentView = background.background.contentView
|
||||
} else {
|
||||
parentView = self.view
|
||||
}
|
||||
|
||||
parentView.addSubview(self.textNode.textNode.view)
|
||||
parentView.addSubview(self.iconNode.view)
|
||||
parentView.addSubview(self.placeholderNode.view)
|
||||
|
||||
self.textSelectionNode.flatMap { parentView.addSubview($0.view) }
|
||||
|
||||
parentView.addSubview(textSelectionNode.highlightAreaNode.view)
|
||||
|
||||
parentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
|
||||
let shimmeringForegroundColor: UIColor
|
||||
if presentationData.theme.overallDarkAppearance {
|
||||
let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0)
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1)
|
||||
} else {
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07)
|
||||
}
|
||||
|
||||
self.placeholderNode.backgroundColor = shimmeringForegroundColor
|
||||
|
||||
self.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
}
|
||||
|
||||
@objc func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.requestDismiss({
|
||||
self.action?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateTransitionInside(other: InnerTextSelectionTipContainerNode) {
|
||||
let nodes: [ASDisplayNode] = [
|
||||
self.textNode.textNode,
|
||||
self.iconNode,
|
||||
self.placeholderNode
|
||||
]
|
||||
|
||||
for node in nodes {
|
||||
if let background = other.background {
|
||||
background.background.contentView.addSubview(node.view)
|
||||
} else {
|
||||
other.view.addSubview(node.view)
|
||||
}
|
||||
node.layer.animateAlpha(from: node.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
node?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateContentIn() {
|
||||
let nodes: [ASDisplayNode] = [
|
||||
self.textNode.textNode,
|
||||
self.iconNode
|
||||
]
|
||||
|
||||
for node in nodes {
|
||||
node.layer.animateAlpha(from: 0.0, to: node.alpha, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, presentation: ContextControllerActionsStackNode.Presentation, width: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let topInset: CGFloat = self.background != nil ? 16.0 : 9.0
|
||||
let bottomInset: CGFloat = self.background != nil ? 16.0 : 9.0
|
||||
let horizontalInset: CGFloat = 18.0
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
let iconSideInset: CGFloat = 20.0
|
||||
|
||||
let textFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
let boldTextFont = Font.bold(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
let textColor = self.presentationData.theme.contextMenu.primaryColor
|
||||
let linkColor = self.presentationData.theme.overallDarkAppearance ? UIColor(rgb: 0x64d2ff) : self.presentationData.theme.contextMenu.badgeFillColor
|
||||
|
||||
let iconSize = self.iconNode.image?.size ?? CGSize(width: 16.0, height: 16.0)
|
||||
|
||||
let text = self.text.replacingOccurrences(of: "#", with: "# ")
|
||||
let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { _ in
|
||||
return nil
|
||||
})))
|
||||
if let file = self.file {
|
||||
let range = (attributedText.string as NSString).range(of: "#")
|
||||
if range.location != NSNotFound {
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), range: range)
|
||||
}
|
||||
}
|
||||
|
||||
let shimmeringForegroundColor: UIColor
|
||||
if presentationData.theme.overallDarkAppearance {
|
||||
let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0)
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1)
|
||||
} else {
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07)
|
||||
}
|
||||
|
||||
let textRightInset: CGFloat
|
||||
if let _ = self.iconNode.image {
|
||||
textRightInset = iconSize.width - 2.0
|
||||
} else {
|
||||
textRightInset = 0.0
|
||||
}
|
||||
|
||||
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, minimumNumberOfLines: 0, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - horizontalInset * 2.0 - textRightInset, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.12, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
|
||||
let _ = textApply(self.arguments?.withUpdatedPlaceholderColor(shimmeringForegroundColor))
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: horizontalInset, y: topInset), size: textLayout.size)
|
||||
transition.updateFrame(node: self.textNode.textNode, frame: textFrame)
|
||||
if textFrame.size.height.isZero {
|
||||
self.textNode.textNode.alpha = 0.0
|
||||
} else if self.textNode.textNode.alpha.isZero {
|
||||
self.textNode.textNode.alpha = 1.0
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 1.0, duration: 0.2)
|
||||
}
|
||||
self.textNode.visibilityRect = CGRect.infinite
|
||||
|
||||
var contentHeight = textLayout.size.height
|
||||
if contentHeight.isZero {
|
||||
contentHeight = 32.0
|
||||
}
|
||||
|
||||
let size = CGSize(width: width, height: contentHeight + topInset + bottomInset)
|
||||
|
||||
let lineHeight: CGFloat = 8.0
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: horizontalInset, y: floorToScreenPixels((size.height - lineHeight) / 2.0)), size: CGSize(width: width - horizontalInset * 2.0, height: lineHeight)))
|
||||
transition.updateAlpha(node: self.placeholderNode, alpha: textFrame.height.isZero ? 1.0 : 0.0)
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
|
||||
if let textSelectionNode = self.textSelectionNode {
|
||||
transition.updateFrame(node: textSelectionNode, frame: textFrame)
|
||||
textSelectionNode.highlightAreaNode.frame = textFrame
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func setActualSize(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let transition = ComponentTransition(transition)
|
||||
|
||||
if let background = self.background {
|
||||
background.container.update(size: size, isDark: self.presentationData.theme.overallDarkAppearance, transition: transition)
|
||||
transition.setFrame(view: background.container, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
background.background.update(size: size, cornerRadius: min(30.0, size.height * 0.5), isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition)
|
||||
transition.setFrame(view: background.background, frame: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let textSelectionNode = self.textSelectionNode, let targetSelectionIndex = self.targetSelectionIndex {
|
||||
textSelectionNode.pretendInitiateSelection()
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.textSelectionNode?.pretendExtendSelection(to: targetSelectionIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateHighlight(animated: Bool) {
|
||||
}
|
||||
|
||||
private var isButtonHighlighted = false
|
||||
private var isHighlighted = false
|
||||
func setHighlighted(_ highlighted: Bool) {
|
||||
guard self.isHighlighted != highlighted else {
|
||||
return
|
||||
}
|
||||
self.isHighlighted = highlighted
|
||||
|
||||
if highlighted {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.tap()
|
||||
}
|
||||
|
||||
self.updateHighlight(animated: false)
|
||||
}
|
||||
|
||||
func highlightGestureMoved(location: CGPoint) {
|
||||
if self.bounds.contains(location) && self.isUserInteractionEnabled {
|
||||
self.setHighlighted(true)
|
||||
} else {
|
||||
self.setHighlighted(false)
|
||||
}
|
||||
}
|
||||
|
||||
func highlightGestureFinished(performAction: Bool) {
|
||||
if self.isHighlighted {
|
||||
self.setHighlighted(false)
|
||||
if performAction {
|
||||
self.requestDismiss({
|
||||
self.action?()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextActionsContainerNode: ASDisplayNode {
|
||||
private let presentationData: PresentationData
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let blurBackground: Bool
|
||||
private let shadowNode: ASImageNode
|
||||
private let additionalShadowNode: ASImageNode?
|
||||
private let additionalActionsNode: InnerActionsContainerNode?
|
||||
private let actionsNode: InnerActionsContainerNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var tip: ContextController.Tip?
|
||||
private var textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||
private var textSelectionTipNodeDisposable: Disposable?
|
||||
|
||||
var panSelectionGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
if self.panSelectionGestureEnabled != oldValue {
|
||||
self.actionsNode.panSelectionGestureEnabled = self.panSelectionGestureEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasAdditionalActions: Bool {
|
||||
return self.additionalActionsNode != nil
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, items: ContextController.Items, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) {
|
||||
self.presentationData = presentationData
|
||||
self.getController = getController
|
||||
self.blurBackground = blurBackground
|
||||
self.shadowNode = ASImageNode()
|
||||
self.shadowNode.displaysAsynchronously = false
|
||||
self.shadowNode.displayWithoutProcessing = true
|
||||
self.shadowNode.image = UIImage(bundleImageName: "Components/Context Menu/Shadow")?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
self.shadowNode.isHidden = true
|
||||
|
||||
var items = items
|
||||
if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional {
|
||||
let additionalShadowNode = ASImageNode()
|
||||
additionalShadowNode.displaysAsynchronously = false
|
||||
additionalShadowNode.displayWithoutProcessing = true
|
||||
additionalShadowNode.image = self.shadowNode.image
|
||||
additionalShadowNode.contentMode = .scaleToFill
|
||||
additionalShadowNode.isHidden = true
|
||||
self.additionalShadowNode = additionalShadowNode
|
||||
|
||||
self.additionalActionsNode = InnerActionsContainerNode(presentationData: presentationData, items: [firstItem], getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
itemList.removeFirst()
|
||||
items.content = .list(itemList)
|
||||
} else {
|
||||
self.additionalShadowNode = nil
|
||||
self.additionalActionsNode = nil
|
||||
}
|
||||
|
||||
var itemList: [ContextMenuItem] = []
|
||||
if case let .list(list) = items.content {
|
||||
itemList = list
|
||||
}
|
||||
|
||||
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: itemList, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
|
||||
self.tip = items.tip
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.additionalActionsNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.scrollNode.addSubnode(self.actionsNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
if let tipSignal = items.tipSignal {
|
||||
self.textSelectionTipNodeDisposable = (tipSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] tip in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.tip = tip
|
||||
requestLayout()
|
||||
}).strict()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.textSelectionTipNodeDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, presentation: ContextControllerActionsStackNode.Presentation, constrainedWidth: CGFloat, constrainedHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var widthClass = widthClass
|
||||
if !self.blurBackground {
|
||||
widthClass = .regular
|
||||
}
|
||||
|
||||
var contentSize = CGSize()
|
||||
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, minimalWidth: nil, transition: transition)
|
||||
|
||||
if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode {
|
||||
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, constrainedHeight: constrainedHeight, minimalWidth: actionsSize.width, transition: transition)
|
||||
contentSize = additionalActionsSize
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: additionalActionsSize)
|
||||
transition.updateFrame(node: additionalShadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
additionalShadowNode.isHidden = widthClass == .compact
|
||||
|
||||
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsSize))
|
||||
contentSize.height += 8.0
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsSize)
|
||||
transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
self.shadowNode.isHidden = widthClass == .compact
|
||||
|
||||
contentSize.width = max(contentSize.width, actionsSize.width)
|
||||
contentSize.height += actionsSize.height
|
||||
|
||||
transition.updateFrame(node: self.actionsNode, frame: bounds)
|
||||
|
||||
if let tip = self.tip {
|
||||
if let textSelectionTipNode = self.textSelectionTipNode, textSelectionTipNode.tip == tip {
|
||||
} else {
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
self.textSelectionTipNode = nil
|
||||
textSelectionTipNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: self.presentationData, tip: tip, isInline: false)
|
||||
let getController = self.getController
|
||||
textSelectionTipNode.requestDismiss = { completion in
|
||||
getController()?.dismiss(completion: completion)
|
||||
}
|
||||
self.textSelectionTipNode = textSelectionTipNode
|
||||
self.scrollNode.addSubnode(textSelectionTipNode)
|
||||
}
|
||||
} else {
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
self.textSelectionTipNode = nil
|
||||
textSelectionTipNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
contentSize.height += 8.0
|
||||
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, presentation: presentation, width: actionsSize.width, transition: transition)
|
||||
transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize))
|
||||
textSelectionTipNode.setActualSize(size: textSelectionTipSize, transition: transition)
|
||||
contentSize.height += textSelectionTipSize.height
|
||||
}
|
||||
|
||||
return contentSize
|
||||
}
|
||||
|
||||
func updateSize(containerSize: CGSize, contentSize: CGSize) {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
|
||||
return self.actionsNode.actionNode(at: self.view.convert(point, to: self.actionsNode.view))
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
self.actionsNode.updateTheme(presentationData: presentationData)
|
||||
self.textSelectionTipNode?.updateTheme(presentationData: presentationData)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.textSelectionTipNode?.animateIn()
|
||||
}
|
||||
|
||||
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
transition.animatePosition(node: additionalShadowNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
additionalShadowNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
+2206
File diff suppressed because it is too large
Load Diff
+6
@@ -0,0 +1,6 @@
|
||||
import ContextUI
|
||||
|
||||
public typealias ContextControllerImpl = ContextController
|
||||
public typealias ContextControllerActionsStackNodeImpl = ContextControllerActionsStackNode
|
||||
public typealias PeekControllerImpl = PeekController
|
||||
public typealias PinchControllerImpl = PinchController
|
||||
+1835
File diff suppressed because it is too large
Load Diff
+2203
File diff suppressed because it is too large
Load Diff
+42
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ReactionSelectionNode
|
||||
import ContextUI
|
||||
|
||||
enum ContextControllerPresentationNodeStateTransition {
|
||||
case animateIn
|
||||
case animateOut(result: ContextMenuActionResult, completion: () -> Void)
|
||||
}
|
||||
|
||||
protocol ContextControllerPresentationNode: ASDisplayNode {
|
||||
var ready: Signal<Bool, NoError> { get }
|
||||
|
||||
func replaceItems(items: ContextController.Items, animated: Bool?)
|
||||
func pushItems(items: ContextController.Items)
|
||||
func popItems()
|
||||
func wantsDisplayBelowKeyboard() -> Bool
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition,
|
||||
stateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
)
|
||||
|
||||
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void)
|
||||
func cancelReactionAnimation()
|
||||
|
||||
func highlightGestureMoved(location: CGPoint, hover: Bool)
|
||||
func highlightGestureFinished(performAction: Bool)
|
||||
|
||||
func decreaseHighlightedIndex()
|
||||
func increaseHighlightedIndex()
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
+886
@@ -0,0 +1,886 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import ReactionSelectionNode
|
||||
import ComponentFlow
|
||||
import TabSelectorComponent
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
import ComponentDisplayAdapters
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class ContextSourceContainer: ASDisplayNode {
|
||||
final class Source {
|
||||
weak var controller: ContextControllerImpl?
|
||||
|
||||
let id: AnyHashable
|
||||
let title: String
|
||||
let footer: String?
|
||||
let context: AccountContext?
|
||||
let source: ContextContentSource
|
||||
let closeActionTitle: String?
|
||||
let closeAction: (() -> Void)?
|
||||
|
||||
private var _presentationNode: ContextControllerPresentationNode?
|
||||
var presentationNode: ContextControllerPresentationNode {
|
||||
return self._presentationNode!
|
||||
}
|
||||
|
||||
var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
|
||||
var validLayout: ContainerViewLayout?
|
||||
var presentationData: PresentationData?
|
||||
var delayLayoutUpdate: Bool = false
|
||||
var isAnimatingOut: Bool = false
|
||||
|
||||
var itemsDisposables = DisposableSet()
|
||||
|
||||
let ready = Promise<Bool>()
|
||||
private let contentReady = Promise<Bool>()
|
||||
private let actionsReady = Promise<Bool>()
|
||||
|
||||
init(
|
||||
controller: ContextControllerImpl,
|
||||
id: AnyHashable,
|
||||
title: String,
|
||||
footer: String?,
|
||||
context: AccountContext?,
|
||||
source: ContextContentSource,
|
||||
items: Signal<ContextController.Items, NoError>,
|
||||
closeActionTitle: String? = nil,
|
||||
closeAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.controller = controller
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self.context = context
|
||||
self.source = source
|
||||
self.closeActionTitle = closeActionTitle
|
||||
self.closeAction = closeAction
|
||||
|
||||
self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get())
|
||||
|> map { a, b -> Bool in
|
||||
return a && b
|
||||
}
|
||||
|> distinctUntilChanged)
|
||||
|
||||
switch source {
|
||||
case let .location(source):
|
||||
self.contentReady.set(.single(true))
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .location(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
case let .reference(source):
|
||||
self.contentReady.set(.single(true))
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .reference(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
case let .extracted(source):
|
||||
self.contentReady.set(.single(true))
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
if let _ = self.closeActionTitle {
|
||||
} else {
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
}
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .extracted(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
case let .controller(source):
|
||||
self.contentReady.set(source.controller.ready.get())
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .controller(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
}
|
||||
|
||||
self.itemsDisposables.add((items |> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.setItems(items: items, animated: nil)
|
||||
self.actionsReady.set(.single(true))
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.itemsDisposables.dispose()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.currentPresentationStateTransition = .animateIn
|
||||
self.update(transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
|
||||
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||
self.currentPresentationStateTransition = .animateOut(result: result, completion: completion)
|
||||
if let _ = self.validLayout {
|
||||
if case let .custom(transition) = result {
|
||||
self.delayLayoutUpdate = true
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.delayLayoutUpdate = false
|
||||
self.update(transition: transition)
|
||||
self.isAnimatingOut = true
|
||||
}
|
||||
} else {
|
||||
self.update(transition: .animated(duration: 0.35, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
self.presentationNode.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
|
||||
func cancelReactionAnimation() {
|
||||
self.presentationNode.cancelReactionAnimation()
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) {
|
||||
self.presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion)
|
||||
}
|
||||
|
||||
func setItems(items: Signal<ContextController.Items, NoError>, animated: Bool) {
|
||||
self.itemsDisposables.dispose()
|
||||
self.itemsDisposables = DisposableSet()
|
||||
self.itemsDisposables.add((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setItems(items: items, animated: animated)
|
||||
}))
|
||||
}
|
||||
|
||||
func setItems(items: ContextController.Items, animated: Bool?) {
|
||||
self.presentationNode.replaceItems(items: items, animated: animated)
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
self.itemsDisposables.add((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentationNode.pushItems(items: items)
|
||||
}))
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
self.itemsDisposables.removeLast()
|
||||
self.presentationNode.popItems()
|
||||
}
|
||||
|
||||
func update(transition: ContainedViewLayoutTransition) {
|
||||
guard let validLayout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
guard let presentationData = self.presentationData else {
|
||||
return
|
||||
}
|
||||
self.update(presentationData: presentationData, layout: validLayout, transition: transition)
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) {
|
||||
if self.isAnimatingOut || self.delayLayoutUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
self.presentationData = presentationData
|
||||
|
||||
let presentationStateTransition = self.currentPresentationStateTransition
|
||||
self.currentPresentationStateTransition = .none
|
||||
|
||||
self.presentationNode.update(
|
||||
presentationData: presentationData,
|
||||
layout: layout,
|
||||
transition: transition,
|
||||
stateTransition: presentationStateTransition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PanState {
|
||||
var fraction: CGFloat
|
||||
|
||||
init(fraction: CGFloat) {
|
||||
self.fraction = fraction
|
||||
}
|
||||
}
|
||||
|
||||
private weak var controller: ContextControllerImpl?
|
||||
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
|
||||
var sources: [Source] = []
|
||||
var activeIndex: Int = 0
|
||||
|
||||
private var tabSelector: ComponentView<Empty>?
|
||||
private var footer: ComponentView<Empty>?
|
||||
private var closeButton: ComponentView<Empty>?
|
||||
|
||||
private var presentationData: PresentationData?
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var panState: PanState?
|
||||
|
||||
let ready = Promise<Bool>()
|
||||
|
||||
var activeSource: Source? {
|
||||
if self.activeIndex >= self.sources.count {
|
||||
return nil
|
||||
}
|
||||
return self.sources[self.activeIndex]
|
||||
}
|
||||
|
||||
var overlayWantsToBeBelowKeyboard: Bool {
|
||||
return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false
|
||||
}
|
||||
|
||||
init(controller: ContextControllerImpl, configuration: ContextController.Configuration, context: AccountContext?) {
|
||||
self.controller = controller
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
for i in 0 ..< configuration.sources.count {
|
||||
let source = configuration.sources[i]
|
||||
|
||||
let mappedSource = Source(
|
||||
controller: controller,
|
||||
id: source.id,
|
||||
title: source.title,
|
||||
footer: source.footer,
|
||||
context: context,
|
||||
source: source.source,
|
||||
items: source.items,
|
||||
closeActionTitle: source.closeActionTitle,
|
||||
closeAction: source.closeAction
|
||||
)
|
||||
self.sources.append(mappedSource)
|
||||
self.addSubnode(mappedSource.presentationNode)
|
||||
|
||||
if source.id == configuration.initialId {
|
||||
self.activeIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
self.ready.set(self.sources[self.activeIndex].ready.get())
|
||||
|
||||
self.view.addGestureRecognizer(InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
|
||||
guard let self else {
|
||||
return []
|
||||
}
|
||||
if self.sources.count <= 1 {
|
||||
return []
|
||||
}
|
||||
return [.left, .right]
|
||||
}))
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: InteractiveTransitionGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
if let validLayout = self.validLayout {
|
||||
var translationX = recognizer.translation(in: self.view).x
|
||||
if self.activeIndex == 0 && translationX > 0.0 {
|
||||
translationX = scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0)
|
||||
} else if self.activeIndex == self.sources.count - 1 && translationX < 0.0 {
|
||||
translationX = -scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0)
|
||||
}
|
||||
|
||||
self.panState = PanState(fraction: translationX / validLayout.size.width)
|
||||
self.update(transition: .immediate)
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
if let panState = self.panState {
|
||||
self.panState = nil
|
||||
|
||||
let velocity = recognizer.velocity(in: self.view)
|
||||
|
||||
var nextIndex = self.activeIndex
|
||||
if panState.fraction < -0.4 {
|
||||
nextIndex += 1
|
||||
} else if panState.fraction > 0.4 {
|
||||
nextIndex -= 1
|
||||
} else if abs(velocity.x) >= 200.0 {
|
||||
if velocity.x < 0.0 {
|
||||
nextIndex += 1
|
||||
} else {
|
||||
nextIndex -= 1
|
||||
}
|
||||
}
|
||||
if nextIndex < 0 {
|
||||
nextIndex = 0
|
||||
}
|
||||
if nextIndex > self.sources.count - 1 {
|
||||
nextIndex = self.sources.count - 1
|
||||
}
|
||||
if nextIndex != self.activeIndex {
|
||||
self.activeIndex = nextIndex
|
||||
}
|
||||
|
||||
self.update(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
// if let activeSource = self.activeSource {
|
||||
// activeSource.animateIn()
|
||||
// }
|
||||
for source in self.sources {
|
||||
source.animateIn()
|
||||
}
|
||||
if let footerView = self.footer?.view {
|
||||
footerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
if let tabSelectorView = self.tabSelector?.view {
|
||||
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
if let closeButtonView = self.closeButton?.view {
|
||||
closeButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||
let delayDismissal = self.activeSource?.closeAction != nil
|
||||
let delay: Double = delayDismissal ? 0.2 : 0.0
|
||||
let duration: Double = delayDismissal ? 0.35 : 0.2
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false, completion: { _ in
|
||||
if delayDismissal {
|
||||
Queue.mainQueue().after(0.55) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if let footerView = self.footer?.view {
|
||||
footerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
|
||||
}
|
||||
if let tabSelectorView = self.tabSelector?.view {
|
||||
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
|
||||
}
|
||||
if let closeButtonView = self.closeButton?.view {
|
||||
closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
for source in self.sources {
|
||||
if source !== self.activeSource {
|
||||
source.animateOut(result: result, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.animateOut(result: result, completion: delayDismissal ? {} : completion)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func highlightGestureMoved(location: CGPoint, hover: Bool) {
|
||||
if self.activeIndex >= self.sources.count {
|
||||
return
|
||||
}
|
||||
self.sources[self.activeIndex].presentationNode.highlightGestureMoved(location: location, hover: hover)
|
||||
}
|
||||
|
||||
func highlightGestureFinished(performAction: Bool) {
|
||||
if self.activeIndex >= self.sources.count {
|
||||
return
|
||||
}
|
||||
self.sources[self.activeIndex].presentationNode.highlightGestureFinished(performAction: performAction)
|
||||
}
|
||||
|
||||
func performHighlightedAction() {
|
||||
self.activeSource?.presentationNode.highlightGestureFinished(performAction: true)
|
||||
}
|
||||
|
||||
func decreaseHighlightedIndex() {
|
||||
self.activeSource?.presentationNode.decreaseHighlightedIndex()
|
||||
}
|
||||
|
||||
func increaseHighlightedIndex() {
|
||||
self.activeSource?.presentationNode.increaseHighlightedIndex()
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelReactionAnimation() {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.cancelReactionAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func setItems(items: Signal<ContextController.Items, NoError>, animated: Bool) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.setItems(items: items, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.pushItems(items: items)
|
||||
}
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.popItems()
|
||||
}
|
||||
}
|
||||
|
||||
private func update(transition: ContainedViewLayoutTransition) {
|
||||
if let presentationData = self.presentationData, let validLayout = self.validLayout {
|
||||
self.update(presentationData: presentationData, layout: validLayout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.validLayout = layout
|
||||
|
||||
var childLayout = layout
|
||||
|
||||
if let activeSource = self.activeSource {
|
||||
switch activeSource.source {
|
||||
case .location, .reference:
|
||||
self.backgroundNode.updateColor(
|
||||
color: .clear,
|
||||
enableBlur: false,
|
||||
forceKeepBlur: false,
|
||||
transition: .immediate
|
||||
)
|
||||
case let .extracted(extracted):
|
||||
self.backgroundNode.updateColor(
|
||||
color: extracted.blurBackground ? presentationData.theme.contextMenu.dimColor : .clear,
|
||||
enableBlur: extracted.blurBackground,
|
||||
forceKeepBlur: extracted.blurBackground,
|
||||
transition: .immediate
|
||||
)
|
||||
case .controller:
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
self.backgroundNode.updateColor(
|
||||
color: UIColor(white: 0.0, alpha: 0.4),
|
||||
enableBlur: false,
|
||||
forceKeepBlur: false,
|
||||
transition: .immediate
|
||||
)
|
||||
} else {
|
||||
self.backgroundNode.updateColor(
|
||||
color: presentationData.theme.contextMenu.dimColor,
|
||||
enableBlur: true,
|
||||
forceKeepBlur: true,
|
||||
transition: .immediate
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true)
|
||||
self.backgroundNode.update(size: layout.size, transition: transition)
|
||||
|
||||
if self.sources.count > 1 {
|
||||
let tabSelector: ComponentView<Empty>
|
||||
if let current = self.tabSelector {
|
||||
tabSelector = current
|
||||
} else {
|
||||
tabSelector = ComponentView()
|
||||
self.tabSelector = tabSelector
|
||||
}
|
||||
let mappedItems = self.sources.map { source -> TabSelectorComponent.Item in
|
||||
return TabSelectorComponent.Item(id: source.id, title: source.title)
|
||||
}
|
||||
let tabSelectorSize = tabSelector.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(TabSelectorComponent(
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.8),
|
||||
selection: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1)
|
||||
),
|
||||
theme: presentationData.theme,
|
||||
customLayout: TabSelectorComponent.CustomLayout(
|
||||
font: Font.medium(14.0),
|
||||
spacing: 9.0
|
||||
),
|
||||
items: mappedItems,
|
||||
selectedId: self.activeSource?.id,
|
||||
setSelectedId: { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let index = self.sources.firstIndex(where: { $0.id == id }) {
|
||||
self.activeIndex = index
|
||||
self.update(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 44.0)
|
||||
)
|
||||
childLayout.intrinsicInsets.bottom += 30.0
|
||||
|
||||
if let footerText = self.activeSource?.footer {
|
||||
var footerTransition = transition
|
||||
let footer: ComponentView<Empty>
|
||||
if let current = self.footer {
|
||||
footer = current
|
||||
} else {
|
||||
footerTransition = .immediate
|
||||
footer = ComponentView()
|
||||
self.footer = footer
|
||||
}
|
||||
|
||||
let footerSize = footer.update(
|
||||
transition: ComponentTransition(footerTransition),
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 144.0)
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 20.0
|
||||
childLayout.intrinsicInsets.bottom += footerSize.height + spacing
|
||||
|
||||
if let footerView = footer.view {
|
||||
if footerView.superview == nil {
|
||||
self.view.addSubview(footerView)
|
||||
|
||||
footerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
footerTransition.updateFrame(view: footerView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - footerSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height - footerSize.height - spacing), size: footerSize))
|
||||
}
|
||||
} else if let footer = self.footer {
|
||||
self.footer = nil
|
||||
footer.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
footer.view?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let tabSelectorView = tabSelector.view {
|
||||
if tabSelectorView.superview == nil {
|
||||
self.view.addSubview(tabSelectorView)
|
||||
}
|
||||
transition.updateFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - tabSelectorSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height), size: tabSelectorSize))
|
||||
}
|
||||
} else if let source = self.sources.first, let closeActionTitle = source.closeActionTitle {
|
||||
let closeButton: ComponentView<Empty>
|
||||
if let current = self.closeButton {
|
||||
closeButton = current
|
||||
} else {
|
||||
closeButton = ComponentView()
|
||||
self.closeButton = closeButton
|
||||
}
|
||||
|
||||
let closeButtonSize = closeButton.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
CloseButtonComponent(
|
||||
backgroundColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1),
|
||||
text: closeActionTitle
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self, weak source] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let source, let closeAction = source.closeAction {
|
||||
closeAction()
|
||||
} else {
|
||||
self.controller?.dismiss(result: .dismissWithoutContent, completion: nil)
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 44.0)
|
||||
)
|
||||
childLayout.intrinsicInsets.bottom += 30.0
|
||||
|
||||
if let closeButtonView = closeButton.view {
|
||||
if closeButtonView.superview == nil {
|
||||
self.view.addSubview(closeButtonView)
|
||||
}
|
||||
transition.updateFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - closeButtonSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - closeButtonSize.height - 10.0), size: closeButtonSize))
|
||||
}
|
||||
} else if let tabSelector = self.tabSelector {
|
||||
self.tabSelector = nil
|
||||
tabSelector.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
for i in 0 ..< self.sources.count {
|
||||
var itemFrame = CGRect(origin: CGPoint(), size: childLayout.size)
|
||||
itemFrame.origin.x += CGFloat(i - self.activeIndex) * childLayout.size.width
|
||||
if let panState = self.panState {
|
||||
itemFrame.origin.x += panState.fraction * childLayout.size.width
|
||||
}
|
||||
|
||||
let itemTransition = transition
|
||||
itemTransition.updateFrame(node: self.sources[i].presentationNode, frame: itemFrame)
|
||||
self.sources[i].update(
|
||||
presentationData: presentationData,
|
||||
layout: childLayout,
|
||||
transition: itemTransition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let tabSelectorView = self.tabSelector?.view {
|
||||
if let result = tabSelectorView.hitTest(self.view.convert(point, to: tabSelectorView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
if let closeButtonView = self.closeButton?.view {
|
||||
if let result = closeButtonView.hitTest(self.view.convert(point, to: closeButtonView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
guard let activeSource = self.activeSource else {
|
||||
return nil
|
||||
}
|
||||
return activeSource.presentationNode.view.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class CloseButtonComponent: CombinedComponent {
|
||||
let backgroundColor: UIColor
|
||||
let text: String
|
||||
|
||||
init(
|
||||
backgroundColor: UIColor,
|
||||
text: String
|
||||
) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: CloseButtonComponent, rhs: CloseButtonComponent) -> Bool {
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let text = Child(Text.self)
|
||||
|
||||
return { context in
|
||||
let text = text.update(
|
||||
component: Text(
|
||||
text: "\(context.component.text)",
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
),
|
||||
availableSize: CGSize(width: 200.0, height: 100.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let backgroundSize = CGSize(width: text.size.width + 34.0, height: 36.0)
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: 18.0),
|
||||
availableSize: backgroundSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||
)
|
||||
|
||||
return backgroundSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
|
||||
public final class PeekControllerImpl: ViewController, PeekController, ContextControllerProtocol {
|
||||
public var useComplexItemsTransitionAnimation: Bool = false
|
||||
public var immediateItemsTransitionAnimation = false
|
||||
|
||||
public func getActionsMinHeight() -> ContextController.ActionsHeight? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, animated: Bool) {
|
||||
}
|
||||
|
||||
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
|
||||
}
|
||||
|
||||
public func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
self.controllerNode.pushItems(items: items)
|
||||
}
|
||||
|
||||
public func popItems() {
|
||||
self.controllerNode.popItems()
|
||||
}
|
||||
|
||||
private var controllerNode: PeekControllerNode {
|
||||
return self.displayNode as! PeekControllerNode
|
||||
}
|
||||
|
||||
public var contentNode: PeekControllerContentNode & ASDisplayNode {
|
||||
return self.controllerNode.contentNode
|
||||
}
|
||||
|
||||
private let presentationData: PresentationData
|
||||
private let content: PeekControllerContent
|
||||
public var sourceView: () -> (UIView, CGRect)?
|
||||
private let activateImmediately: Bool
|
||||
|
||||
public var visibilityUpdated: ((Bool) -> Void)?
|
||||
|
||||
public var getOverlayViews: (() -> [UIView])?
|
||||
|
||||
public var appeared: (() -> Void)?
|
||||
public var disappeared: (() -> Void)?
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?, activateImmediately: Bool = false) {
|
||||
self.presentationData = presentationData
|
||||
self.content = content
|
||||
self.sourceView = sourceView
|
||||
self.activateImmediately = activateImmediately
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PeekControllerNode(presentationData: self.presentationData, controller: self, content: self.content, requestDismiss: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
private func getSourceRect() -> CGRect {
|
||||
if let (sourceView, sourceRect) = self.sourceView() {
|
||||
return sourceView.convert(sourceRect, to: self.view)
|
||||
} else {
|
||||
let size = self.displayNode.bounds.size
|
||||
return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0))
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedIn {
|
||||
self.animatedIn = true
|
||||
self.controllerNode.animateIn(from: self.getSourceRect())
|
||||
|
||||
self.visibilityUpdated?(true)
|
||||
|
||||
if self.activateImmediately {
|
||||
self.controllerNode.activateMenu(immediately: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.visibilityUpdated?(false)
|
||||
self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
})
|
||||
}
|
||||
|
||||
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
|
||||
self.dismiss(completion: completion)
|
||||
}
|
||||
}
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
|
||||
private let animationDurationFactor: Double = 1.0
|
||||
|
||||
final class PeekControllerNode: ViewControllerTracingNode, PeekControllerNodeProtocol {
|
||||
private let requestDismiss: () -> Void
|
||||
|
||||
private let presentationData: PresentationData
|
||||
private let theme: PeekControllerTheme
|
||||
|
||||
private weak var controller: PeekController?
|
||||
|
||||
private let blurView: UIView
|
||||
private let dimNode: ASDisplayNode
|
||||
private let containerBackgroundNode: ASImageNode
|
||||
private let containerNode: ASDisplayNode
|
||||
private let darkDimNode: ASDisplayNode
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var content: PeekControllerContent
|
||||
var contentNode: PeekControllerContentNode & ASDisplayNode
|
||||
private var contentNodeHasValidLayout = false
|
||||
|
||||
private var topAccessoryNode: ASDisplayNode?
|
||||
private var fullScreenAccessoryNode: (PeekControllerAccessoryNode & ASDisplayNode)?
|
||||
|
||||
private var actionsStackNode: ContextControllerActionsStackNode
|
||||
|
||||
private var hapticFeedback = HapticFeedback()
|
||||
|
||||
private var initialContinueGesturePoint: CGPoint?
|
||||
private var didMoveFromInitialGesturePoint = false
|
||||
private var highlightedActionNode: ContextActionNodeProtocol?
|
||||
|
||||
init(presentationData: PresentationData, controller: PeekController, content: PeekControllerContent, requestDismiss: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.requestDismiss = requestDismiss
|
||||
self.theme = PeekControllerTheme(presentationTheme: presentationData.theme)
|
||||
self.controller = controller
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.isDark ? .dark : .light))
|
||||
blurView.isUserInteractionEnabled = false
|
||||
self.blurView = blurView
|
||||
|
||||
self.darkDimNode = ASDisplayNode()
|
||||
self.darkDimNode.alpha = 0.0
|
||||
self.darkDimNode.backgroundColor = presentationData.theme.contextMenu.dimColor
|
||||
self.darkDimNode.isUserInteractionEnabled = false
|
||||
|
||||
switch content.menuActivation() {
|
||||
case .drag:
|
||||
self.dimNode.backgroundColor = nil
|
||||
self.blurView.alpha = 1.0
|
||||
case .press:
|
||||
self.dimNode.backgroundColor = UIColor(white: self.theme.isDark ? 0.0 : 1.0, alpha: 0.5)
|
||||
self.blurView.alpha = 0.0
|
||||
}
|
||||
|
||||
self.containerBackgroundNode = ASImageNode()
|
||||
self.containerBackgroundNode.isLayerBacked = true
|
||||
self.containerBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
|
||||
self.content = content
|
||||
self.contentNode = content.node()
|
||||
self.topAccessoryNode = content.topAccessoryNode()
|
||||
self.fullScreenAccessoryNode = content.fullScreenAccessoryNode(blurView: blurView)
|
||||
self.fullScreenAccessoryNode?.alpha = 0.0
|
||||
|
||||
var activatedActionImpl: (() -> Void)?
|
||||
var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
self.actionsStackNode = ContextControllerActionsStackNodeImpl(
|
||||
context: nil,
|
||||
getController: { [weak controller] in
|
||||
return controller
|
||||
},
|
||||
requestDismiss: { result in
|
||||
activatedActionImpl?()
|
||||
},
|
||||
requestUpdate: { transition in
|
||||
requestLayoutImpl?(transition)
|
||||
}
|
||||
)
|
||||
self.actionsStackNode.alpha = 0.0
|
||||
|
||||
let items = ContextController.Items(
|
||||
id: 0,
|
||||
content: .list(content.menuItems()),
|
||||
context: nil,
|
||||
reactionItems: [],
|
||||
selectedReactionItems: Set(),
|
||||
reactionsTitle: nil,
|
||||
reactionsLocked: false,
|
||||
animationCache: nil,
|
||||
alwaysAllowPremiumReactions: false,
|
||||
allPresetReactionsAreAvailable: false,
|
||||
getEmojiContent: nil,
|
||||
disablePositionLock: false,
|
||||
tip: nil,
|
||||
tipSignal: nil,
|
||||
dismissed: nil
|
||||
)
|
||||
if let item = makeContextControllerActionsStackItem(items: items).first {
|
||||
self.actionsStackNode.replace(
|
||||
item: item,
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
requestLayoutImpl = { [weak self] transition in
|
||||
self?.updateLayout(transition: transition)
|
||||
}
|
||||
|
||||
if content.presentation() == .freeform {
|
||||
self.containerNode.isUserInteractionEnabled = false
|
||||
} else {
|
||||
self.containerNode.clipsToBounds = true
|
||||
self.containerNode.cornerRadius = 16.0
|
||||
}
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.view.addSubview(self.blurView)
|
||||
self.addSubnode(self.darkDimNode)
|
||||
self.containerNode.addSubnode(self.contentNode)
|
||||
|
||||
self.addSubnode(self.actionsStackNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
self.fullScreenAccessoryNode?.dismiss = { [weak self] in
|
||||
self?.requestDismiss()
|
||||
}
|
||||
self.addSubnode(fullScreenAccessoryNode)
|
||||
}
|
||||
|
||||
activatedActionImpl = { [weak self] in
|
||||
self?.requestDismiss()
|
||||
}
|
||||
|
||||
self.hapticFeedback.prepareTap()
|
||||
|
||||
controller.ready.set(self.contentNode.ready())
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
func updateLayout(transition: ContainedViewLayoutTransition = .immediate) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceItem(items: Signal<ContextController.Items, NoError>) {
|
||||
let _ = (items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let item = makeContextControllerActionsStackItem(items: items).first {
|
||||
self.actionsStackNode.replace(item: item, animated: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
let _ = (items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let item = makeContextControllerActionsStackItem(items: items).first {
|
||||
self.actionsStackNode.push(item: item, currentScrollingState: nil, positionLock: nil, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
self.actionsStackNode.pop()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.darkDimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
var layoutInsets = layout.insets(options: [])
|
||||
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0)
|
||||
layoutInsets.right = layoutInsets.left
|
||||
if !layoutInsets.bottom.isZero {
|
||||
layoutInsets.bottom -= 12.0
|
||||
}
|
||||
|
||||
let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0)
|
||||
|
||||
let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate)
|
||||
if self.contentNodeHasValidLayout {
|
||||
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
|
||||
} else {
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
}
|
||||
|
||||
let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0
|
||||
|
||||
let actionsSize = self.actionsStackNode.update(
|
||||
presentationData: self.presentationData,
|
||||
constrainedSize: CGSize(width: layout.size.width - actionsSideInset * 2.0, height: layout.size.height),
|
||||
presentation: .inline,
|
||||
transition: transition
|
||||
)
|
||||
|
||||
let containerFrame: CGRect
|
||||
let actionsFrame: CGRect
|
||||
if layout.size.width > layout.size.height {
|
||||
if self.actionsStackNode.alpha.isZero {
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
} else {
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 3.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
}
|
||||
actionsFrame = CGRect(origin: CGPoint(x: containerFrame.maxX + 32.0, y: floor((layout.size.height - actionsSize.height) / 2.0)), size: actionsSize)
|
||||
} else {
|
||||
switch self.content.presentation() {
|
||||
case .contained:
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
case .freeform:
|
||||
var fraction: CGFloat = 1.0 / 3.0
|
||||
if let _ = self.controller?.appeared {
|
||||
fraction *= 1.33
|
||||
}
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) * fraction)), size: contentSize)
|
||||
}
|
||||
actionsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - actionsSize.width) / 2.0), y: containerFrame.maxY + 64.0), size: actionsSize)
|
||||
}
|
||||
transition.updateFrame(node: self.containerNode, frame: containerFrame)
|
||||
|
||||
transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame)
|
||||
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
transition.updateFrame(node: fullScreenAccessoryNode, frame: CGRect(origin: .zero, size: layout.size))
|
||||
fullScreenAccessoryNode.updateLayout(size: layout.size, transition: transition)
|
||||
}
|
||||
|
||||
self.contentNodeHasValidLayout = true
|
||||
}
|
||||
|
||||
func animateIn(from rect: CGRect) {
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3)
|
||||
|
||||
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
|
||||
|
||||
if let appeared = self.controller?.appeared {
|
||||
appeared()
|
||||
let scale = rect.width / self.contentNode.frame.width
|
||||
self.containerNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
} else {
|
||||
self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
if let topAccessoryNode = self.topAccessoryNode {
|
||||
topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
|
||||
topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
if case .press = self.content.menuActivation() {
|
||||
self.hapticFeedback.tap()
|
||||
} else {
|
||||
self.hapticFeedback.impact()
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(to rect: CGRect, completion: @escaping () -> Void) {
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
self.darkDimNode.layer.animateAlpha(from: self.darkDimNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
let springDuration: Double = 0.42 * animationDurationFactor
|
||||
let springDamping: CGFloat = 104.0
|
||||
|
||||
var scaleCompleted = false
|
||||
var positionCompleted = false
|
||||
let outCompletion = {
|
||||
if scaleCompleted && positionCompleted {
|
||||
}
|
||||
}
|
||||
|
||||
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
positionCompleted = true
|
||||
outCompletion()
|
||||
completion()
|
||||
})
|
||||
|
||||
if let _ = self.controller?.disappeared {
|
||||
self.controller?.disappeared?()
|
||||
let scale = rect.width / self.contentNode.frame.width
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: scale, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
scaleCompleted = true
|
||||
outCompletion()
|
||||
})
|
||||
} else {
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
scaleCompleted = true
|
||||
outCompletion()
|
||||
})
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
if !self.actionsStackNode.alpha.isZero {
|
||||
let actionsOffset = CGPoint(x: rect.midX - self.actionsStackNode.position.x, y: rect.midY - self.actionsStackNode.position.y)
|
||||
self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false)
|
||||
self.actionsStackNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false)
|
||||
self.actionsStackNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: actionsOffset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
}
|
||||
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero {
|
||||
fullScreenAccessoryNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.requestDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard case .drag = self.content.menuActivation() else {
|
||||
return
|
||||
}
|
||||
|
||||
let location = recognizer.location(in: self.view)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
self.applyDraggingOffset(location)
|
||||
case .cancelled, .ended:
|
||||
self.endDragging(location)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func applyDraggingOffset(_ offset: CGPoint) {
|
||||
let localPoint = offset
|
||||
let initialPoint: CGPoint
|
||||
if let current = self.initialContinueGesturePoint {
|
||||
initialPoint = current
|
||||
} else {
|
||||
initialPoint = localPoint
|
||||
self.initialContinueGesturePoint = localPoint
|
||||
}
|
||||
if !self.actionsStackNode.alpha.isZero {
|
||||
if !self.didMoveFromInitialGesturePoint {
|
||||
let distance = abs(localPoint.y - initialPoint.y)
|
||||
if distance > 12.0 {
|
||||
self.didMoveFromInitialGesturePoint = true
|
||||
}
|
||||
}
|
||||
if self.didMoveFromInitialGesturePoint {
|
||||
let actionPoint = self.view.convert(localPoint, to: self.actionsStackNode.view)
|
||||
self.actionsStackNode.highlightGestureMoved(location: actionPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endDragging(_ location: CGPoint) {
|
||||
if self.didMoveFromInitialGesturePoint {
|
||||
self.actionsStackNode.highlightGestureFinished(performAction: true)
|
||||
} else if self.actionsStackNode.alpha.isZero {
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero {
|
||||
} else {
|
||||
self.requestDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activateMenu(immediately: Bool) {
|
||||
if self.content.menuItems().isEmpty {
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
fullScreenAccessoryNode.alpha = 1.0
|
||||
fullScreenAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
let previousBlurAlpha = self.blurView.alpha
|
||||
self.blurView.alpha = 1.0
|
||||
self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.3)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
fullScreenAccessoryNode.alpha = 1.0
|
||||
fullScreenAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
}
|
||||
if case .press = self.content.menuActivation() {
|
||||
self.hapticFeedback.impact()
|
||||
}
|
||||
|
||||
let springDuration: Double = 0.42 * animationDurationFactor
|
||||
let springDamping: CGFloat = 104.0
|
||||
|
||||
let previousBlurAlpha = self.blurView.alpha
|
||||
self.blurView.alpha = 1.0
|
||||
self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.25)
|
||||
|
||||
let previousDarkDimAlpha = self.darkDimNode.alpha
|
||||
self.darkDimNode.alpha = 1.0
|
||||
self.darkDimNode.layer.animateAlpha(from: previousDarkDimAlpha, to: 1.0, duration: 0.3)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: springDuration, curve: .spring))
|
||||
}
|
||||
|
||||
let animateIn = {
|
||||
self.actionsStackNode.alpha = 1.0
|
||||
self.actionsStackNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor)
|
||||
self.actionsStackNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
|
||||
|
||||
let localContentSourceFrame = self.containerNode.frame
|
||||
self.actionsStackNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localContentSourceFrame.center.x - self.actionsStackNode.position.x, y: localContentSourceFrame.center.y - self.actionsStackNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
}
|
||||
if immediately {
|
||||
animateIn()
|
||||
} else {
|
||||
Queue.mainQueue().after(0.02, animateIn)
|
||||
}
|
||||
}
|
||||
|
||||
func updateContent(content: PeekControllerContent) {
|
||||
let contentNode = self.contentNode
|
||||
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in
|
||||
contentNode?.removeFromSupernode()
|
||||
})
|
||||
contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false)
|
||||
|
||||
self.content = content
|
||||
self.contentNode = content.node()
|
||||
self.containerNode.addSubnode(self.contentNode)
|
||||
self.contentNodeHasValidLayout = false
|
||||
|
||||
self.replaceItem(items: .single(ContextController.Items(content: .list(content.menuItems()))))
|
||||
|
||||
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut))
|
||||
}
|
||||
|
||||
self.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import UIKitRuntimeUtils
|
||||
import ContextUI
|
||||
|
||||
private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
private weak var controller: PinchController?
|
||||
|
||||
private var initialSourceFrame: CGRect?
|
||||
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let scrollingContainer: ASDisplayNode
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
private let disableScreenshots: Bool
|
||||
private let getContentAreaInScreenSpace: () -> CGRect
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var isAnimatingOut: Bool = false
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
init(controller: PinchController, sourceNode: PinchSourceContainerNode, disableScreenshots: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
self.disableScreenshots = disableScreenshots
|
||||
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.dimNode.alpha = 0.0
|
||||
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
|
||||
self.scrollingContainer = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.clippingNode)
|
||||
self.clippingNode.addSubnode(self.scrollingContainer)
|
||||
|
||||
self.sourceNode.deactivate = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
|
||||
self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in
|
||||
guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else {
|
||||
return
|
||||
}
|
||||
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0))
|
||||
|
||||
let pinchOffset = CGPoint(
|
||||
x: pinchLocation.x - initialSourceFrame.width / 2.0,
|
||||
y: pinchLocation.y - initialSourceFrame.height / 2.0
|
||||
)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DTranslate(transform, offset.x - pinchOffset.x * (scale - 1.0), offset.y - pinchOffset.y * (scale - 1.0), 0.0)
|
||||
transform = CATransform3DScale(transform, scale, scale, 0.0)
|
||||
|
||||
strongSelf.sourceNode.contentNode.transform = transform
|
||||
}
|
||||
|
||||
if self.disableScreenshots {
|
||||
setLayerDisableScreenshots(self.layer, true)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) {
|
||||
if self.isAnimatingOut {
|
||||
return
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.initialSourceFrame = convertedFrame
|
||||
self.scrollingContainer.addSubnode(self.sourceNode.contentNode)
|
||||
|
||||
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
|
||||
updatedContentAreaInScreenSpace.origin.x = 0.0
|
||||
updatedContentAreaInScreenSpace.size.width = self.bounds.width
|
||||
|
||||
self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.isAnimatingOut = true
|
||||
|
||||
let performCompletion: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.isAnimatingOut = false
|
||||
|
||||
strongSelf.sourceNode.restoreToNaturalSize()
|
||||
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
|
||||
|
||||
strongSelf.sourceNode.animatedOut?()
|
||||
|
||||
completion()
|
||||
}
|
||||
|
||||
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.initialSourceFrame = convertedFrame
|
||||
|
||||
if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame {
|
||||
let duration = 0.3
|
||||
let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
||||
|
||||
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
|
||||
updatedContentAreaInScreenSpace.origin.x = 0.0
|
||||
updatedContentAreaInScreenSpace.size.width = self.bounds.width
|
||||
|
||||
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.prepareImpact(.light)
|
||||
self.hapticFeedback?.impact(.light)
|
||||
|
||||
self.sourceNode.scaleUpdated?(1.0, transition)
|
||||
|
||||
let pinchOffset = CGPoint(
|
||||
x: pinchLocation.x - initialSourceFrame.width / 2.0,
|
||||
y: pinchLocation.y - initialSourceFrame.height / 2.0
|
||||
)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, scale, scale, 0.0)
|
||||
|
||||
self.sourceNode.contentNode.transform = CATransform3DIdentity
|
||||
self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY)
|
||||
self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0)
|
||||
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
|
||||
performCompletion()
|
||||
})
|
||||
|
||||
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve)
|
||||
dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
|
||||
} else {
|
||||
performCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
if self.isAnimatingOut {
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y)
|
||||
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public final class PinchControllerImpl: ViewController, PinchController, StandalonePresentableController {
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
private let disableScreenshots: Bool
|
||||
private let getContentAreaInScreenSpace: () -> CGRect
|
||||
|
||||
private var wasDismissed = false
|
||||
|
||||
private var controllerNode: PinchControllerNode {
|
||||
return self.displayNode as! PinchControllerNode
|
||||
}
|
||||
|
||||
public init(sourceNode: PinchSourceContainerNode, disableScreenshots: Bool = false, getContentAreaInScreenSpace: @escaping () -> CGRect) {
|
||||
self.sourceNode = sourceNode
|
||||
self.disableScreenshots = disableScreenshots
|
||||
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.lockOrientation = true
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, disableScreenshots: self.disableScreenshots, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self._ready.set(.single(true))
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
if self.ignoreAppearanceMethodInvocations() {
|
||||
return
|
||||
}
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.wasDismissed {
|
||||
self.wasDismissed = true
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import EmojiStatusComponent
|
||||
|
||||
final class ReactionPreviewView: UIView {
|
||||
private let context: AccountContext
|
||||
private let file: TelegramMediaFile
|
||||
|
||||
private let icon = ComponentView<Empty>()
|
||||
|
||||
init(context: AccountContext, file: TelegramMediaFile) {
|
||||
self.context = context
|
||||
self.file = file
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: self.context,
|
||||
animationCache: self.context.animationCache,
|
||||
animationRenderer: self.context.animationRenderer,
|
||||
content: .animation(
|
||||
content: .file(file: self.file),
|
||||
size: size,
|
||||
placeholderColor: .clear,
|
||||
themeColor: .white,
|
||||
loopMode: .forever
|
||||
),
|
||||
isVisibleForAnimations: true,
|
||||
action: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user