GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ContextUI",
module_name = "ContextUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TextSelectionNode:TextSelectionNode",
"//submodules/AppBundle:AppBundle",
"//submodules/AccountContext:AccountContext",
"//submodules/ReactionSelectionNode:ReactionSelectionNode",
"//submodules/Markdown:Markdown",
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/UndoUI:UndoUI",
"//submodules/AnimationUI:AnimationUI",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,972 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TextSelectionNode
import ReactionSelectionNode
import TelegramCore
import SwiftSignalKit
import AccountContext
import TextNodeWithEntities
import EntityKeyboard
import AnimationCache
import MultiAnimationRenderer
import UndoUI
import UIKitRuntimeUtils
public protocol ContextControllerProtocol: ViewController {
var useComplexItemsTransitionAnimation: Bool { get set }
var immediateItemsTransitionAnimation: Bool { get set }
var getOverlayViews: (() -> [UIView])? { get set }
func dismiss(completion: (() -> Void)?)
func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?)
func getActionsMinHeight() -> ContextController.ActionsHeight?
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, animated: Bool)
func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition)
func pushItems(items: Signal<ContextController.Items, NoError>)
func popItems()
}
public enum ContextMenuActionItemTextLayout {
case singleLine
case twoLinesMax
case secondLineWithValue(String)
case secondLineWithAttributedValue(NSAttributedString)
case multiline
}
public enum ContextMenuActionItemTextColor {
case primary
case destructive
case disabled
}
public enum ContextMenuActionResult {
case `default`
case dismissWithoutContent
case custom(ContainedViewLayoutTransition)
}
public enum ContextMenuActionItemFont {
case regular
case small
case custom(font: UIFont, height: CGFloat?, verticalOffset: CGFloat?)
}
public struct ContextMenuActionItemIconSource {
public let size: CGSize
public let contentMode: UIView.ContentMode
public let cornerRadius: CGFloat
public let signal: Signal<UIImage?, NoError>
public init(size: CGSize, contentMode: UIView.ContentMode = .scaleToFill, cornerRadius: CGFloat = 0.0, signal: Signal<UIImage?, NoError>) {
self.size = size
self.contentMode = contentMode
self.cornerRadius = cornerRadius
self.signal = signal
}
}
public enum ContextMenuActionItemIconPosition {
case left
case right
}
public enum ContextMenuActionBadgeColor {
case accent
case inactive
}
public struct ContextMenuActionBadge: Equatable {
public enum Style {
case badge
case label
}
public var value: String
public var color: ContextMenuActionBadgeColor
public var style: Style
public init(value: String, color: ContextMenuActionBadgeColor, style: Style = .badge) {
self.value = value
self.color = color
self.style = style
}
}
public final class ContextMenuActionItem {
public final class Action {
public let controller: ContextControllerProtocol?
public let dismissWithResult: (ContextMenuActionResult) -> Void
public let updateAction: (AnyHashable, ContextMenuActionItem) -> Void
public init(controller: ContextControllerProtocol?, dismissWithResult: @escaping (ContextMenuActionResult) -> Void, updateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) {
self.controller = controller
self.dismissWithResult = dismissWithResult
self.updateAction = updateAction
}
}
public struct IconAnimation: Equatable {
public var name: String
public var loop: Bool
public init(name: String, loop: Bool = false) {
self.name = name
self.loop = loop
}
}
public let id: AnyHashable?
public let text: String
public let entities: [MessageTextEntity]
public let entityFiles: [Int64: TelegramMediaFile]
public let enableEntityAnimations: Bool
public let textColor: ContextMenuActionItemTextColor
public let textFont: ContextMenuActionItemFont
public let textLayout: ContextMenuActionItemTextLayout
public let customTextInsets: UIEdgeInsets?
public let parseMarkdown: Bool
public let badge: ContextMenuActionBadge?
public let icon: (PresentationTheme) -> UIImage?
public let additionalLeftIcon: ((PresentationTheme) -> UIImage?)?
public let iconSource: ContextMenuActionItemIconSource?
public let iconPosition: ContextMenuActionItemIconPosition
public let animationName: String?
public let iconAnimation: IconAnimation?
public let textIcon: (PresentationTheme) -> UIImage?
public let textLinkAction: () -> Void
public let action: ((Action) -> Void)?
public let longPressAction: ((Action) -> Void)?
convenience public init(
id: AnyHashable? = nil,
text: String,
entities: [MessageTextEntity] = [],
entityFiles: [Int64: TelegramMediaFile] = [:],
enableEntityAnimations: Bool = true,
textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
customTextInsets: UIEdgeInsets? = nil,
textFont: ContextMenuActionItemFont = .regular,
parseMarkdown: Bool = false,
badge: ContextMenuActionBadge? = nil,
icon: @escaping (PresentationTheme) -> UIImage?,
additionalLeftIcon: ((PresentationTheme) -> UIImage?)? = nil,
iconSource: ContextMenuActionItemIconSource? = nil,
iconPosition: ContextMenuActionItemIconPosition = .right,
animationName: String? = nil,
iconAnimation: IconAnimation? = nil,
textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil },
textLinkAction: @escaping () -> Void = {},
action: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)?,
longPressAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
) {
self.init(
id: id,
text: text,
entities: entities,
entityFiles: entityFiles,
enableEntityAnimations: enableEntityAnimations,
textColor: textColor,
textLayout: textLayout,
customTextInsets: customTextInsets,
textFont: textFont,
parseMarkdown: parseMarkdown,
badge: badge,
icon: icon,
additionalLeftIcon: additionalLeftIcon,
iconSource: iconSource,
iconPosition: iconPosition,
animationName: animationName,
iconAnimation: iconAnimation,
textIcon: textIcon,
textLinkAction: textLinkAction,
action: action.flatMap { action in
return { impl in
action(impl.controller, impl.dismissWithResult)
}
},
longPressAction: longPressAction.flatMap { longPressAction in
return { impl in
longPressAction(impl.controller, impl.dismissWithResult)
}
}
)
}
public init(
id: AnyHashable? = nil,
text: String,
entities: [MessageTextEntity] = [],
entityFiles: [Int64: TelegramMediaFile] = [:],
enableEntityAnimations: Bool = true,
textColor: ContextMenuActionItemTextColor = .primary,
textLayout: ContextMenuActionItemTextLayout = .twoLinesMax,
customTextInsets: UIEdgeInsets? = nil,
textFont: ContextMenuActionItemFont = .regular,
parseMarkdown: Bool = false,
badge: ContextMenuActionBadge? = nil,
icon: @escaping (PresentationTheme) -> UIImage?,
additionalLeftIcon: ((PresentationTheme) -> UIImage?)? = nil,
iconSource: ContextMenuActionItemIconSource? = nil,
iconPosition: ContextMenuActionItemIconPosition = .right,
animationName: String? = nil,
iconAnimation: IconAnimation? = nil,
textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil },
textLinkAction: @escaping () -> Void = {},
action: ((Action) -> Void)?,
longPressAction: ((Action) -> Void)? = nil
) {
self.id = id
self.text = text
self.entities = entities
self.entityFiles = entityFiles
self.enableEntityAnimations = enableEntityAnimations
self.textColor = textColor
self.textFont = textFont
self.textLayout = textLayout
self.customTextInsets = customTextInsets
self.parseMarkdown = parseMarkdown
self.badge = badge
self.icon = icon
self.additionalLeftIcon = additionalLeftIcon
self.iconSource = iconSource
self.iconPosition = iconPosition
self.animationName = animationName
self.iconAnimation = iconAnimation
self.textIcon = textIcon
self.textLinkAction = textLinkAction
self.action = action
self.longPressAction = longPressAction
}
}
public protocol ContextMenuCustomNode: ASDisplayNode {
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void)
func updateTheme(presentationData: PresentationData)
func canBeHighlighted() -> Bool
func updateIsHighlighted(isHighlighted: Bool)
func performAction()
var needsSeparator: Bool { get }
var needsPadding: Bool { get }
}
public extension ContextMenuCustomNode {
var needsSeparator: Bool {
return true
}
var needsPadding: Bool {
return true
}
}
public protocol ContextMenuCustomItem {
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode
}
public enum ContextMenuItem {
case action(ContextMenuActionItem)
case custom(ContextMenuCustomItem, Bool)
case separator
}
public final class ContextControllerLocationViewInfo {
public let location: CGPoint
public let contentAreaInScreenSpace: CGRect
public let insets: UIEdgeInsets
public init(location: CGPoint, contentAreaInScreenSpace: CGRect, insets: UIEdgeInsets = UIEdgeInsets()) {
self.location = location
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.insets = insets
}
}
public protocol ContextLocationContentSource: AnyObject {
var shouldBeDismissed: Signal<Bool, NoError> { get }
func transitionInfo() -> ContextControllerLocationViewInfo?
}
public extension ContextLocationContentSource {
var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false)
}
}
public final class ContextControllerReferenceViewInfo {
public enum ActionsPosition {
case bottom
case top
}
public let referenceView: UIView
public let contentAreaInScreenSpace: CGRect
public let insets: UIEdgeInsets
public let customPosition: CGPoint?
public let actionsPosition: ActionsPosition
public init(referenceView: UIView, contentAreaInScreenSpace: CGRect, insets: UIEdgeInsets = UIEdgeInsets(), customPosition: CGPoint? = nil, actionsPosition: ActionsPosition = .bottom) {
self.referenceView = referenceView
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.insets = insets
self.customPosition = customPosition
self.actionsPosition = actionsPosition
}
}
public protocol ContextReferenceContentSource: AnyObject {
var keepInPlace: Bool { get }
var shouldBeDismissed: Signal<Bool, NoError> { get }
var forceDisplayBelowKeyboard: Bool { get }
func transitionInfo() -> ContextControllerReferenceViewInfo?
}
public extension ContextReferenceContentSource {
var keepInPlace: Bool {
return false
}
var forceDisplayBelowKeyboard: Bool {
return false
}
var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false)
}
}
public final class ContextControllerTakeViewInfo {
public enum ContainingItem {
case node(ContextExtractedContentContainingNode)
case view(ContextExtractedContentContainingView)
}
public let containingItem: ContainingItem
public let contentAreaInScreenSpace: CGRect
public let maskView: UIView?
public init(containingItem: ContainingItem, contentAreaInScreenSpace: CGRect, maskView: UIView? = nil) {
self.containingItem = containingItem
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.maskView = maskView
}
}
public final class ContextControllerPutBackViewInfo {
public let contentAreaInScreenSpace: CGRect
public let maskView: UIView?
public init(contentAreaInScreenSpace: CGRect, maskView: UIView? = nil) {
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.maskView = maskView
}
}
public enum ContextActionsHorizontalAlignment {
case `default`
case left
case center
case right
}
public protocol ContextExtractedContentSource: AnyObject {
var initialAppearanceOffset: CGPoint { get }
var centerVertically: Bool { get }
var keepInPlace: Bool { get }
var adjustContentHorizontally: Bool { get }
var adjustContentForSideInset: Bool { get }
var ignoreContentTouches: Bool { get }
var keepDefaultContentTouches: Bool { get }
var blurBackground: Bool { get }
var shouldBeDismissed: Signal<Bool, NoError> { get }
var additionalInsets: UIEdgeInsets { get }
var actionsHorizontalAlignment: ContextActionsHorizontalAlignment { get }
func takeView() -> ContextControllerTakeViewInfo?
func putBack() -> ContextControllerPutBackViewInfo?
}
public extension ContextExtractedContentSource {
var initialAppearanceOffset: CGPoint {
return .zero
}
var centerVertically: Bool {
return false
}
var adjustContentHorizontally: Bool {
return false
}
var adjustContentForSideInset: Bool {
return false
}
var additionalInsets: UIEdgeInsets {
return .zero
}
var actionsHorizontalAlignment: ContextActionsHorizontalAlignment {
return .default
}
var shouldBeDismissed: Signal<Bool, NoError> {
return .single(false)
}
var keepDefaultContentTouches: Bool {
return false
}
}
public final class ContextControllerTakeControllerInfo {
public let contentAreaInScreenSpace: CGRect
public let sourceNode: () -> (UIView, CGRect)?
public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (UIView, CGRect)?) {
self.contentAreaInScreenSpace = contentAreaInScreenSpace
self.sourceNode = sourceNode
}
}
public protocol ContextControllerContentSource: AnyObject {
var controller: ViewController { get }
var navigationController: NavigationController? { get }
var passthroughTouches: Bool { get }
func transitionInfo() -> ContextControllerTakeControllerInfo?
func animatedIn()
}
public enum ContextContentSource {
case location(ContextLocationContentSource)
case reference(ContextReferenceContentSource)
case extracted(ContextExtractedContentSource)
case controller(ContextControllerContentSource)
}
public protocol ContextControllerItemsNode: ASDisplayNode {
func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat)
var apparentHeight: CGFloat { get }
}
public protocol ContextControllerItemsContent: AnyObject {
func node(
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerItemsNode
}
public final class ContextControllerSource {
public let id: AnyHashable
public let title: String
public let footer: String?
public let source: ContextContentSource
public let items: Signal<ContextController.Items, NoError>
public let closeActionTitle: String?
public let closeAction: (() -> Void)?
public init(
id: AnyHashable,
title: String,
footer: String? = nil,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>,
closeActionTitle: String? = nil,
closeAction: (() -> Void)? = nil
) {
self.id = id
self.title = title
self.footer = footer
self.source = source
self.items = items
self.closeActionTitle = closeActionTitle
self.closeAction = closeAction
}
}
public final class ContextControllerConfiguration {
public let sources: [ContextControllerSource]
public let initialId: AnyHashable
public init(sources: [ContextControllerSource], initialId: AnyHashable) {
self.sources = sources
self.initialId = initialId
}
}
public struct ContextControllerItems {
public enum Content {
case list([ContextMenuItem])
case twoLists([ContextMenuItem], [ContextMenuItem])
case custom(ContextControllerItemsContent)
}
public var id: AnyHashable?
public var content: Content
public var context: AccountContext?
public var reactionItems: [ReactionContextItem]
public var selectedReactionItems: Set<MessageReaction.Reaction>
public var reactionsTitle: String?
public var reactionsLocked: Bool
public var animationCache: AnimationCache?
public var alwaysAllowPremiumReactions: Bool
public var allPresetReactionsAreAvailable: Bool
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
public var disablePositionLock: Bool
public var previewReaction: TelegramMediaFile?
public var tip: ContextControllerTip?
public var tipSignal: Signal<ContextControllerTip?, NoError>?
public var dismissed: (() -> Void)?
public init(
id: AnyHashable? = nil,
content: Content,
context: AccountContext? = nil,
reactionItems: [ReactionContextItem] = [],
selectedReactionItems: Set<MessageReaction.Reaction> = Set(),
reactionsTitle: String? = nil,
reactionsLocked: Bool = false,
animationCache: AnimationCache? = nil,
alwaysAllowPremiumReactions: Bool = false,
allPresetReactionsAreAvailable: Bool = false,
getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)? = nil,
disablePositionLock: Bool = false,
previewReaction: TelegramMediaFile? = nil,
tip: ContextControllerTip? = nil,
tipSignal: Signal<ContextControllerTip?, NoError>? = nil,
dismissed: (() -> Void)? = nil
) {
self.id = id
self.content = content
self.context = context
self.animationCache = animationCache
self.reactionItems = reactionItems
self.selectedReactionItems = selectedReactionItems
self.reactionsTitle = reactionsTitle
self.reactionsLocked = reactionsLocked
self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions
self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable
self.getEmojiContent = getEmojiContent
self.disablePositionLock = disablePositionLock
self.previewReaction = previewReaction
self.tip = tip
self.tipSignal = tipSignal
self.dismissed = dismissed
}
public init() {
self.id = nil
self.content = .list([])
self.context = nil
self.reactionItems = []
self.selectedReactionItems = Set()
self.reactionsTitle = nil
self.reactionsLocked = false
self.alwaysAllowPremiumReactions = false
self.allPresetReactionsAreAvailable = false
self.getEmojiContent = nil
self.disablePositionLock = false
self.previewReaction = nil
self.tip = nil
self.tipSignal = nil
self.dismissed = nil
}
}
public enum ContextControllerPreviousActionsTransition {
case scale
case slide(forward: Bool)
}
public enum ContextControllerTip: Equatable {
case textSelection
case quoteSelection
case messageViewsPrivacy
case messageCopyProtection(text: String)
case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?)
case notificationTopicExceptions(text: String, action: (() -> Void)?)
case starsReactions(topCount: Int)
case videoProcessing
case collageReordering
public static func ==(lhs: ContextControllerTip, rhs: ContextControllerTip) -> Bool {
switch lhs {
case .textSelection:
if case .textSelection = rhs {
return true
} else {
return false
}
case .quoteSelection:
if case .quoteSelection = rhs {
return true
} else {
return false
}
case .messageViewsPrivacy:
if case .messageViewsPrivacy = rhs {
return true
} else {
return false
}
case let .messageCopyProtection(text):
if case .messageCopyProtection(text) = rhs {
return true
} else {
return false
}
case let .animatedEmoji(text, _, file, _):
if case let .animatedEmoji(rhsText, _, rhsFile, _) = rhs {
if text != rhsText {
return false
}
if file?.fileId != rhsFile?.fileId {
return false
}
return true
} else {
return false
}
case let .notificationTopicExceptions(text, _):
if case .notificationTopicExceptions(text, _) = rhs {
return true
} else {
return false
}
case let .starsReactions(topCount):
if case .starsReactions(topCount) = rhs {
return true
} else {
return false
}
case .videoProcessing:
if case .videoProcessing = rhs {
return true
} else {
return false
}
case .collageReordering:
if case .collageReordering = rhs {
return true
} else {
return false
}
}
}
}
public final class ContextControllerActionsHeight {
public let minY: CGFloat
public let contentOffset: CGFloat
public init(minY: CGFloat, contentOffset: CGFloat) {
self.minY = minY
self.contentOffset = contentOffset
}
}
public enum ContextControllerHandledTouchEvent {
case ignore
case dismiss(consume: Bool, result: UIView?)
}
public enum ContextActionSibling {
case none
case item
case separator
}
public protocol ContextActionNodeProtocol: ASDisplayNode {
func setIsHighlighted(_ value: Bool)
func performAction()
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol
var isActionEnabled: Bool { get }
}
public protocol ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder {
typealias ContentSource = ContextContentSource
typealias ItemsNode = ContextControllerItemsNode
typealias ItemsContent = ContextControllerItemsContent
typealias Source = ContextControllerSource
typealias Configuration = ContextControllerConfiguration
typealias Items = ContextControllerItems
typealias PreviousActionsTransition = ContextControllerPreviousActionsTransition
typealias Tip = ContextControllerTip
typealias ActionsHeight = ContextControllerActionsHeight
typealias HandledTouchEvent = ContextControllerHandledTouchEvent
var dismissed: (() -> Void)? { get set }
var dismissedForCancel: (() -> Void)? { get set }
var passthroughTouchEvent: ((UIView, CGPoint) -> HandledTouchEvent)? { get set }
var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? { get set }
var premiumReactionsSelected: (() -> Void)? { get set }
var getOverlayViews: (() -> [UIView])? { get set }
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
func updateTheme(presentationData: PresentationData)
func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)?)
func dismissWithoutContent()
func dismissNow()
func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: (() -> Void)?)
func animateDismissalIfNeeded()
func cancelReactionAnimation()
}
public func makeContextController(
context: AccountContext? = nil,
presentationData: PresentationData,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>,
recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil,
gesture: ContextGesture? = nil,
workaroundUseLegacyImplementation: Bool = false,
disableScreenshots: Bool = false,
hideReactionPanelTail: Bool = false
) -> ContextController {
return makeContextController(
context: context,
presentationData: presentationData,
configuration: ContextController.Configuration(
sources: [ContextController.Source(
id: AnyHashable(0 as Int),
title: "",
source: source,
items: items
)],
initialId: AnyHashable(0 as Int)
),
recognizer: recognizer,
gesture: gesture,
workaroundUseLegacyImplementation: workaroundUseLegacyImplementation,
disableScreenshots: disableScreenshots,
hideReactionPanelTail: hideReactionPanelTail
)
}
public var makeContextControllerImpl: ((
_ context: AccountContext?,
_ presentationData: PresentationData,
_ configuration: ContextController.Configuration,
_ recognizer: TapLongTapOrDoubleTapGestureRecognizer?,
_ gesture: ContextGesture?,
_ workaroundUseLegacyImplementation: Bool,
_ disableScreenshots: Bool,
_ hideReactionPanelTail: Bool
) -> ContextController)? = nil
public func makeContextController(
context: AccountContext? = nil,
presentationData: PresentationData,
configuration: ContextController.Configuration,
recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil,
gesture: ContextGesture? = nil,
workaroundUseLegacyImplementation: Bool = false,
disableScreenshots: Bool = false,
hideReactionPanelTail: Bool = false
) -> ContextController {
return makeContextControllerImpl!(
context,
presentationData,
configuration,
recognizer,
gesture,
workaroundUseLegacyImplementation,
disableScreenshots,
hideReactionPanelTail
)
}
public enum ContextControllerActionsStackNodePresentation {
case modal
case inline
case additional
}
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
var wantsFullWidth: Bool { get }
func update(
presentationData: PresentationData,
constrainedSize: CGSize,
standardMinWidth: CGFloat,
standardMaxWidth: CGFloat,
additionalBottomInset: CGFloat,
transition: ContainedViewLayoutTransition
) -> (size: CGSize, apparentHeight: CGFloat)
func highlightGestureShouldBegin(location: CGPoint) -> Bool
func highlightGestureMoved(location: CGPoint)
func highlightGestureFinished(performAction: Bool)
func decreaseHighlightedIndex()
func increaseHighlightedIndex()
}
public protocol ContextControllerActionsStackItem: AnyObject {
func node(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerActionsStackItemNode
var id: AnyHashable? { get }
var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: ContextControllerReactionItems? { get }
var previewReaction: ContextControllerPreviewReaction? { get }
var dismissed: (() -> Void)? { get }
}
public struct ContextControllerReactionItems {
public var context: AccountContext
public var reactionItems: [ReactionContextItem]
public var selectedReactionItems: Set<MessageReaction.Reaction>
public var reactionsTitle: String?
public var reactionsLocked: Bool
public var animationCache: AnimationCache
public var alwaysAllowPremiumReactions: Bool
public var allPresetReactionsAreAvailable: Bool
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
public init(context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, reactionsLocked: Bool, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?) {
self.context = context
self.reactionItems = reactionItems
self.selectedReactionItems = selectedReactionItems
self.reactionsTitle = reactionsTitle
self.reactionsLocked = reactionsLocked
self.animationCache = animationCache
self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions
self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable
self.getEmojiContent = getEmojiContent
}
}
public final class ContextControllerPreviewReaction {
public let context: AccountContext
public let file: TelegramMediaFile
public init(context: AccountContext, file: TelegramMediaFile) {
self.context = context
self.file = file
}
}
public protocol ContextControllerActionsStackNode: ASDisplayNode {
typealias Presentation = ContextControllerActionsStackNodePresentation
var topReactionItems: ContextControllerReactionItems? { get }
var topPreviewReaction: ContextControllerPreviewReaction? { get }
var topPositionLock: CGFloat? { get }
var storedScrollingState: CGFloat? { get }
func replace(item: ContextControllerActionsStackItem, animated: Bool?)
func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool)
func clearStoredScrollingState()
func pop()
func update(
presentationData: PresentationData,
constrainedSize: CGSize,
presentation: Presentation,
transition: ContainedViewLayoutTransition
) -> CGSize
func highlightGestureMoved(location: CGPoint)
func highlightGestureFinished(performAction: Bool)
func decreaseHighlightedIndex()
func increaseHighlightedIndex()
func updatePanSelection(isEnabled: Bool)
func animateIn()
}
public var makeContextControllerActionsListStackItemImpl: ((
_ id: AnyHashable?,
_ items: [ContextMenuItem],
_ reactionItems: ContextControllerReactionItems?,
_ previewReaction: ContextControllerPreviewReaction?,
_ tip: ContextController.Tip?,
_ tipSignal: Signal<ContextController.Tip?, NoError>?,
_ dismissed: (() -> Void)?
) -> ContextControllerActionsStackItem)?
public func makeContextControllerActionsListStackItem(
id: AnyHashable?,
items: [ContextMenuItem],
reactionItems: ContextControllerReactionItems?,
previewReaction: ContextControllerPreviewReaction?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
) -> ContextControllerActionsStackItem {
return makeContextControllerActionsListStackItemImpl!(
id,
items,
reactionItems,
previewReaction,
tip,
tipSignal,
dismissed
)
}
public var makeContextControllerActionsStackNodeImpl: ((
_ context: AccountContext?,
_ getController: @escaping () -> ContextControllerProtocol?,
_ requestDismiss: @escaping (ContextMenuActionResult) -> Void,
_ requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerActionsStackNode)?
public func makeContextControllerActionsStackNode(
context: AccountContext?,
getController: @escaping () -> ContextControllerProtocol?,
requestDismiss: @escaping (ContextMenuActionResult) -> Void,
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerActionsStackNode {
return makeContextControllerActionsStackNodeImpl!(
context,
getController,
requestDismiss,
requestUpdate
)
}
public var makeContextActionNodeImpl: ((
_ presentationData: PresentationData,
_ action: ContextMenuActionItem,
_ getController: @escaping () -> ContextControllerProtocol?,
_ actionSelected: @escaping (ContextMenuActionResult) -> Void,
_ requestLayout: @escaping () -> Void,
_ requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void
) -> ContextActionNodeProtocol)?
public func makeContextActionNode(
presentationData: PresentationData,
action: ContextMenuActionItem,
getController: @escaping () -> ContextControllerProtocol?,
actionSelected: @escaping (ContextMenuActionResult) -> Void,
requestLayout: @escaping () -> Void,
requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void
) -> ContextActionNodeProtocol {
return makeContextActionNodeImpl!(
presentationData,
action,
getController,
actionSelected,
requestLayout,
requestUpdateAction
)
}
@@ -0,0 +1,94 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
public enum PeekControllerContentPresentation {
case contained
case freeform
}
public enum PeerControllerMenuActivation {
case drag
case press
}
public protocol PeekControllerContent {
func presentation() -> PeekControllerContentPresentation
func menuActivation() -> PeerControllerMenuActivation
func menuItems() -> [ContextMenuItem]
func node() -> PeekControllerContentNode & ASDisplayNode
func topAccessoryNode() -> ASDisplayNode?
func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)?
func isEqual(to: PeekControllerContent) -> Bool
}
public protocol PeekControllerContentNode {
func ready() -> Signal<Bool, NoError>
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
}
public protocol PeekControllerAccessoryNode {
var dismiss: () -> Void { get set }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
}
public final class PeekControllerTheme {
public let isDark: Bool
public let menuBackgroundColor: UIColor
public let menuItemHighligtedColor: UIColor
public let menuItemSeparatorColor: UIColor
public let accentColor: UIColor
public let destructiveColor: UIColor
public init(isDark: Bool, menuBackgroundColor: UIColor, menuItemHighligtedColor: UIColor, menuItemSeparatorColor: UIColor, accentColor: UIColor, destructiveColor: UIColor) {
self.isDark = isDark
self.menuBackgroundColor = menuBackgroundColor
self.menuItemHighligtedColor = menuItemHighligtedColor
self.menuItemSeparatorColor = menuItemSeparatorColor
self.accentColor = accentColor
self.destructiveColor = destructiveColor
}
}
extension PeekControllerTheme {
convenience public init(presentationTheme: PresentationTheme) {
let actionSheet = presentationTheme.actionSheet
self.init(isDark: actionSheet.backgroundType == .dark, menuBackgroundColor: actionSheet.opaqueItemBackgroundColor, menuItemHighligtedColor: actionSheet.opaqueItemHighlightedBackgroundColor, menuItemSeparatorColor: actionSheet.opaqueItemSeparatorColor, accentColor: actionSheet.controlAccentColor, destructiveColor: actionSheet.destructiveActionTextColor)
}
}
public protocol PeekController: ViewController, ContextControllerProtocol {
var visibilityUpdated: ((Bool) -> Void)? { get set }
var getOverlayViews: (() -> [UIView])? { get set }
var appeared: (() -> Void)? { get set }
var disappeared: (() -> Void)? { get set }
var sourceView: () -> (UIView, CGRect)? { get set }
var contentNode: PeekControllerContentNode & ASDisplayNode { get }
}
public var makePeekControllerImpl: ((
_ presentationData: PresentationData,
_ content: PeekControllerContent,
_ sourceView: @escaping () -> (UIView, CGRect)?,
_ activateImmediately: Bool
) -> PeekController)?
public func makePeekController(
presentationData: PresentationData,
content: PeekControllerContent,
sourceView: @escaping () -> (UIView, CGRect)?,
activateImmediately: Bool = false
) -> PeekController {
return makePeekControllerImpl!(
presentationData,
content,
sourceView,
activateImmediately
)
}
@@ -0,0 +1,328 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
public protocol PeekControllerNodeProtocol: AnyObject {
func applyDraggingOffset(_ offset: CGPoint)
func activateMenu(immediately: Bool)
func endDragging(_ location: CGPoint)
func updateContent(content: PeekControllerContent)
}
private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool {
if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating {
return true
}
for subview in view.subviews {
let subviewPoint = view.convert(point, to: subview)
if traceDeceleratingScrollView(subview, at: subviewPoint) {
return true
}
}
return false
}
public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer {
private let contentAtPoint: (CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>?
private let present: (PeekControllerContent, UIView, CGRect) -> ViewController?
private let updateContent: (PeekControllerContent?) -> Void
private let activateBySingleTap: Bool
public var longPressEnabled = true
public var checkSingleTapActivationAtPoint: ((CGPoint) -> Bool)?
private var tapLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var pressTimer: SwiftSignalKit.Timer?
private let candidateContentDisposable = MetaDisposable()
private var candidateContent: (UIView, CGRect, PeekControllerContent)? {
didSet {
self.updateContent(self.candidateContent?.2)
}
}
private var menuActivation: PeerControllerMenuActivation?
private weak var presentedController: PeekController?
public init(contentAtPoint: @escaping (CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, UIView, CGRect) -> ViewController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) {
self.contentAtPoint = contentAtPoint
self.present = present
self.updateContent = updateContent
self.activateBySingleTap = activateBySingleTap
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
self.candidateContentDisposable.dispose()
}
private func startLongTapTimer() {
guard self.longPressEnabled else {
return
}
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func startPressTimer() {
self.pressTimer?.invalidate()
let pressTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
self?.pressTimerFired()
}, queue: Queue.mainQueue())
self.pressTimer = pressTimer
pressTimer.start()
}
private func stopLongTapTimer() {
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func stopPressTimer() {
self.pressTimer?.invalidate()
self.pressTimer = nil
}
override public func reset() {
super.reset()
self.stopLongTapTimer()
self.stopPressTimer()
self.tapLocation = nil
self.candidateContent = nil
self.menuActivation = nil
self.presentedController = nil
}
private func longTapTimerFired() {
guard let tapLocation = self.tapLocation else {
return
}
self.checkCandidateContent(at: tapLocation)
}
private func pressTimerFired() {
if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation {
if let presentedController = self.presentedController {
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNodeProtocol)?.activateMenu(immediately: false)
}
self.menuActivation = nil
// self.presentedController = nil
// self.state = .ended
}
}
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let view = self.view, let tapLocation = touches.first?.location(in: view) {
if traceDeceleratingScrollView(view, at: tapLocation) {
self.candidateContent = nil
self.state = .failed
} else {
self.tapLocation = tapLocation
self.startLongTapTimer()
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
var activateBySingleTap = self.activateBySingleTap
if !activateBySingleTap, let checkSingleTapActivationAtPoint = self.checkSingleTapActivationAtPoint, let tapLocation = self.tapLocation {
activateBySingleTap = checkSingleTapActivationAtPoint(tapLocation)
}
if activateBySingleTap, self.presentedController == nil {
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
if let tapLocation = self.tapLocation {
self.checkCandidateContent(at: tapLocation, forceActivate: true)
}
self.state = .ended
} else {
if let presentedController = self.presentedController, presentedController.isNodeLoaded, let location = touches.first?.location(in: presentedController.view) {
(presentedController.displayNode as? PeekControllerNodeProtocol)?.endDragging(location)
self.presentedController = nil
self.menuActivation = nil
}
self.tapLocation = nil
self.candidateContent = nil
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
self.candidateContentDisposable.set(nil)
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
if let presentedController = self.presentedController {
self.menuActivation = nil
self.presentedController = nil
presentedController.dismiss()
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first, let initialTapLocation = self.tapLocation {
let touchLocation = touch.location(in: self.view)
if let presentedController = self.presentedController, self.menuActivation == nil {
if presentedController.isNodeLoaded {
let touchLocation = touch.location(in: presentedController.view)
(presentedController.displayNode as? PeekControllerNodeProtocol)?.applyDraggingOffset(touchLocation)
}
} else if let menuActivation = self.menuActivation, let presentedController = self.presentedController {
switch menuActivation {
case .drag:
var offset = touchLocation.y - initialTapLocation.y
let delta = abs(offset)
let factor: CGFloat = 60.0
offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0)
if presentedController.isNodeLoaded {
// (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset)
}
case .press:
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if touch.force >= 2.5 {
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNodeProtocol)?.activateMenu(immediately: false)
self.menuActivation = nil
self.presentedController = nil
self.candidateContent = nil
self.state = .ended
self.candidateContentDisposable.set(nil)
return
}
}
}
if self.pressTimer != nil {
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.startPressTimer()
}
}
if self.presentedController != nil {
self.checkCandidateContent(at: touchLocation)
}
}
} else {
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
}
}
}
}
private func checkCandidateContent(at touchLocation: CGPoint, forceActivate: Bool = false) {
//print("check begin")
if let contentSignal = self.contentAtPoint(touchLocation) {
self.candidateContentDisposable.set((contentSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
let processResult: Bool
if forceActivate {
processResult = true
} else {
switch strongSelf.state {
case .possible, .changed:
processResult = true
default:
processResult = false
}
}
//print("check received, will process: \(processResult), force: \(forceActivate), state: \(strongSelf.state)")
if processResult {
if let (sourceView, sourceRect, content) = result {
if let currentContent = strongSelf.candidateContent {
if !currentContent.2.isEqual(to: content) {
strongSelf.tapLocation = touchLocation
strongSelf.candidateContent = (sourceView, sourceRect, content)
strongSelf.menuActivation = content.menuActivation()
if let presentedController = strongSelf.presentedController, presentedController.isNodeLoaded {
presentedController.sourceView = {
return (sourceView, sourceRect)
}
(presentedController.displayNode as? PeekControllerNodeProtocol)?.updateContent(content: content)
}
}
} else {
if let presentedController = strongSelf.present(content, sourceView, sourceRect) {
if let presentedController = presentedController as? PeekController {
if forceActivate {
strongSelf.candidateContent = nil
if case .press = content.menuActivation() {
(presentedController.displayNode as? PeekControllerNodeProtocol)?.activateMenu(immediately: false)
}
} else {
strongSelf.candidateContent = (sourceView, sourceRect, content)
strongSelf.menuActivation = content.menuActivation()
strongSelf.presentedController = presentedController
strongSelf.state = .began
switch content.menuActivation() {
case .drag:
break
case .press:
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if presentedController.traitCollection.forceTouchCapability != .available {
strongSelf.startPressTimer()
}
} else {
strongSelf.startPressTimer()
}
}
}
} else {
if strongSelf.state != .ended {
strongSelf.state = .ended
}
}
}
}
} else if strongSelf.presentedController == nil {
if strongSelf.state != .possible && strongSelf.state != .ended {
strongSelf.state = .failed
}
}
}
}
}))
} else if self.presentedController == nil {
self.state = .failed
}
}
}
@@ -0,0 +1,27 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
public protocol PinchController: ViewController {
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
}
public var makePinchControllerImpl: ((
_ sourceNode: PinchSourceContainerNode,
_ disableScreenshots: Bool,
_ getContentAreaInScreenSpace: @escaping () -> CGRect
) -> PinchController)?
public func makePinchController(
sourceNode: PinchSourceContainerNode,
disableScreenshots: Bool = false,
getContentAreaInScreenSpace: @escaping () -> CGRect
) -> PinchController {
return makePinchControllerImpl!(
sourceNode,
disableScreenshots,
getContentAreaInScreenSpace
)
}
@@ -0,0 +1,226 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private func cancelContextGestures(node: ASDisplayNode) {
if let node = node as? ContextControllerSourceNode {
node.cancelGesture()
}
if let supernode = node.supernode {
cancelContextGestures(node: supernode)
}
}
private func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer {
recognizer.cancel()
} else if let recognizer = recognizer as? WindowPanRecognizer {
recognizer.cancel()
}
}
}
if let superview = view.superview {
cancelContextGestures(view: superview)
}
}
public final class PinchSourceGesture: UIPinchGestureRecognizer {
private final class Target {
var updated: (() -> Void)?
@objc func onGesture(_ gesture: UIPinchGestureRecognizer) {
self.updated?()
}
}
private let target: Target
public private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
public var began: (() -> Void)?
public var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
public var ended: (() -> Void)?
private var initialLocation: CGPoint?
private var pinchLocation = CGPoint()
private var currentOffset = CGPoint()
private var currentNumberOfTouches = 0
public init() {
self.target = Target()
super.init(target: self.target, action: #selector(self.target.onGesture(_:)))
self.target.updated = { [weak self] in
self?.gestureUpdated()
}
}
override public func reset() {
super.reset()
self.currentNumberOfTouches = 0
self.initialLocation = nil
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
//self.currentTouches.formUnion(touches)
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
}
private func gestureUpdated() {
switch self.state {
case .began:
self.currentOffset = CGPoint()
let pinchLocation = self.location(in: self.view)
self.pinchLocation = pinchLocation
self.initialLocation = pinchLocation
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.currentNumberOfTouches = self.numberOfTouches
self.began?()
case .changed:
let locationSum = self.location(in: self.view)
if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 {
self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y)
}
self.currentNumberOfTouches = self.numberOfTouches
if let initialLocation = self.initialLocation {
self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y)
}
if let (scale, pinchLocation, _) = self.currentTransform {
self.currentTransform = (scale, pinchLocation, self.currentOffset)
self.updated?(scale, pinchLocation, self.currentOffset)
}
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.updated?(scale, self.pinchLocation, self.currentOffset)
case .ended, .cancelled:
self.ended?()
default:
break
}
}
}
public final class PinchSourceContainerNode: ASDisplayNode, ASGestureRecognizerDelegate {
public let contentNode: ASDisplayNode
public var contentRect: CGRect = CGRect()
private(set) var naturalContentFrame: CGRect?
public let gesture: PinchSourceGesture
public var isPinchGestureEnabled: Bool = true {
didSet {
if self.isPinchGestureEnabled != oldValue {
self.gesture.isEnabled = self.isPinchGestureEnabled
}
}
}
public var maxPinchScale: CGFloat = 10.0
private var isActive: Bool = false
public var activate: ((PinchSourceContainerNode) -> Void)?
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
public var animatedOut: (() -> Void)?
public var deactivate: (() -> Void)?
public var deactivated: (() -> Void)?
public var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
override public init() {
self.gesture = PinchSourceGesture()
self.contentNode = ASDisplayNode()
super.init()
self.addSubnode(self.contentNode)
self.gesture.began = { [weak self] in
guard let strongSelf = self else {
return
}
cancelContextGestures(node: strongSelf)
cancelContextGestures(view: strongSelf.view)
strongSelf.isActive = true
strongSelf.activate?(strongSelf)
}
self.gesture.ended = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isActive = false
strongSelf.deactivate?()
strongSelf.deactivated?()
}
self.gesture.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self else {
return
}
strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset)
strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate)
}
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(self.gesture)
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let strongSelf = self else {
return false
}
return strongSelf.isActive
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
let contentFrame = CGRect(origin: CGPoint(), size: size)
self.naturalContentFrame = contentFrame
if !self.isActive {
transition.updateFrame(node: self.contentNode, frame: contentFrame)
}
}
public func restoreToNaturalSize() {
guard let naturalContentFrame = self.naturalContentFrame else {
return
}
self.contentNode.frame = naturalContentFrame
}
}