Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "QuickShareScreen",
module_name = "QuickShareScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/PresentationDataUtils",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,644 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TextFormat
import TelegramPresentationData
import MultilineTextComponent
import AccountContext
import ViewControllerComponent
import AvatarNode
import ComponentDisplayAdapters
private let largeCircleSize: CGFloat = 16.0
private let smallCircleSize: CGFloat = 8.0
private final class QuickShareScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let sourceNode: ASDisplayNode
let gesture: ContextGesture
let completion: (EnginePeer, CGRect) -> Void
let ready: Promise<Bool>
init(
context: AccountContext,
sourceNode: ASDisplayNode,
gesture: ContextGesture,
completion: @escaping (EnginePeer, CGRect) -> Void,
ready: Promise<Bool>
) {
self.context = context
self.sourceNode = sourceNode
self.gesture = gesture
self.completion = completion
self.ready = ready
}
static func ==(lhs: QuickShareScreenComponent, rhs: QuickShareScreenComponent) -> Bool {
return true
}
final class View: UIView {
private let backgroundShadowLayer: SimpleLayer
private let backgroundView: BlurredBackgroundView
private let backgroundTintView: UIView
private let containerView: UIView
private let largeCircleLayer: SimpleLayer
private let largeCircleShadowLayer: SimpleLayer
private let smallCircleLayer: SimpleLayer
private let smallCircleShadowLayer: SimpleLayer
private var items: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var isUpdating: Bool = false
private var component: QuickShareScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
private var disposable: Disposable?
private var peers: [EnginePeer]?
private var selectedPeerId: EnginePeer.Id?
private var didCompleteAnimationIn: Bool = false
private var initialContinueGesturePoint: CGPoint?
private var didMoveFromInitialGesturePoint = false
private let hapticFeedback = HapticFeedback()
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundView.clipsToBounds = true
self.backgroundTintView = UIView()
self.backgroundTintView.clipsToBounds = true
self.backgroundShadowLayer = SimpleLayer()
self.backgroundShadowLayer.opacity = 0.0
self.largeCircleLayer = SimpleLayer()
self.largeCircleShadowLayer = SimpleLayer()
self.smallCircleLayer = SimpleLayer()
self.smallCircleShadowLayer = SimpleLayer()
self.largeCircleLayer.backgroundColor = UIColor.black.cgColor
self.largeCircleLayer.masksToBounds = true
self.largeCircleLayer.cornerRadius = largeCircleSize / 2.0
self.smallCircleLayer.backgroundColor = UIColor.black.cgColor
self.smallCircleLayer.masksToBounds = true
self.smallCircleLayer.cornerRadius = smallCircleSize / 2.0
self.containerView = UIView()
self.containerView.clipsToBounds = true
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.backgroundTintView)
self.layer.addSublayer(self.backgroundShadowLayer)
self.addSubview(self.containerView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.disposable?.dispose()
}
func animateIn() {
self.hapticFeedback.impact()
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))
transition.animateBoundsSize(view: self.backgroundView, from: CGSize(width: 0.0, height: self.backgroundView.bounds.height), to: self.backgroundView.bounds.size)
transition.animateBounds(view: self.containerView, from: CGRect(x: self.containerView.bounds.width / 2.0, y: 0.0, width: 0.0, height: self.backgroundView.bounds.height), to: self.containerView.bounds)
self.backgroundView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1)
self.backgroundTintView.layer.animate(from: 0.0 as NSNumber, to: self.backgroundTintView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1)
self.backgroundShadowLayer.opacity = 1.0
transition.animateBoundsSize(layer: self.backgroundShadowLayer, from: CGSize(width: 0.0, height: self.backgroundShadowLayer.bounds.height), to: self.backgroundShadowLayer.bounds.size)
self.backgroundShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let mainCircleDelay: Double = 0.01
let backgroundCenter = self.backgroundView.frame.width / 2.0
let backgroundWidth = self.backgroundView.frame.width
for item in self.items.values {
guard let itemView = item.view else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundCenter)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.3
itemView.isHidden = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay * UIView.animationDurationFactor(), execute: { [weak itemView] in
guard let itemView else {
return
}
itemView.isHidden = false
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 0.63 as NSNumber, keyPath: "transform.scale", duration: 0.4)
})
}
Queue.mainQueue().after(0.3) {
self.containerView.clipsToBounds = false
self.didCompleteAnimationIn = true
}
}
func animateOut(completion: @escaping () -> Void) {
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .linear))
transition.setAlpha(view: self, alpha: 0.0, completion: { _ in
completion()
})
}
func highlightGestureMoved(location: CGPoint) {
var selectedPeerId: EnginePeer.Id?
for (peerId, view) in self.items {
guard let view = view.view else {
continue
}
if view.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) {
selectedPeerId = peerId
break
}
}
if let selectedPeerId, selectedPeerId != self.selectedPeerId {
self.hapticFeedback.tap()
}
self.selectedPeerId = selectedPeerId
self.state?.updated(transition: .spring(duration: 0.3))
}
func highlightGestureFinished(performAction: Bool) {
if let selectedPeerId = self.selectedPeerId, performAction {
if let component = self.component, let peer = self.peers?.first(where: { $0.id == selectedPeerId }), let view = self.items[selectedPeerId]?.view as? ItemComponent.View {
component.completion(peer, view.convert(view.bounds, to: nil))
view.avatarNode.isHidden = true
}
self.animateOut {
if let controller = self.environment?.controller() {
controller.dismiss()
}
}
} else {
self.animateOut {
if let controller = self.environment?.controller() {
controller.dismiss()
}
}
}
}
func update(component: QuickShareScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.state = state
if self.component == nil {
let peers = component.context.engine.peers.recentPeers()
|> take(1)
|> mapToSignal { recentPeers -> Signal<[EnginePeer], NoError> in
if case let .peers(peers) = recentPeers, !peers.isEmpty {
return .single(peers.map(EnginePeer.init))
} else {
return component.context.account.stateManager.postbox.tailChatListView(
groupId: .root,
count: 20,
summaryComponents: ChatListEntrySummaryComponents()
)
|> take(1)
|> map { view -> [EnginePeer] in
var peers: [EnginePeer] = []
for entry in view.0.entries.reversed() {
if case let .MessageEntry(entryData) = entry {
if let user = entryData.renderedPeer.chatMainPeer as? TelegramUser, user.isGenericUser && user.id != component.context.account.peerId && !user.id.isSecretChat {
peers.append(EnginePeer(user))
}
}
}
return peers
}
}
}
self.disposable = combineLatest(queue: Queue.mainQueue(),
peers,
component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId))
).start(next: { [weak self] peers, accountPeer in
guard let self else {
return
}
if !peers.isEmpty, let accountPeer {
self.peers = Array([accountPeer] + peers.prefix(4))
self.state?.updated()
component.ready.set(.single(true))
} else {
self.environment?.controller()?.dismiss()
}
})
component.gesture.externalUpdated = { [weak self] view, point in
guard let self else {
return
}
let localPoint = self.convert(point, from: view)
let initialPoint: CGPoint
if let current = self.initialContinueGesturePoint {
initialPoint = current
} else {
initialPoint = localPoint
self.initialContinueGesturePoint = localPoint
}
if self.didCompleteAnimationIn {
if !self.didMoveFromInitialGesturePoint {
let distance = abs(localPoint.y - initialPoint.y)
if distance > 4.0 {
self.didMoveFromInitialGesturePoint = true
}
}
if self.didMoveFromInitialGesturePoint {
let presentationPoint = self.convert(localPoint, to: self.containerView)
self.highlightGestureMoved(location: presentationPoint)
}
}
}
component.gesture.externalEnded = { [weak self] viewAndPoint in
guard let self, let gesture = self.component?.gesture else {
return
}
gesture.externalUpdated = nil
if self.didMoveFromInitialGesturePoint {
self.highlightGestureFinished(performAction: viewAndPoint != nil)
} else {
self.highlightGestureFinished(performAction: false)
}
}
}
self.component = component
let theme = environment.theme
if theme.overallDarkAppearance {
self.backgroundView.updateColor(color: theme.contextMenu.backgroundColor, forceKeepBlur: true, transition: .immediate)
self.backgroundTintView.backgroundColor = .clear
} else {
self.backgroundView.updateColor(color: .clear, forceKeepBlur: true, transition: .immediate)
self.backgroundTintView.backgroundColor = theme.contextMenu.backgroundColor
}
let sourceRect = component.sourceNode.view.convert(component.sourceNode.view.bounds, to: nil)
let sideInset: CGFloat = 16.0
let padding: CGFloat = 5.0
let spacing: CGFloat = 7.0
let itemSize = CGSize(width: 38.0, height: 38.0)
let selectedItemSize = CGSize(width: 60.0, height: 60.0)
let itemsCount = self.peers?.count ?? 5
let widthExtension: CGFloat = self.selectedPeerId != nil ? selectedItemSize.width - itemSize.width : 0.0
let size = CGSize(width: itemSize.width * CGFloat(itemsCount) + spacing * CGFloat(itemsCount - 1) + padding * 2.0 + widthExtension, height: itemSize.height + padding * 2.0)
let contentRect = CGRect(
origin: CGPoint(
x: max(sideInset, min(availableSize.width - sideInset - size.width, sourceRect.maxX + itemSize.width + spacing - size.width)),
y: sourceRect.minY - size.height - padding * 2.0
),
size: size
)
var itemFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: itemSize)
if let peers = self.peers {
for peer in peers {
var componentTransition = transition
let componentView: ComponentView<Empty>
if let current = self.items[peer.id] {
componentView = current
} else {
componentTransition = .immediate
componentView = ComponentView<Empty>()
self.items[peer.id] = componentView
}
var isFocused: Bool?
if let selectedPeerId {
isFocused = peer.id == selectedPeerId
}
let effectiveItemSize = isFocused == true ? selectedItemSize : itemSize
let effectiveItemFrame = CGRect(origin: itemFrame.origin.offsetBy(dx: 0.0, dy: itemSize.height - effectiveItemSize.height), size: effectiveItemSize)
let _ = componentView.update(
transition: componentTransition,
component: AnyComponent(
ItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
peer: peer,
safeInsets: UIEdgeInsets(top: 0.0, left: contentRect.minX + effectiveItemFrame.minX, bottom: 0.0, right: availableSize.width - contentRect.maxX + contentRect.width - effectiveItemFrame.maxX),
isFocused: isFocused
)
),
environment: {},
containerSize: itemSize
)
if let view = componentView.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
componentTransition.setScale(view: view, scale: effectiveItemSize.width / selectedItemSize.width)
componentTransition.setBounds(view: view, bounds: CGRect(origin: .zero, size: selectedItemSize))
componentTransition.setPosition(view: view, position: effectiveItemFrame.center)
}
itemFrame.origin.x += effectiveItemFrame.width + spacing
}
}
self.containerView.layer.cornerRadius = size.height / 2.0
self.backgroundView.layer.cornerRadius = size.height / 2.0
self.backgroundTintView.layer.cornerRadius = size.height / 2.0
transition.setFrame(view: self.backgroundView, frame: contentRect)
transition.setFrame(view: self.containerView, frame: contentRect)
self.backgroundView.update(size: contentRect.size, cornerRadius: 0.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundTintView, frame: CGRect(origin: .zero, size: contentRect.size))
let shadowInset: CGFloat = 15.0
let shadowColor = UIColor(white: 0.0, alpha: 0.4)
if self.backgroundShadowLayer.contents == nil, let image = generateBubbleShadowImage(shadow: shadowColor, diameter: 46.0, shadowBlur: shadowInset) {
ASDisplayNodeSetResizableContents(self.backgroundShadowLayer, image)
}
transition.setFrame(layer: self.backgroundShadowLayer, frame: contentRect.insetBy(dx: -shadowInset, dy: -shadowInset))
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class QuickShareScreen: ViewControllerComponentContainer {
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
private let readyValue = Promise<Bool>()
override public var ready: Promise<Bool> {
return self.readyValue
}
public init(
context: AccountContext,
sourceNode: ASDisplayNode,
gesture: ContextGesture,
completion: @escaping (EnginePeer, CGRect) -> Void
) {
let componentReady = Promise<Bool>()
super.init(
context: context,
component: QuickShareScreenComponent(
context: context,
sourceNode: sourceNode,
gesture: gesture,
completion: completion,
ready: componentReady
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
presentationMode: .default,
updatedPresentationData: nil
)
self.navigationPresentation = .flatModal
self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.processedDidAppear {
self.processedDidAppear = true
if let componentView = self.node.hostView.componentView as? QuickShareScreenComponent.View {
componentView.animateIn()
}
}
}
private func superDismiss() {
super.dismiss()
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.processedDidDisappear {
self.processedDidDisappear = true
if let componentView = self.node.hostView.componentView as? QuickShareScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
if let self {
self.superDismiss()
}
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
}
}
private final class ItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let peer: EnginePeer
let safeInsets: UIEdgeInsets
let isFocused: Bool?
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
peer: EnginePeer,
safeInsets: UIEdgeInsets,
isFocused: Bool?
) {
self.context = context
self.theme = theme
self.strings = strings
self.peer = peer
self.safeInsets = safeInsets
self.isFocused = isFocused
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.isFocused != rhs.isFocused {
return false
}
return true
}
final class View: UIView {
fileprivate let avatarNode: AvatarNode
private let backgroundNode: NavigationBackgroundNode
private let text = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: QuickShareScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.backgroundNode = NavigationBackgroundNode(color: .clear)
super.init(frame: frame)
self.addSubview(self.avatarNode.view)
self.addSubview(self.backgroundNode.view)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let size = CGSize(width: 60.0, height: 60.0)
var title = component.peer.compactDisplayTitle
var overrideImage: AvatarNodeImageOverride?
if component.peer.id == component.context.account.peerId {
overrideImage = .savedMessagesIcon
title = component.strings.DialogList_SavedMessages
}
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: component.peer,
overrideImage: overrideImage,
synchronousLoad: true
)
self.avatarNode.view.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.avatarNode.view.bounds = CGRect(origin: .zero, size: size)
self.avatarNode.updateSize(size: size)
var textAlpha: CGFloat = 0.0
var textOffset: CGFloat = 6.0
if let isFocused = component.isFocused {
textAlpha = isFocused ? 1.0 : 0.0
textOffset = isFocused ? 0.0 : 6.0
}
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: Font.semibold(13.0), textColor: .white)))
),
environment: {},
containerSize: CGSize(width: 160.0, height: 33.0)
)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
let initialX = floor((size.width - textSize.width) / 2.0)
var textFrame = CGRect(origin: CGPoint(x: initialX, y: -13.0 - textSize.height + textOffset), size: textSize)
let sideInset: CGFloat = 8.0
let textPadding: CGFloat = 8.0
let leftDistanceToEdge = 0.0 - textFrame.minX
let rightDistanceToEdge = textFrame.maxX - size.width
let leftSafeInset = component.safeInsets.left - textPadding - sideInset
let rightSafeInset = component.safeInsets.right - textPadding - sideInset
if leftSafeInset < leftDistanceToEdge {
textFrame.origin.x = -leftSafeInset
}
if rightSafeInset < rightDistanceToEdge {
textFrame.origin.x = size.width + rightSafeInset - textFrame.width
}
transition.setFrame(view: textView, frame: textFrame)
let backgroundFrame = textFrame.insetBy(dx: -textPadding, dy: -3.0 - UIScreenPixel)
transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame)
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.size.height / 2.0, transition: .immediate)
self.backgroundNode.updateColor(color: component.theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillStatic, enableBlur: true, transition: .immediate)
transition.setAlpha(view: textView, alpha: textAlpha)
transition.setAlpha(view: self.backgroundNode.view, alpha: textAlpha)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(shadow.cgColor)
context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
})?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0))
}
@@ -0,0 +1,418 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ViewControllerComponent
import AccountContext
import MultilineTextComponent
import AvatarNode
import Markdown
import LottieComponent
import PlainButtonComponent
private final class QuickShareToastScreenComponent: Component {
let context: AccountContext
let peer: EnginePeer
let sourceFrame: CGRect
let action: (QuickShareToastScreen.Action) -> Void
init(
context: AccountContext,
peer: EnginePeer,
sourceFrame: CGRect,
action: @escaping (QuickShareToastScreen.Action) -> Void
) {
self.context = context
self.peer = peer
self.sourceFrame = sourceFrame
self.action = action
}
static func ==(lhs: QuickShareToastScreenComponent, rhs: QuickShareToastScreenComponent) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.sourceFrame != rhs.sourceFrame {
return false
}
return true
}
final class View: UIView {
private let contentView: UIView
private let backgroundView: BlurredBackgroundView
private let avatarNode: AvatarNode
private let animation = ComponentView<Empty>()
private let content = ComponentView<Empty>()
private let actionButton = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: QuickShareToastScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
private var doneTimer: Foundation.Timer?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.contentView = UIView()
self.contentView.isUserInteractionEnabled = false
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0))
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.contentView)
self.contentView.addSubview(self.avatarNode.view)
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture)))
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.doneTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.backgroundView.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
@objc private func tapGesture() {
guard let component = self.component else {
return
}
component.action(.info)
self.doneTimer?.invalidate()
self.environment?.controller()?.dismiss()
}
func animateIn() {
guard let component = self.component, let environment = self.environment else {
return
}
func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x
let y1 = sourcePoint.y
let x2 = midPoint.x
let y2 = midPoint.y
let x3 = targetPosition.x
let y3 = targetPosition.y
var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(CGPoint(x: x, y: y))
}
} else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c
keyframes.append(CGPoint(x: x, y: y))
}
}
return keyframes
}
let playIconAnimation: (Double) -> Void = { duration in
self.avatarNode.contentNode.alpha = 0.0
self.avatarNode.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
self.avatarNode.contentNode.layer.animateScale(from: 1.0, to: 0.01, duration: duration, removeOnCompletion: false)
if let view = self.animation.view as? LottieComponent.View {
view.alpha = 1.0
view.playOnce()
}
}
if component.peer.id == component.context.account.peerId {
playIconAnimation(0.2)
}
let offset = self.bounds.height - environment.inputHeight - self.backgroundView.frame.minY
self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
if component.peer.id != component.context.account.peerId {
playIconAnimation(0.1)
}
HapticFeedback().success()
})
if let component = self.component {
let fromPoint = self.avatarNode.view.convert(component.sourceFrame.center, from: nil).offsetBy(dx: 0.0, dy: -offset)
let positionValues = generateAvatarParabollicMotionKeyframes(from: fromPoint, to: .zero, elevation: 20.0)
self.avatarNode.layer.animateKeyframes(values: positionValues.map { NSValue(cgPoint: $0) }, duration: 0.35, keyPath: "position", additive: true)
self.avatarNode.layer.animateScale(from: component.sourceFrame.width / self.avatarNode.bounds.width, to: 1.0, duration: 0.35)
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.5))
}
}
func animateOut(completion: @escaping () -> Void) {
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { _ in
completion()
})
self.backgroundView.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
func update(component: QuickShareToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
if self.component == nil {
self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in
guard let self, let controller = self.environment?.controller() as? QuickShareToastScreen else {
return
}
controller.dismissWithCommitAction()
})
}
self.component = component
self.environment = environment
self.state = state
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
let tabBarHeight = 49.0 + max(environment.safeInsets.bottom, environment.inputHeight)
let containerInsets = UIEdgeInsets(
top: environment.safeInsets.top,
left: environment.safeInsets.left + 12.0,
bottom: tabBarHeight + 3.0,
right: environment.safeInsets.right + 12.0
)
let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom)
let spacing: CGFloat = 8.0
let iconSize = CGSize(width: 30.0, height: 30.0)
let tooltipText: String
var overrideImage: AvatarNodeImageOverride?
var animationName: String = "anim_forward"
if component.peer.id == component.context.account.peerId {
tooltipText = environment.strings.Conversation_ForwardTooltip_SavedMessages_One
overrideImage = .savedMessagesIcon
animationName = "anim_savedmessages"
} else {
tooltipText = environment.strings.Conversation_ForwardTooltip_Chat_One(component.peer.compactDisplayTitle).string
}
let actionButtonSize = self.actionButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.id != component.context.account.peerId ? environment.strings.Undo_Undo : "", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)))
)),
effectAlignment: .center,
contentInsets: UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0),
action: { [weak self] in
guard let self, let _ = self.component else {
return
}
self.doneTimer?.invalidate()
self.environment?.controller()?.dismiss()
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width, height: availableContentSize.height)
)
let contentSize = self.content.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .markdown(
text: tooltipText,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)),
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white),
linkAttribute: { _ in return nil })
))),
environment: {},
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - actionButtonSize.width - 16.0 - 4.0, height: availableContentSize.height)
)
var contentHeight: CGFloat = 0.0
contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
self.avatarNode.setPeer(context: component.context, theme: environment.theme, peer: component.peer, overrideImage: overrideImage, synchronousLoad: true)
self.avatarNode.updateSize(size: avatarFrame.size)
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
let _ = self.animation.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: animationName
),
size: CGSize(width: 38.0, height: 38.0),
loop: false
)),
environment: {},
containerSize: iconSize
)
if let animationView = self.animation.view {
if animationView.superview == nil {
animationView.alpha = 0.0
self.avatarNode.view.addSubview(animationView)
}
animationView.frame = CGRect(origin: .zero, size: iconSize).insetBy(dx: -2.0, dy: -2.0)
}
if let contentView = self.content.view {
if contentView.superview == nil {
self.contentView.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize))
}
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.backgroundView.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: availableContentSize.width - contentInsets.right - 16.0 - actionButtonSize.width, y: floor((contentHeight - actionButtonSize.height) * 0.5)), size: actionButtonSize))
}
let size = CGSize(width: availableContentSize.width, height: contentHeight)
let backgroundFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size)
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: backgroundFrame.size))
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class QuickShareToastScreen: ViewControllerComponentContainer {
public enum Action {
case info
case commit
}
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
private let action: (Action) -> Void
public init(
context: AccountContext,
peer: EnginePeer,
sourceFrame: CGRect,
action: @escaping (Action) -> Void
) {
self.action = action
super.init(
context: context,
component: QuickShareToastScreenComponent(
context: context,
peer: peer,
sourceFrame: sourceFrame,
action: action
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
presentationMode: .default,
updatedPresentationData: nil
)
self.navigationPresentation = .flatModal
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.processedDidAppear {
self.processedDidAppear = true
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
componentView.animateIn()
}
}
}
private func superDismiss() {
super.dismiss()
}
private var didCommit = false
public func dismissWithCommitAction() {
if !self.didCommit {
self.didCommit = true
self.action(.commit)
}
self.dismiss()
}
public override func dismiss(completion: (() -> Void)? = nil) {
if !self.processedDidDisappear {
self.processedDidDisappear = true
if let componentView = self.node.hostView.componentView as? QuickShareToastScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
if let self {
self.superDismiss()
}
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
}
}