Initial implementation of attachment menu

This commit is contained in:
Ilya Laktyushin 2022-02-11 19:38:28 +03:00
parent 8bd663e908
commit 7da0357b6d
45 changed files with 3105 additions and 191 deletions

View file

@ -7256,3 +7256,13 @@ Sorry for the inconvenience.";
"SharedMedia.CommonGroupCount_1" = "%@ group in common";
"SharedMedia.CommonGroupCount_any" = "%@ groups in common";
"Attachment.Camera" = "Camera";
"Attachment.Gallery" = "Gallery";
"Attachment.File" = "File";
"Attachment.Location" = "Location";
"Attachment.Contact" = "Contact";
"Attachment.Poll" = "Poll";
"Attachment.SelectFromGallery" = "Select from Gallery";
"Attachment.SelectFromFiles" = "Select from Files";

View file

@ -0,0 +1,31 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AttachmentUI",
module_name = "AttachmentUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/AppBundle:AppBundle",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,464 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import UIKitRuntimeUtils
import Display
import DirectionalPanGesture
final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let scrollNode: ASDisplayNode
let container: NavigationContainer
private(set) var isReady: Bool = false
private(set) var dismissProgress: CGFloat = 0.0
var isReadyUpdated: (() -> Void)?
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var interactivelyDismissed: (() -> Void)?
var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var isUpdatingState = false
private var isDismissed = false
private var isInteractiveDimissEnabled = true
private var validLayout: (layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat)?
var keyboardViewManager: KeyboardViewManager? {
didSet {
if self.keyboardViewManager !== oldValue {
self.container.keyboardViewManager = self.keyboardViewManager
}
}
}
var canHaveKeyboardFocus: Bool = false {
didSet {
self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
}
}
private var panGestureRecognizer: UIPanGestureRecognizer?
init(controllerRemoved: @escaping (ViewController) -> Void) {
self.scrollNode = ASDisplayNode()
self.container = NavigationContainer(controllerRemoved: controllerRemoved)
self.container.clipsToBounds = true
super.init()
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.container)
self.isReady = self.container.isReady
self.container.isReadyUpdated = { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
if !strongSelf.isUpdatingState {
strongSelf.isReadyUpdated?()
}
}
}
applySmoothRoundedCorners(self.container.layer)
}
override func didLoad() {
super.didLoad()
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.panGestureRecognizer = panRecognizer
self.scrollNode.view.addGestureRecognizer(panRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
return true
}
return false
}
private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)?
let defaultTopInset: CGFloat = 210.0
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard let (layout, controllers, coveredByModalTransition) = self.validLayout else {
return
}
let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
switch recognizer.state {
case .began:
let topInset: CGFloat
if self.isExpanded {
topInset = 0.0
} else {
topInset = edgeTopInset
}
let point = recognizer.location(in: self.view)
let currentHitView = self.hitTest(point, with: nil)
let scrollViewAndListNode = self.findScrollView(view: currentHitView)
let scrollView = scrollViewAndListNode?.0
let listNode = scrollViewAndListNode?.1
self.panGestureArguments = (topInset, 0.0, scrollView, listNode)
case .changed:
guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
return
}
let visibleContentOffset = listNode?.visibleContentOffset()
let contentOffset = scrollView?.contentOffset.y ?? 0.0
var translation = recognizer.translation(in: self.view).y
if case let .known(value) = visibleContentOffset, value <= 0.5 {
} else if contentOffset <= 0.5 {
} else {
translation = panOffset
if self.isExpanded {
recognizer.setTranslation(CGPoint(), in: self.view)
}
}
self.panGestureArguments = (topInset, translation, scrollView, listNode)
let currentOffset = topInset + translation
if !self.isExpanded {
if currentOffset > 0.0, let scrollView = scrollView {
scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
}
var bounds = self.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
self.bounds = bounds
}
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .immediate)
case .ended:
guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
return
}
let visibleContentOffset = listNode?.visibleContentOffset()
let contentOffset = scrollView?.contentOffset.y ?? 0.0
let translation = recognizer.translation(in: self.view).y
var velocity = recognizer.velocity(in: self.view)
if case let .known(value) = visibleContentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = visibleContentOffset {
velocity = CGPoint()
} else if contentOffset > 0.0 {
velocity = CGPoint()
}
var bounds = self.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
let offset = currentTopInset + panOffset
let topInset: CGFloat = edgeTopInset
if self.isExpanded {
self.panGestureArguments = nil
if velocity.y > 300.0 || offset > topInset / 2.0 {
self.isExpanded = false
if let listNode = listNode {
listNode.scroller.setContentOffset(CGPoint(), animated: false)
} else if let scrollView = scrollView {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
}
let distance = topInset - offset
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance)
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition)
} else {
self.isExpanded = true
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut))
}
} else {
self.panGestureArguments = nil
var dismissing = false
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
self.interactivelyDismissed?()
dismissing = true
} else if (velocity.y < -300.0 || offset < topInset / 2.0) {
if velocity.y > -2200.0, let listNode = listNode {
DispatchQueue.main.async {
listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset)
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
self.isExpanded = true
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition)
} else {
if let listNode = listNode {
listNode.scroller.setContentOffset(CGPoint(), animated: false)
} else if let scrollView = scrollView {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false)
}
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut))
}
if !dismissing {
var bounds = self.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.bounds = bounds
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
}
case .cancelled:
self.panGestureArguments = nil
self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut))
default:
break
}
}
private func checkInteractiveDismissWithControllers() -> Bool {
if let controller = self.container.controllers.last {
if !controller.attemptNavigation({
}) {
return false
}
}
return true
}
var isExpanded = false
func update(layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isDismissed {
return
}
self.isUpdatingState = true
self.validLayout = (layout, controllers, coveredByModalTransition)
self.panGestureRecognizer?.isEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0)
// self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled
let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
let topInset: CGFloat
if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
if self.isExpanded {
topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
} else {
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
}
} else {
topInset = self.isExpanded ? 0.0 : edgeTopInset
}
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size))
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset)
self.updateModalProgress?(modalProgress, transition)
let containerLayout: ContainerViewLayout
let containerFrame: CGRect
let containerScale: CGFloat
if layout.metrics.widthClass == .compact {
self.container.clipsToBounds = true
if isLandscape {
self.container.cornerRadius = 0.0
} else {
self.container.cornerRadius = 10.0
}
if #available(iOS 11.0, *) {
if layout.safeInsets.bottom.isZero {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
} else {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
}
var containerTopInset: CGFloat
if isLandscape {
containerTopInset = 0.0
containerLayout = layout
let unscaledFrame = CGRect(origin: CGPoint(), size: containerLayout.size)
containerScale = 1.0
containerFrame = unscaledFrame
} else {
containerTopInset = 10.0
if let statusBarHeight = layout.statusBarHeight {
containerTopInset += statusBarHeight
}
let effectiveStatusBarHeight: CGFloat? = nil
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: containerLayout.size)
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
let maxScaledTopInset: CGFloat = containerTopInset - 10.0
let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
}
} else {
self.container.clipsToBounds = true
self.container.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
let verticalInset: CGFloat = 44.0
let maxSide = max(layout.size.width, layout.size.height)
let minSide = min(layout.size.width, layout.size.height)
let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
containerScale = 1.0
var inputHeight: CGFloat?
if let inputHeightValue = layout.inputHeight {
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
}
let effectiveStatusBarHeight: CGFloat? = nil
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
}
transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame)
transition.updateTransformScale(node: self.container, scale: containerScale)
self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
self.isUpdatingState = false
}
func dismiss(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) -> ContainedViewLayoutTransition {
for controller in self.container.controllers {
controller.viewWillDisappear(transition.isAnimated)
}
if let firstController = self.container.controllers.first, case .standaloneModal = firstController.navigationPresentation {
for controller in self.container.controllers {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.displayNode.removeFromSupernode()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(transition.isAnimated)
}
completion()
return transition
} else {
if transition.isAnimated {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
for controller in strongSelf.container.controllers {
controller.viewDidDisappear(transition.isAnimated)
}
completion()
})
return positionTransition
} else {
for controller in self.container.controllers {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.displayNode.removeFromSupernode()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(transition.isAnimated)
}
completion()
return transition
}
}
}
private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? {
if let view = view {
if let view = view as? UIScrollView {
return (view, nil)
}
if let node = view.asyncdisplaykit_node as? ListView {
return (node.scroller, node)
}
return findScrollView(view: view.superview)
} else {
return nil
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if !self.scrollNode.frame.contains(point) {
return false
}
return super.point(inside: point, with: event)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
// var currentParent: UIView? = result
// var enableScrolling = true
// while true {
// if currentParent == nil {
// break
// }
// if currentParent is UIKeyInput {
// if currentParent?.disablesInteractiveModalDismiss == true {
// enableScrolling = false
// break
// }
// } else if let scrollView = currentParent as? UIScrollView {
// if scrollView === self.scrollNode.view {
// break
// }
// if scrollView.disablesInteractiveModalDismiss {
// enableScrolling = false
// break
// } else {
// if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
// return self.scrollNode.view
// }
// }
// } else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
// if listNode.view.disablesInteractiveModalDismiss {
// enableScrolling = false
// break
// } else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
// return self.scrollNode.view
// }
// }
// currentParent = currentParent?.superview
// }
// if let controller = self.container.controllers.last {
// if controller.view.disablesInteractiveModalDismiss {
// enableScrolling = false
// }
// }
// self.isInteractiveDimissEnabled = enableScrolling
// if let layout = self.validLayout {
// if layout.inputHeight != nil && layout.inputHeight != 0.0 {
// enableScrolling = false
// }
// }
// self.scrollNode.view.isScrollEnabled = enableScrolling
return result
}
}

View file

@ -0,0 +1,251 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import TelegramStringFormatting
import UIKitRuntimeUtils
public enum AttachmentButtonType: Equatable {
case camera
case gallery
case file
case location
case contact
case poll
case app(String)
}
public class AttachmentController: ViewController {
private let context: AccountContext
private final class Node: ASDisplayNode {
private weak var controller: AttachmentController?
private let dim: ASDisplayNode
private let container: AttachmentContainer
private let panel: AttachmentPanel
private var validLayout: ContainerViewLayout?
private var modalProgress: CGFloat = 0.0
private var currentType: AttachmentButtonType?
private var currentController: ViewController?
init(controller: AttachmentController) {
self.controller = controller
self.dim = ASDisplayNode()
self.dim.alpha = 0.0
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
self.container = AttachmentContainer(controllerRemoved: { _ in
})
self.panel = AttachmentPanel(context: controller.context)
super.init()
self.addSubnode(self.dim)
self.container.updateModalProgress = { [weak self] progress, transition in
if let strongSelf = self, let layout = strongSelf.validLayout {
strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition)
strongSelf.modalProgress = progress
strongSelf.containerLayoutUpdated(layout, transition: transition)
}
}
self.container.isReadyUpdated = { [weak self] in
if let strongSelf = self, let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring))
}
}
self.panel.selectionChanged = { [weak self] type, ascending in
if let strongSelf = self {
strongSelf.switchToController(type, ascending)
}
}
self.container.interactivelyDismissed = { [weak self] in
if let strongSelf = self {
strongSelf.controller?.dismiss(animated: true)
}
}
}
override func didLoad() {
super.didLoad()
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.switchToController(.gallery, false)
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.controller?.dismiss(animated: true)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let controller = self.controller, controller.isInteractionDisabled() {
return self.view
} else {
return super.hitTest(point, with: event)
}
}
func dismiss(animated: Bool, completion: @escaping () -> Void = {}) {
if animated {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
})
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
} else {
self.controller?.dismiss(animated: false, completion: nil)
}
}
func switchToController(_ type: AttachmentButtonType, _ ascending: Bool) {
guard self.currentType != type else {
return
}
let previousType = self.currentType
self.currentType = type
self.controller?.requestController(type, { [weak self] controller in
if let strongSelf = self, let controller = controller {
controller._presentedInModal = true
controller.navigation_setPresenting(strongSelf.controller)
let animateTransition = previousType != nil
strongSelf.currentController = controller
if animateTransition, let snapshotView = strongSelf.container.scrollNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = strongSelf.container.scrollNode.frame
strongSelf.container.view.insertSubview(snapshotView, belowSubview: strongSelf.panel.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
func animateIn(transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.dim, alpha: 1.0)
transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
}
private var isCollapsed: Bool = false
private var isUpdatingContainer = false
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
let containerTransition: ContainedViewLayoutTransition
if self.container.supernode == nil {
containerTransition = .immediate
} else {
containerTransition = transition
}
if !self.isUpdatingContainer {
self.isUpdatingContainer = true
let controllers = self.currentController.flatMap { [$0] } ?? []
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: layout.size))
self.container.update(layout: layout, controllers: controllers, coveredByModalTransition: 0.0, transition: .immediate)
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
self.addSubnode(self.container)
self.container.addSubnode(self.panel)
self.animateIn(transition: transition)
}
self.isUpdatingContainer = false
}
let buttons: [AttachmentButtonType] = [.camera, .gallery, .file, .location, .contact, .poll, .app("App")]
let sideInset: CGFloat = 16.0
let bottomInset: CGFloat
if layout.intrinsicInsets.bottom > 0.0 {
bottomInset = layout.intrinsicInsets.bottom - 4.0
} else {
bottomInset = 4.0
}
if self.modalProgress < 0.75 {
self.isCollapsed = false
} else if self.modalProgress == 1.0 {
self.isCollapsed = true
}
let panelSize = CGSize(width: layout.size.width - sideInset * 2.0, height: panelButtonSize.height)
transition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelSize.height - bottomInset), size: panelSize))
self.panel.update(buttons: buttons, isCollapsed: self.isCollapsed, size: panelSize, transition: transition)
}
}
public var requestController: (AttachmentButtonType, @escaping (ViewController?) -> Void) -> Void = { _, completion in
completion(nil)
}
public init(context: AccountContext) {
self.context = context
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var node: Node {
return self.displayNode as! Node
}
open override func loadDisplayNode() {
self.displayNode = Node(controller: self)
self.displayNodeDidLoad()
}
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.view.endEditing(true)
if flag {
self.node.dismiss(animated: true, completion: {
super.dismiss(animated: flag, completion: {})
completion?()
})
} else {
super.dismiss(animated: false, completion: {})
completion?()
}
}
private func isInteractionDisabled() -> Bool {
return false
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, transition: transition)
}
}

View file

@ -0,0 +1,622 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AccountContext
let panelButtonSize = CGSize(width: 80.0, height: 72.0)
let smallPanelButtonSize = CGSize(width: 54.0, height: 46.0)
private let iconSize = CGSize(width: 54.0, height: 42.0)
private let sideInset: CGFloat = 3.0
private let smallSideInset: CGFloat = 0.0
private enum AttachmentButtonTransition {
case transitionIn
case selection
}
private func generateShadowImage() -> UIImage? {
return generateImage(CGSize(width: 90.0, height: 90.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
// let rect = bounds.insetBy(dx: 12.0, dy: 24.0)
// let path = UIBezierPath(roundedRect: CGRect(origin: rect.origin, size: CGSize(width: rect.width, height: rect.height + 33.0)), cornerRadius: 24.0).cgPath
// context.addRect(bounds)
// context.addPath(path)
// context.clip(using: .evenOdd)
//
// context.addPath(path)
// context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 40.0, color: UIColor(rgb: 0x000000, alpha: 0.06).cgColor)
// context.setFillColor(UIColor.white.cgColor)
// context.setBlendMode(.multiply)
// context.fillPath()
//
// context.resetClip()
// context.setBlendMode(.normal)
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x000000, alpha: 0.0).cgColor, UIColor(rgb: 0x000000, alpha: 0.2).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: [])
})?.stretchableImage(withLeftCapWidth: 45, topCapHeight: 70)
}
private func generateBackgroundImage(colors: [UIColor]) -> UIImage? {
return generateImage(iconSize, rotatedContext: { size, context in
var locations: [CGFloat]
if colors.count == 3 {
locations = [1.0, 0.5, 0.0]
} else {
locations = [1.0, 0.0]
}
let colors: [CGColor] = colors.map { $0.cgColor }
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
if colors.count == 2 {
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: .drawsAfterEndLocation)
} else if colors.count == 3 {
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: size.width, y: 0.0), options: .drawsAfterEndLocation)
}
// let center = CGPoint(x: 10.0, y: 10.0)
// context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation)
})
}
private let buttonGlowImage: UIImage? = {
let inset: CGFloat = 6.0
return generateImage(CGSize(width: iconSize.width + inset * 2.0, height: iconSize.height + inset * 2.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let rect = bounds.insetBy(dx: inset, dy: inset)
let path = UIBezierPath(roundedRect: rect, cornerRadius: 21.0).cgPath
context.addRect(bounds)
context.addPath(path)
context.clip(using: .evenOdd)
context.addPath(path)
context.setShadow(offset: CGSize(), blur: 14.0, color: UIColor(rgb: 0xffffff, alpha: 0.8).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate)
}()
private let buttonSelectionMaskImage: UIImage? = {
let inset: CGFloat = 3.0
return generateImage(CGSize(width: iconSize.width + inset * 2.0, height: iconSize.height + inset * 2.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 23.0).cgPath
context.addPath(path)
context.setFillColor(UIColor(rgb: 0xffffff).cgColor)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate)
}()
private let ringImage: UIImage? = {
return generateFilledCircleImage(diameter: iconSize.width, color: nil, strokeColor: .white, strokeWidth: 2.0, backgroundColor: nil)
}()
private final class AttachButtonComponent: CombinedComponent {
let context: AccountContext
let type: AttachmentButtonType
let isSelected: Bool
let isCollapsed: Bool
let transitionFraction: CGFloat
let strings: PresentationStrings
let theme: PresentationTheme
let action: () -> Void
init(
context: AccountContext,
type: AttachmentButtonType,
isSelected: Bool,
isCollapsed: Bool,
transitionFraction: CGFloat,
strings: PresentationStrings,
theme: PresentationTheme,
action: @escaping () -> Void
) {
self.context = context
self.type = type
self.isSelected = isSelected
self.isCollapsed = isCollapsed
self.transitionFraction = transitionFraction
self.strings = strings
self.theme = theme
self.action = action
}
static func ==(lhs: AttachButtonComponent, rhs: AttachButtonComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.type != rhs.type {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.isCollapsed != rhs.isCollapsed {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
static var body: Body {
let icon = Child(AttachButtonIconComponent.self)
let title = Child(Text.self)
return { context in
let name: String
let animationName: String?
let imageName: String?
let backgroundColors: [UIColor]
let foregroundColor: UIColor = .white
let isCollapsed = context.component.isCollapsed
switch context.component.type {
case .camera:
name = context.component.strings.Attachment_Camera
animationName = "anim_camera"
imageName = "Chat/Attach Menu/Camera"
backgroundColors = [UIColor(rgb: 0xba4aae), UIColor(rgb: 0xdd4e6f), UIColor(rgb: 0xf3b76c)]
case .gallery:
name = context.component.strings.Attachment_Gallery
animationName = "anim_gallery"
imageName = "Chat/Attach Menu/Gallery"
backgroundColors = [UIColor(rgb: 0x2071f1), UIColor(rgb: 0x1bc9fa)]
case .file:
name = context.component.strings.Attachment_File
animationName = "anim_file"
imageName = "Chat/Attach Menu/File"
backgroundColors = [UIColor(rgb: 0xed705d), UIColor(rgb: 0xffa14c)]
case .location:
name = context.component.strings.Attachment_Location
animationName = "anim_location"
imageName = "Chat/Attach Menu/Location"
backgroundColors = [UIColor(rgb: 0x5fb84f), UIColor(rgb: 0x99de6f)]
case .contact:
name = context.component.strings.Attachment_Contact
animationName = "anim_contact"
imageName = "Chat/Attach Menu/Contact"
backgroundColors = [UIColor(rgb: 0xaa47d6), UIColor(rgb: 0xd67cf4)]
case .poll:
name = context.component.strings.Attachment_Poll
animationName = "anim_poll"
imageName = "Chat/Attach Menu/Poll"
backgroundColors = [UIColor(rgb: 0xe9484f), UIColor(rgb: 0xee707e)]
case let .app(appName):
name = appName
animationName = nil
imageName = nil
backgroundColors = [UIColor(rgb: 0x000000), UIColor(rgb: 0x000000)]
}
let icon = icon.update(
component: AttachButtonIconComponent(
animationName: animationName,
imageName: imageName,
isSelected: context.component.isSelected,
backgroundColors: backgroundColors,
foregroundColor: foregroundColor,
theme: context.component.theme,
context: context.component.context,
action: context.component.action
),
availableSize: iconSize,
transition: context.transition
)
let title = title.update(
component: Text(
text: name,
font: Font.regular(11.0),
color: context.component.theme.actionSheet.primaryTextColor
),
availableSize: context.availableSize,
transition: .immediate
)
let topInset: CGFloat = 8.0
let spacing: CGFloat = 3.0 + UIScreenPixel
let normalIconScale = isCollapsed ? 0.7 : 1.0
let smallIconScale = isCollapsed ? 0.5 : 0.6
let iconScale = normalIconScale - (normalIconScale - smallIconScale) * abs(context.component.transitionFraction)
let iconOffset: CGFloat = (isCollapsed ? 10.0 : 20.0) * context.component.transitionFraction
let iconFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - icon.size.width) / 2.0) + iconOffset, y: isCollapsed ? 3.0 : topInset), size: icon.size)
var titleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - title.size.width) / 2.0) + iconOffset, y: iconFrame.midY + (iconFrame.height * 0.5 * iconScale) + spacing), size: title.size)
if isCollapsed {
titleFrame.origin.y = floor(iconFrame.midY - title.size.height / 2.0)
}
context.add(title
.position(CGPoint(x: titleFrame.midX, y: titleFrame.midY))
.opacity(isCollapsed ? 0.0 : 1.0 - abs(context.component.transitionFraction))
)
context.add(icon
.position(CGPoint(x: iconFrame.midX, y: iconFrame.midY))
.scale(iconScale)
)
return context.availableSize
}
}
}
private final class AttachButtonIconComponent: Component {
let animationName: String?
let imageName: String?
let isSelected: Bool
let backgroundColors: [UIColor]
let foregroundColor: UIColor
let theme: PresentationTheme
let context: AccountContext
let action: () -> Void
init(
animationName: String?,
imageName: String?,
isSelected: Bool,
backgroundColors: [UIColor],
foregroundColor: UIColor,
theme: PresentationTheme,
context: AccountContext,
action: @escaping () -> Void
) {
self.animationName = animationName
self.imageName = imageName
self.isSelected = isSelected
self.backgroundColors = backgroundColors
self.foregroundColor = foregroundColor
self.theme = theme
self.context = context
self.action = action
}
static func ==(lhs: AttachButtonIconComponent, rhs: AttachButtonIconComponent) -> Bool {
if lhs.animationName != rhs.animationName {
return false
}
if lhs.imageName != rhs.imageName {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.backgroundColors != rhs.backgroundColors {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.context !== rhs.context {
return false
}
return true
}
final class View: HighlightTrackingButton {
private let glowView: UIImageView
private let selectionView: UIImageView
private let backgroundView: UIView
private let iconView: UIImageView
private var action: (() -> Void)?
private var currentColors: [UIColor] = []
private var currentImageName: String?
private var currentIsSelected: Bool?
init() {
self.glowView = UIImageView()
self.glowView.image = buttonGlowImage
self.glowView.isUserInteractionEnabled = false
self.selectionView = UIImageView()
self.selectionView.image = buttonSelectionMaskImage
self.selectionView.isUserInteractionEnabled = false
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.backgroundView.isUserInteractionEnabled = false
self.backgroundView.layer.cornerRadius = 21.0
self.iconView = UIImageView()
super.init(frame: CGRect())
self.addSubview(self.glowView)
self.addSubview(self.selectionView)
self.addSubview(self.backgroundView)
self.addSubview(self.iconView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { _ in
}
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.action?()
}
func update(component: AttachButtonIconComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.action = component.action
if self.currentColors != component.backgroundColors {
self.currentColors = component.backgroundColors
self.backgroundView.layer.contents = generateBackgroundImage(colors: component.backgroundColors)?.cgImage
if let color = component.backgroundColors.last {
self.glowView.tintColor = color
self.selectionView.tintColor = color.withAlphaComponent(0.2)
}
}
if self.currentImageName != component.imageName {
self.currentImageName = component.imageName
if let imageName = component.imageName, let image = UIImage(bundleImageName: imageName) {
self.iconView.image = image
let scale: CGFloat = 0.875
let iconSize = CGSize(width: floorToScreenPixels(image.size.width * scale), height: floorToScreenPixels(image.size.height * scale))
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - iconSize.height) / 2.0)), size: iconSize)
}
}
if self.currentIsSelected != component.isSelected {
self.currentIsSelected = component.isSelected
transition.setScale(view: self.selectionView, scale: component.isSelected ? 1.0 : 0.8)
}
let contentFrame = CGRect(origin: CGPoint(), size: availableSize)
self.backgroundView.frame = contentFrame
self.glowView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: contentFrame.width + 12.0, height: contentFrame.height + 12.0))
self.glowView.center = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
self.selectionView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: contentFrame.width + 6.0, height: contentFrame.height + 6.0))
self.selectionView.center = CGPoint(x: contentFrame.midX, y: contentFrame.midY)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private let shadowNode: ASImageNode
private let containerNode: ASDisplayNode
private var effectView: UIVisualEffectView?
private let scrollNode: ASScrollNode
private let backgroundNode: ASDisplayNode
private var buttonViews: [Int: ComponentHostView<Empty>] = [:]
private var buttons: [AttachmentButtonType] = []
private var selectedIndex: Int = 1
private var isCollapsed: Bool = false
private var validLayout: CGSize?
private var scrollLayout: (width: CGFloat, contentSize: CGSize)?
var selectionChanged: (AttachmentButtonType, Bool) -> Void = { _, _ in }
init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.shadowNode = ASImageNode()
self.shadowNode.contentMode = .scaleToFill
self.shadowNode.displaysAsynchronously = false
self.shadowNode.image = generateShadowImage()
self.containerNode = ASDisplayNode()
self.containerNode.clipsToBounds = true
self.scrollNode = ASScrollNode()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
super.init()
self.addSubnode(self.shadowNode)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.scrollNode)
}
override func didLoad() {
super.didLoad()
if #available(iOS 13.0, *) {
self.containerNode.layer.cornerCurve = .continuous
}
self.containerNode.layer.cornerRadius = 23.0
self.scrollNode.view.delegate = self
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
let effect: UIVisualEffect
switch self.presentationData.theme.actionSheet.backgroundType {
case .light:
effect = UIBlurEffect(style: .light)
case .dark:
effect = UIBlurEffect(style: .dark)
}
let effectView = UIVisualEffectView(effect: effect)
self.effectView = effectView
self.containerNode.view.insertSubview(effectView, at: 0)
}
func updateViews(transition: Transition) {
guard let _ = self.validLayout else {
return
}
let visibleRect = self.scrollNode.bounds.insetBy(dx: -180.0, dy: 0.0)
let actualVisibleRect = self.scrollNode.bounds
var validButtons = Set<Int>()
let buttonSize = self.isCollapsed ? smallPanelButtonSize : panelButtonSize
for i in 0 ..< self.buttons.count {
let buttonFrame = CGRect(origin: CGPoint(x: (self.isCollapsed ? smallSideInset : sideInset) + buttonSize.width * CGFloat(i), y: 0.0), size: buttonSize)
if !visibleRect.intersects(buttonFrame) {
continue
}
validButtons.insert(i)
let edge = buttonSize.width * 0.75
let leftEdge = max(-edge, min(0.0, buttonFrame.minX - actualVisibleRect.minX)) / -edge
let rightEdge = min(edge, max(0.0, buttonFrame.maxX - actualVisibleRect.maxX)) / edge
let transitionFraction: CGFloat
if leftEdge > rightEdge {
transitionFraction = leftEdge
} else {
transitionFraction = -rightEdge
}
var buttonTransition = transition
let buttonView: ComponentHostView<Empty>
if let current = self.buttonViews[i] {
buttonView = current
} else {
buttonTransition = .immediate
buttonView = ComponentHostView<Empty>()
self.buttonViews[i] = buttonView
self.scrollNode.view.addSubview(buttonView)
}
let type = self.buttons[i]
let _ = buttonView.update(
transition: buttonTransition,
component: AnyComponent(AttachButtonComponent(
context: self.context,
type: type,
isSelected: i == self.selectedIndex,
isCollapsed: self.isCollapsed,
transitionFraction: transitionFraction,
strings: self.presentationData.strings,
theme: self.presentationData.theme,
action: { [weak self] in
if let strongSelf = self {
let ascending = i > strongSelf.selectedIndex
strongSelf.selectedIndex = i
strongSelf.selectionChanged(type, ascending)
strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring)))
}
})
),
environment: {},
containerSize: buttonSize
)
buttonTransition.setFrame(view: buttonView, frame: buttonFrame)
}
}
private func updateScrollLayoutIfNeeded(force: Bool, transition: ContainedViewLayoutTransition) -> Bool {
guard let size = self.validLayout else {
return false
}
if self.scrollLayout?.width == size.width && !force {
return false
}
let buttonSize = self.isCollapsed ? smallPanelButtonSize : panelButtonSize
let contentSize = CGSize(width: (self.isCollapsed ? smallSideInset : sideInset) * 2.0 + CGFloat(self.buttons.count) * buttonSize.width, height: buttonSize.height)
self.scrollLayout = (size.width, contentSize)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
self.scrollNode.view.contentSize = contentSize
return true
}
func update(buttons: [AttachmentButtonType], isCollapsed: Bool, size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
self.buttons = buttons
let isCollapsedUpdated = self.isCollapsed != isCollapsed
self.isCollapsed = isCollapsed
let bounds = CGRect(origin: CGPoint(), size: size)
let containerTransition: ContainedViewLayoutTransition
let containerFrame: CGRect
if isCollapsed {
containerFrame = CGRect(origin: CGPoint(x: 0.0, y: bounds.height - smallPanelButtonSize.height), size: CGSize(width: bounds.width, height: smallPanelButtonSize.height))
} else {
containerFrame = bounds
}
let containerBounds = CGRect(origin: CGPoint(), size: containerFrame.size)
if isCollapsedUpdated {
containerTransition = .animated(duration: 0.25, curve: .easeInOut)
} else {
containerTransition = transition
}
containerTransition.updateFrame(node: self.containerNode, frame: containerFrame)
if let effectView = self.effectView {
containerTransition.updateFrame(view: effectView, frame: bounds)
}
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
var shadowFrame = bounds.insetBy(dx: -16.0, dy: -24.0)
shadowFrame.size.height += 44.0
transition.updateFrame(node: self.shadowNode, frame: shadowFrame)
let _ = self.updateScrollLayoutIfNeeded(force: isCollapsedUpdated, transition: containerTransition)
var buttonTransition: Transition = .immediate
if isCollapsedUpdated {
buttonTransition = .easeInOut(duration: 0.25)
}
self.updateViews(transition: buttonTransition)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateViews(transition: .immediate)
}
}

View file

@ -67,8 +67,14 @@ public extension Transition.DisappearWithGuide {
public extension Transition.Update {
static let `default` = Transition.Update { component, view, transition in
let frame = component.size.centered(around: component._position ?? CGPoint())
if view.frame != frame {
transition.setFrame(view: view, frame: frame)
if let scale = component._scale {
transition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: frame.size))
transition.setPosition(view: view, position: frame.center)
transition.setScale(view: view, scale: scale)
} else {
if view.frame != frame {
transition.setFrame(view: view, frame: frame)
}
}
let opacity = component._opacity ?? 1.0
if view.alpha != opacity {

View file

@ -175,6 +175,7 @@ public final class _UpdatedChildComponent {
var _removed: Bool = false
var _position: CGPoint?
var _scale: CGFloat?
var _opacity: CGFloat?
var _cornerRadius: CGFloat?
var _clipsToBounds: Bool?
@ -239,6 +240,11 @@ public final class _UpdatedChildComponent {
return self
}
@discardableResult public func scale(_ scale: CGFloat) -> _UpdatedChildComponent {
self._scale = scale
return self
}
@discardableResult public func opacity(_ opacity: CGFloat) -> _UpdatedChildComponent {
self._opacity = opacity
return self
@ -683,7 +689,13 @@ public extension CombinedComponent {
view.insertSubview(updatedChild.view, at: index)
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
if let scale = updatedChild._scale {
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
updatedChild.view.center = updatedChild._position ?? CGPoint()
updatedChild.view.transform = CGAffineTransform(scaleX: scale, y: scale)
} else {
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
}
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0

View file

@ -176,6 +176,40 @@ public struct Transition {
}
}
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
if view.bounds == bounds {
completion?(true)
return
}
switch self.animation {
case .none:
view.bounds = bounds
completion?(true)
case .curve:
let previousBounds = view.bounds
view.bounds = bounds
self.animateBounds(view: view, from: previousBounds, to: view.bounds, completion: completion)
}
}
public func setPosition(view: UIView, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
if view.center == position {
completion?(true)
return
}
switch self.animation {
case .none:
view.center = position
completion?(true)
case .curve:
let previousPosition = view.center
view.center = position
self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion)
}
}
public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
if view.alpha == alpha {
completion?(true)
@ -191,7 +225,35 @@ public struct Transition {
self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion)
}
}
public func setScale(view: UIView, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
let t = view.layer.presentation()?.transform ?? view.layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale == scale {
completion?(true)
return
}
switch self.animation {
case .none:
view.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
completion?(true)
case let .curve(duration, curve):
let previousScale = currentScale
view.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
view.layer.animate(
from: previousScale as NSNumber,
to: scale as NSNumber,
keyPath: "transform.scale",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: false,
completion: completion
)
}
}
public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
switch self.animation {
case .none:

View file

@ -134,7 +134,7 @@ private func endAnimations(view: UIView) {
}
}
func viewTreeContainsFirstResponder(view: UIView) -> Bool {
public func viewTreeContainsFirstResponder(view: UIView) -> Bool {
if view.isFirstResponder {
return true
} else {
@ -147,14 +147,14 @@ func viewTreeContainsFirstResponder(view: UIView) -> Bool {
}
}
final class KeyboardViewManager {
public final class KeyboardViewManager {
private let host: StatusBarHost
init(host: StatusBarHost) {
self.host = host
}
func dismissEditingWithoutAnimation(view: UIView) {
public func dismissEditingWithoutAnimation(view: UIView) {
if viewTreeContainsFirstResponder(view: view) {
view.endEditing(true)
if let keyboardWindow = self.host.keyboardWindow {
@ -165,7 +165,7 @@ final class KeyboardViewManager {
}
}
func update(leftEdge: CGFloat, transition: ContainedViewLayoutTransition) {
public func update(leftEdge: CGFloat, transition: ContainedViewLayoutTransition) {
guard let keyboardWindow = self.host.keyboardWindow else {
return
}

View file

@ -3,7 +3,7 @@ import UIKit
import AsyncDisplayKit
import SwiftSignalKit
final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
private final class Child {
let value: ViewController
var layout: ContainerViewLayout
@ -73,19 +73,20 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
var pending: PendingChild?
}
private(set) var controllers: [ViewController] = []
public private(set) var controllers: [ViewController] = []
private var state: State = State(layout: nil, canBeClosed: nil, top: nil, transition: nil, pending: nil)
private var ignoreInputHeight: Bool = false
private(set) var isReady: Bool = false
var isReadyUpdated: (() -> Void)?
var controllerRemoved: (ViewController) -> Void
var keyboardViewManager: KeyboardViewManager? {
public private(set) var isReady: Bool = false
public var isReadyUpdated: (() -> Void)?
public var controllerRemoved: (ViewController) -> Void
public var keyboardViewManager: KeyboardViewManager? {
didSet {
}
}
var canHaveKeyboardFocus: Bool = false {
public var canHaveKeyboardFocus: Bool = false {
didSet {
if self.canHaveKeyboardFocus != oldValue {
if !self.canHaveKeyboardFocus {
@ -96,14 +97,15 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
var isInFocus: Bool = false {
public var isInFocus: Bool = false {
didSet {
if self.isInFocus != oldValue {
self.inFocusUpdated(isInFocus: self.isInFocus)
}
}
}
func inFocusUpdated(isInFocus: Bool) {
public func inFocusUpdated(isInFocus: Bool) {
self.state.top?.value.isInFocus = isInFocus
}
@ -113,13 +115,13 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
var statusBarStyle: StatusBarStyle = .Ignore
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
init(controllerRemoved: @escaping (ViewController) -> Void) {
public init(controllerRemoved: @escaping (ViewController) -> Void) {
self.controllerRemoved = controllerRemoved
super.init()
}
override func didLoad() {
public override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
@ -263,7 +265,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
func update(layout: ContainerViewLayout, canBeClosed: Bool, controllers: [ViewController], transition: ContainedViewLayoutTransition) {
public func update(layout: ContainerViewLayout, canBeClosed: Bool, controllers: [ViewController], transition: ContainedViewLayoutTransition) {
self.state.layout = layout
self.state.canBeClosed = canBeClosed
@ -539,7 +541,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
func updateAdditionalKeyboardLeftEdgeOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
public func updateAdditionalKeyboardLeftEdgeOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.additionalKeyboardLeftEdgeOffset = offset
self.syncKeyboard(leftEdge: self.currentKeyboardLeftEdge, transition: transition)
}
@ -562,7 +564,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}

View file

@ -534,7 +534,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
navigationController.filterController(self, animated: animated)
} else {
self.presentingViewController?.dismiss(animated: false, completion: nil)
assertionFailure()
}
}
@ -612,7 +611,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
if let navigationController = self.navigationController as? NavigationController {
navigationController.filterController(self, animated: true)
} else {
self.presentingViewController?.dismiss(animated: false, completion: nil)
self.presentingViewController?.dismiss(animated: true, completion: nil)
}
}

View file

@ -14,11 +14,11 @@ public final class GameController: ViewController {
private let context: AccountContext
private let url: String
private let message: EngineMessage
private let message: EngineMessage?
private var presentationData: PresentationData
public init(context: AccountContext, url: String, message: EngineMessage) {
public init(context: AccountContext, url: String, message: EngineMessage?) {
self.context = context
self.url = url
self.message = message
@ -29,26 +29,30 @@ public final class GameController: ViewController {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed))
for media in message.media {
if let game = media as? TelegramMediaGame {
let titleView = GameControllerTitleView(theme: self.presentationData.theme)
var botPeer: EnginePeer?
inner: for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId {
botPeer = message.peers[peerId].flatMap(EnginePeer.init)
break inner
if let message = message {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed))
for media in message.media {
if let game = media as? TelegramMediaGame {
let titleView = GameControllerTitleView(theme: self.presentationData.theme)
var botPeer: EnginePeer?
inner: for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId {
botPeer = message.peers[peerId].flatMap(EnginePeer.init)
break inner
}
}
if botPeer == nil {
botPeer = message.author
}
titleView.set(title: game.title, subtitle: "@\(botPeer?.addressName ?? "")")
self.navigationItem.titleView = titleView
}
if botPeer == nil {
botPeer = message.author
}
titleView.set(title: game.title, subtitle: "@\(botPeer?.addressName ?? "")")
self.navigationItem.titleView = titleView
}
} else {
self.title = "App"
}
}

View file

@ -30,9 +30,9 @@ final class GameControllerNode: ViewControllerTracingNode {
private let context: AccountContext
var presentationData: PresentationData
private let present: (ViewController, Any?) -> Void
private let message: EngineMessage
private let message: EngineMessage?
init(context: AccountContext, presentationData: PresentationData, url: String, present: @escaping (ViewController, Any?) -> Void, message: EngineMessage) {
init(context: AccountContext, presentationData: PresentationData, url: String, present: @escaping (ViewController, Any?) -> Void, message: EngineMessage?) {
self.context = context
self.presentationData = presentationData
self.present = present
@ -107,18 +107,21 @@ final class GameControllerNode: ViewControllerTracingNode {
}
private func shareData() -> (EnginePeer, String)? {
guard let message = self.message else {
return nil
}
var botPeer: EnginePeer?
var gameName: String?
for media in self.message.media {
for media in message.media {
if let game = media as? TelegramMediaGame {
inner: for attribute in self.message.attributes {
inner: for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId {
botPeer = self.message.peers[peerId].flatMap(EnginePeer.init)
botPeer = message.peers[peerId].flatMap(EnginePeer.init)
break inner
}
}
if botPeer == nil {
botPeer = self.message.author
botPeer = message.author
}
gameName = game.name
@ -144,8 +147,8 @@ final class GameControllerNode: ViewControllerTracingNode {
if let (botPeer, gameName) = self.shareData(), let addressName = botPeer.addressName, !addressName.isEmpty, !gameName.isEmpty {
if eventName == "share_score" {
self.present(ShareController(context: self.context, subject: .fromExternal({ [weak self] peerIds, text, account, _ in
if let strongSelf = self {
let signals = peerIds.map { TelegramEngine(account: account).messages.forwardGameWithScore(messageId: strongSelf.message.id, to: $0, as: nil) }
if let strongSelf = self, let message = strongSelf.message {
let signals = peerIds.map { TelegramEngine(account: account).messages.forwardGameWithScore(messageId: message.id, to: $0, as: nil) }
return .single(.preparing)
|> then(
combineLatest(signals)

View file

@ -32,8 +32,6 @@ private struct StickerPackPreviewGridEntry: Comparable, Equatable, Identifiable
func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem {
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isVerified: self.isVerified)
}
}
private struct StickerPackPreviewGridTransaction {

View file

@ -761,6 +761,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
animationNode = current
} else {
animationNode = AnimatedStickerNode()
animationNode.started = { [weak self] in
self?.removePlaceholder(animated: false)
}
strongSelf.animationNode = animationNode
strongSelf.addSubnode(animationNode)

View file

@ -211,6 +211,9 @@
if (strongController == nil)
return;
if (strongController->_toolbarView.superview == nil)
return;
UIView *toolbarView = strongController->_toolbarView;
if (enabled)
{
@ -271,7 +274,8 @@
[groupsController setIsFirstInStack:true];
[pickerController setIsFirstInStack:false];
[assetsController setViewControllers:@[ groupsController, pickerController ]];
// [assetsController setViewControllers:@[ groupsController, pickerController ]];
[assetsController setViewControllers:@[ pickerController ]];
((TGNavigationBar *)assetsController.navigationBar).navigationController = assetsController;
assetsController.recipientName = recipientName;
@ -611,7 +615,8 @@
[strongSelf groupPhotosPressed];
};
}
[self.view addSubview:_toolbarView];
if (_intent != TGMediaAssetsControllerSendMediaIntent)
[self.view addSubview:_toolbarView];
if (@available(iOS 14.0, *)) {
if ([PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite] == PHAuthorizationStatusLimited) {

View file

@ -89,7 +89,8 @@
_assetGroup = assetGroup;
_intent = intent;
[self setTitle:_assetGroup.title];
[self setTitle:@"Gallery"];
// [self setTitle:_assetGroup.title];
_assetsDisposable = [[SMetaDisposable alloc] init];
}
@ -167,7 +168,8 @@
if (strongSelf->_assetGroup == nil)
strongSelf->_assetGroup = assetGroup;
[strongSelf setTitle:assetGroup.title];
[self setTitle:@"Gallery"];
// [strongSelf setTitle:assetGroup.title];
return [strongSelf->_assetsLibrary assetsOfAssetGroup:assetGroup reversed:false];
}] deliverOn:[SQueue mainQueue]] startWithNext:^(id next)
@ -198,7 +200,7 @@
if (scrollToBottom)
{
[strongSelf->_collectionView layoutSubviews];
[strongSelf _adjustContentOffsetToBottom];
// [strongSelf _adjustContentOffsetToBottom];
}
}
else if ([next isKindOfClass:[TGMediaAssetFetchResultChange class]])

View file

@ -326,7 +326,7 @@ static id<TGNavigationBarMusicPlayerProvider> _musicPlayerProvider;
}
[super setFrame:frame];
if (_statusBarBackgroundView != nil && _statusBarBackgroundView.superview != nil)
{
_statusBarBackgroundView.frame = CGRectMake(0, -self.frame.origin.y, self.frame.size.width, 20);

View file

@ -453,6 +453,10 @@ open class LegacyController: ViewController, PresentableController {
}
if self.controllerNode.controllerView == nil {
if self.controllerNode.frame.width == 0.0, let layout = self.validLayout {
self.controllerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
}
self.controllerNode.controllerView = self.legacyController.view
if let legacyController = self.legacyController as? TGViewController {
legacyController.ignoreAppearEvents = true

View file

@ -831,7 +831,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
}
}
let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
// let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
let topInset: CGFloat = 240.0
let overlap: CGFloat = 6.0
let headerHeight: CGFloat
if isPickingLocation, let actionHeight = actionHeight {

View file

@ -65,6 +65,8 @@ public enum PresentationResourceKey: Int32 {
case itemListBlockDestructiveIcon
case itemListAddDeviceIcon
case itemListResetIcon
case itemListImageIcon
case itemListCloudIcon
case itemListVoiceCallIcon
case itemListVideoCallIcon

View file

@ -222,6 +222,18 @@ public struct PresentationResourcesItemList {
})
}
public static func imageIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListImageIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: theme.list.itemAccentColor)
})
}
public static func cloudIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListCloudIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Cloud"), color: theme.list.itemAccentColor)
})
}
public static func cornersImage(_ theme: PresentationTheme, top: Bool, bottom: Bool) -> UIImage? {
if !top && !bottom {
return nil

View file

@ -256,6 +256,7 @@ swift_library(
"//submodules/TabBarUI:TabBarUI",
"//submodules/SoftwareVideo:SoftwareVideo",
"//submodules/ManagedFile:ManagedFile",
"//submodules/AttachmentUI:AttachmentUI",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Property 1=Camera.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,118 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 14.000000 17.000000 cm
1.000000 1.000000 1.000000 scn
0.000000 16.899923 m
0.000000 17.921806 0.000000 18.432747 0.062100 18.861015 c
0.443433 21.490837 2.509163 23.556566 5.138984 23.937901 c
5.567253 24.000000 6.078194 24.000000 7.100077 24.000000 c
7.197224 24.000000 l
7.389089 24.000000 7.485021 24.000000 7.575428 24.005465 c
8.449500 24.058290 9.256863 24.490376 9.785665 25.188347 c
9.840356 25.260532 9.893565 25.340347 9.999977 25.499966 c
10.000000 25.500000 l
10.000046 25.500069 l
10.000060 25.500093 l
10.106449 25.659672 10.159652 25.739475 10.214336 25.811653 c
10.743138 26.509623 11.550501 26.941710 12.424573 26.994535 c
12.514979 27.000000 12.610912 27.000000 12.802777 27.000000 c
19.197224 27.000000 l
19.389088 27.000000 19.485020 27.000000 19.575428 26.994535 c
20.449501 26.941710 21.256863 26.509623 21.785666 25.811653 c
21.840359 25.739460 21.893574 25.659641 22.000000 25.500000 c
22.106426 25.340359 22.159641 25.260540 22.214334 25.188347 c
22.743137 24.490377 23.550499 24.058289 24.424572 24.005465 c
24.514980 24.000000 24.610912 24.000000 24.802776 24.000000 c
24.899923 24.000000 l
25.921803 24.000000 26.432743 24.000000 26.861012 23.937901 c
29.490833 23.556566 31.556562 21.490837 31.937897 18.861015 c
31.999996 18.432747 31.999996 17.921806 31.999996 16.899923 c
31.999996 9.600000 l
31.999996 6.239685 31.999996 4.559526 31.346035 3.276056 c
30.770796 2.147085 29.852911 1.229200 28.723940 0.653961 c
27.440470 0.000000 25.760311 0.000000 22.399996 0.000000 c
9.599999 0.000000 l
6.239685 0.000000 4.559527 0.000000 3.276057 0.653961 c
2.147084 1.229200 1.229201 2.147085 0.653961 3.276056 c
0.000000 4.559526 0.000000 6.239685 0.000000 9.600000 c
0.000000 16.899923 l
h
16.000000 16.500000 m
13.514719 16.500000 11.500000 14.485281 11.500000 12.000000 c
11.500000 9.514719 13.514719 7.500000 16.000000 7.500000 c
18.485281 7.500000 20.500000 9.514719 20.500000 12.000000 c
20.500000 14.485281 18.485281 16.500000 16.000000 16.500000 c
h
8.500000 12.000000 m
8.500000 16.142136 11.857864 19.500000 16.000000 19.500000 c
20.142136 19.500000 23.500000 16.142136 23.500000 12.000000 c
23.500000 7.857864 20.142136 4.500000 16.000000 4.500000 c
11.857864 4.500000 8.500000 7.857864 8.500000 12.000000 c
h
26.000000 16.000000 m
27.104570 16.000000 28.000000 16.895432 28.000000 18.000000 c
28.000000 19.104568 27.104570 20.000000 26.000000 20.000000 c
24.895430 20.000000 24.000000 19.104568 24.000000 18.000000 c
24.000000 16.895432 24.895430 16.000000 26.000000 16.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
2589
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 60.000000 60.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002679 00000 n
0000002702 00000 n
0000002875 00000 n
0000002949 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3008
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "cloud_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,213 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.500000 5.569031 cm
0.000000 0.000000 0.000000 scn
20.853670 11.223368 m
20.197720 11.332699 l
20.167723 11.152725 20.212988 10.968333 20.322916 10.822711 c
20.432844 10.677088 20.597776 10.583029 20.779081 10.562564 c
20.853670 11.223368 l
h
20.858553 11.252664 m
21.514503 11.143333 l
21.520527 11.179465 21.523552 11.216033 21.523552 11.252664 c
20.858553 11.252664 l
h
15.873398 17.663544 m
16.037714 18.307922 l
16.037458 18.307989 l
15.873398 17.663544 l
h
8.432284 14.406836 m
8.123861 13.817684 l
8.445732 13.649182 8.843261 13.770190 9.016658 14.089451 c
8.432284 14.406836 l
h
3.232294 11.457733 m
3.459931 10.832909 l
3.709621 10.923876 3.881181 11.154763 3.896227 11.420082 c
3.232294 11.457733 l
h
20.345879 1.926858 m
20.345879 1.261856 l
20.347536 1.261860 l
20.345879 1.926858 l
h
21.509621 11.114037 m
21.514503 11.143333 l
20.202602 11.361995 l
20.197720 11.332699 l
21.509621 11.114037 l
h
21.523552 11.252664 m
21.523552 14.584707 19.261030 17.485977 16.037714 18.307922 c
15.709081 17.019163 l
18.344908 16.347027 20.193552 13.975069 20.193552 11.252664 c
21.523552 11.252664 l
h
16.037458 18.307989 m
12.807217 19.130329 9.437991 17.651901 7.847911 14.724220 c
9.016658 14.089451 l
10.317089 16.483826 13.070669 17.690840 15.709337 17.019098 c
16.037458 18.307989 l
h
8.740708 14.995987 m
6.028514 16.415833 2.742277 14.562114 2.568361 11.495386 c
3.896227 11.420082 l
4.015268 13.519165 6.265923 14.790322 8.123861 13.817684 c
8.740708 14.995987 l
h
3.004657 12.082559 m
0.798734 11.278893 -0.665000 9.183575 -0.665000 6.843657 c
0.665000 6.843657 l
0.665000 8.624672 1.779289 10.220613 3.459931 10.832909 c
3.004657 12.082559 l
h
-0.665000 6.843657 m
-0.665000 3.761652 1.834792 1.261858 4.916798 1.261858 c
4.916798 2.591858 l
2.569331 2.591858 0.665000 4.496190 0.665000 6.843657 c
-0.665000 6.843657 l
h
4.916798 1.261858 m
20.345879 1.261858 l
20.345879 2.591858 l
4.916798 2.591858 l
4.916798 1.261858 l
h
20.347536 1.261860 m
27.062166 1.278605 27.596050 11.131536 20.928259 11.884171 c
20.779081 10.562564 l
25.790516 9.996892 25.377172 2.604407 20.344219 2.591856 c
20.347536 1.261860 l
h
f
n
Q
endstream
endobj
2 0 obj
2195
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 30.000000 m
30.000000 30.000000 l
30.000000 0.000000 l
0.000000 0.000000 l
0.000000 30.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
232
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000002453 00000 n
0000002476 00000 n
0000002956 00000 n
0000002978 00000 n
0000003276 00000 n
0000003378 00000 n
0000003399 00000 n
0000003572 00000 n
0000003646 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
3706
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Union-2.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,79 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
1.000000 1.000000 1.000000 scn
12.473795 18.000000 m
15.787504 18.000000 18.473795 20.686291 18.473795 24.000000 c
18.473795 27.313709 15.787504 30.000000 12.473795 30.000000 c
9.160086 30.000000 6.473795 27.313709 6.473795 24.000000 c
6.473795 20.686291 9.160086 18.000000 12.473795 18.000000 c
h
12.473970 14.000000 m
17.620707 14.000000 22.118784 11.222771 24.551340 7.085182 c
25.277521 5.850006 24.987329 4.309250 23.837667 3.454098 c
21.863237 1.985462 18.145235 0.000000 12.473970 0.000000 c
6.802708 0.000000 3.084706 1.985462 1.110274 3.454098 c
-0.039390 4.309246 -0.329582 5.850006 0.396601 7.085182 c
2.829157 11.222771 7.327235 14.000000 12.473970 14.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
770
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.947937 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000860 00000 n
0000000882 00000 n
0000001055 00000 n
0000001129 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1188
%%EOF

View file

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Subtract.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,96 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
1.000000 1.000000 1.000000 scn
14.400000 0.000000 m
9.600000 0.000000 l
6.239685 0.000000 4.559527 0.000000 3.276057 0.653961 c
2.147084 1.229200 1.229201 2.147085 0.653961 3.276056 c
0.000000 4.559526 0.000000 6.239685 0.000000 9.600000 c
0.000000 20.400002 l
0.000000 23.760315 0.000000 25.440472 0.653961 26.723944 c
1.229201 27.852915 2.147084 28.770800 3.276057 29.346039 c
4.559527 30.000000 6.239685 30.000000 9.600000 30.000000 c
11.341846 30.000000 l
12.345142 30.000000 12.846791 30.000000 13.317464 29.884233 c
13.734708 29.781607 14.132692 29.612423 14.496114 29.383186 c
14.906072 29.124596 15.254162 28.763371 15.950342 28.040918 c
22.208498 21.546604 l
22.208509 21.546591 l
22.870174 20.859959 23.201010 20.516640 23.437363 20.119482 c
23.646940 19.767319 23.801153 19.385040 23.894608 18.986031 c
24.000000 18.536037 24.000000 18.059254 24.000000 17.105690 c
24.000000 9.600000 l
24.000000 6.239685 24.000000 4.559526 23.346039 3.276056 c
22.770800 2.147085 21.852915 1.229200 20.723944 0.653961 c
19.440474 0.000000 17.760315 0.000000 14.400000 0.000000 c
h
21.023287 16.875000 m
14.187500 16.875000 l
13.082931 16.875000 12.187500 17.770432 12.187500 18.875000 c
12.187500 25.710787 l
12.187500 26.601692 13.264641 27.047859 13.894606 26.417894 c
21.730392 18.582108 l
22.360359 17.952141 21.914192 16.875000 21.023287 16.875000 c
h
f*
n
Q
endstream
endobj
3 0 obj
1447
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001537 00000 n
0000001560 00000 n
0000001733 00000 n
0000001807 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1866
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Union.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,123 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.000050 cm
1.000000 1.000000 1.000000 scn
11.100000 28.000050 m
11.035790 28.000050 l
9.410112 28.000065 8.098883 28.000076 7.037065 27.913322 c
5.943791 27.823997 4.983509 27.635279 4.095072 27.182598 c
2.683856 26.463547 1.536502 25.316193 0.817452 23.904978 c
0.364771 23.016541 0.176052 22.056259 0.086728 20.962984 c
-0.000027 19.901167 -0.000015 18.589939 0.000000 16.964260 c
0.000000 16.900049 l
0.000000 11.100050 l
0.000000 11.035839 l
-0.000015 9.410160 -0.000027 8.098932 0.086728 7.037113 c
0.176052 5.943840 0.364771 4.983559 0.817452 4.095121 c
1.536502 2.683905 2.683856 1.536551 4.095072 0.817501 c
4.983509 0.364819 5.943791 0.176100 7.037065 0.086777 c
8.098874 0.000023 9.410091 0.000034 11.035753 0.000050 c
11.100000 0.000050 l
16.900000 0.000050 l
16.964247 0.000050 l
18.589909 0.000034 19.901125 0.000023 20.962936 0.086777 c
22.056210 0.176100 23.016491 0.364819 23.904928 0.817501 c
25.316145 1.536551 26.463499 2.683905 27.182549 4.095121 c
27.635231 4.983559 27.823950 5.943840 27.913273 7.037113 c
28.000027 8.098925 28.000015 9.410141 28.000000 11.035803 c
28.000000 11.100050 l
28.000000 16.900049 l
28.000000 16.964296 l
28.000015 18.589958 28.000027 19.901175 27.913273 20.962984 c
27.823950 22.056259 27.635231 23.016541 27.182549 23.904978 c
26.463499 25.316193 25.316145 26.463547 23.904928 27.182598 c
23.016491 27.635279 22.056210 27.823997 20.962936 27.913322 c
19.901117 28.000076 18.589890 28.000065 16.964211 28.000050 c
16.900000 28.000050 l
11.100000 28.000050 l
h
3.093551 7.093601 m
3.170904 6.300156 3.303499 5.824047 3.490471 5.457092 c
3.921902 4.610363 4.610314 3.921951 5.457044 3.490520 c
5.852077 3.289240 6.373610 3.150980 7.281361 3.076813 c
8.206622 3.001217 9.395091 3.000050 11.100000 3.000050 c
16.900000 3.000050 l
18.604908 3.000050 19.793379 3.001217 20.718641 3.076813 c
21.626392 3.150980 22.147924 3.289240 22.542957 3.490520 c
23.389687 3.921951 24.078098 4.610363 24.509529 5.457092 c
24.710810 5.852125 24.849070 6.373657 24.923237 7.281408 c
24.963232 7.770926 24.982393 8.334116 24.991571 9.008478 c
19.060661 14.939390 l
18.474873 15.525175 17.525127 15.525177 16.939339 14.939390 c
11.000000 9.000050 l
9.060661 10.939388 l
8.474873 11.525177 7.525128 11.525177 6.939342 10.939392 c
3.093551 7.093601 l
h
9.000000 16.000050 m
10.656855 16.000050 12.000000 17.343195 12.000000 19.000050 c
12.000000 20.656902 10.656855 22.000050 9.000000 22.000050 c
7.343146 22.000050 6.000001 20.656902 6.000001 19.000050 c
6.000001 17.343195 7.343146 16.000050 9.000000 16.000050 c
h
f*
n
Q
endstream
endobj
3 0 obj
2627
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 28.000000 28.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002717 00000 n
0000002740 00000 n
0000002913 00000 n
0000002987 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3046
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "image_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,160 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.335022 5.335083 cm
0.000000 0.000000 0.000000 scn
7.065000 19.329956 m
7.035460 19.329956 l
5.940372 19.329960 5.077796 19.329964 4.383656 19.273251 c
3.675523 19.215395 3.084329 19.095215 2.547134 18.821501 c
1.669358 18.374252 0.955704 17.660599 0.508455 16.782822 c
0.234740 16.245626 0.114562 15.654433 0.056705 14.946301 c
-0.000008 14.252161 -0.000005 13.389584 0.000000 12.294496 c
0.000000 12.264956 l
0.000000 7.064956 l
0.000000 7.035416 l
-0.000005 5.940329 -0.000008 5.077752 0.056705 4.383612 c
0.114562 3.675479 0.234740 3.084284 0.508455 2.547091 c
0.955704 1.669313 1.669358 0.955660 2.547134 0.508410 c
3.084329 0.234695 3.675523 0.114517 4.383656 0.056660 c
5.077746 -0.000051 5.940250 -0.000048 7.035228 -0.000044 c
7.035275 -0.000044 l
7.035323 -0.000044 l
7.035370 -0.000044 l
7.065000 -0.000044 l
12.265000 -0.000044 l
12.294630 -0.000044 l
12.294676 -0.000044 l
12.294722 -0.000044 l
12.294767 -0.000044 l
13.389750 -0.000048 14.252252 -0.000051 14.946344 0.056660 c
15.654477 0.114517 16.245672 0.234695 16.782866 0.508410 c
17.660643 0.955660 18.374296 1.669313 18.821547 2.547091 c
19.095261 3.084284 19.215439 3.675479 19.273296 4.383612 c
19.330008 5.077704 19.330004 5.940207 19.330000 7.035190 c
19.330000 7.035235 l
19.330000 7.035281 l
19.330000 7.035327 l
19.330000 7.064957 l
19.330000 12.264956 l
19.330000 12.294586 l
19.330000 12.294634 l
19.330000 12.294681 l
19.330000 12.294727 l
19.330004 13.389706 19.330008 14.252210 19.273296 14.946301 c
19.215439 15.654433 19.095261 16.245626 18.821547 16.782822 c
18.374296 17.660599 17.660643 18.374252 16.782866 18.821501 c
16.245672 19.095215 15.654477 19.215395 14.946344 19.273251 c
14.252204 19.329964 13.389627 19.329960 12.294539 19.329956 c
12.264999 19.329956 l
7.065000 19.329956 l
h
3.150942 17.636463 m
3.469394 17.798721 3.866076 17.896530 4.491960 17.947668 c
5.125607 17.999439 5.933921 17.999956 7.065000 17.999956 c
12.264999 17.999956 l
13.396078 17.999956 14.204392 17.999439 14.838039 17.947668 c
15.463923 17.896530 15.860606 17.798721 16.179058 17.636463 c
16.806580 17.316725 17.316771 16.806536 17.636507 16.179014 c
17.798767 15.860562 17.896576 15.463881 17.947712 14.837996 c
17.999483 14.204350 18.000000 13.396034 18.000000 12.264956 c
18.000000 7.064957 l
18.000000 6.309554 17.999769 5.698115 17.984194 5.186526 c
15.466902 7.867050 l
14.693245 8.690873 13.378143 8.669041 12.632261 7.819986 c
11.520865 6.554860 l
6.709533 11.633489 l
5.937637 12.448267 4.633699 12.427576 3.888045 11.588715 c
1.330000 8.710914 l
1.330000 12.264956 l
1.330000 13.396034 1.330517 14.204349 1.382288 14.837996 c
1.433425 15.463881 1.531234 15.860562 1.693493 16.179014 c
2.013231 16.806536 2.523421 17.316725 3.150942 17.636463 c
h
14.497394 6.956580 m
17.768957 3.472878 l
17.730608 3.355118 17.686632 3.249275 17.636507 3.150898 c
17.316771 2.523376 16.806580 2.013186 16.179058 1.693449 c
16.167240 1.687428 16.155315 1.681494 16.143274 1.675650 c
12.439418 5.585277 l
13.631460 6.942204 l
13.859314 7.201574 14.261054 7.208245 14.497394 6.956580 c
h
1.330082 6.709091 m
1.330711 5.761070 1.336117 5.057030 1.382288 4.491917 c
1.433425 3.866033 1.531234 3.469350 1.693493 3.150898 c
2.013231 2.523376 2.523421 2.013186 3.150942 1.693449 c
3.469394 1.531189 3.866076 1.433380 4.491960 1.382244 c
5.125607 1.330473 5.933922 1.329956 7.065000 1.329956 c
12.265000 1.329956 l
13.261761 1.329956 14.007863 1.330357 14.604662 1.365883 c
5.744016 10.718788 l
5.508215 10.967690 5.109884 10.961369 4.882100 10.705111 c
1.330082 6.709091 l
h
14.040000 12.164956 m
15.075534 12.164956 15.915000 13.004422 15.915000 14.039956 c
15.915000 15.075490 15.075534 15.914956 14.040000 15.914956 c
13.004466 15.914956 12.165000 15.075490 12.165000 14.039956 c
12.165000 13.004422 13.004466 12.164956 14.040000 12.164956 c
h
f*
n
Q
endstream
endobj
3 0 obj
3879
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003969 00000 n
0000003992 00000 n
0000004165 00000 n
0000004239 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4298
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Subtract-2.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,77 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
1.000000 1.000000 1.000000 scn
12.000000 0.000000 m
15.627419 0.000000 24.000000 11.372581 24.000000 20.000000 c
24.000000 26.627417 18.627419 32.000000 12.000000 32.000000 c
5.372583 32.000000 0.000000 26.627417 0.000000 20.000000 c
0.000000 11.372581 8.372583 0.000000 12.000000 0.000000 c
h
12.000000 15.000000 m
14.761425 15.000000 17.000000 17.238577 17.000000 20.000000 c
17.000000 22.761423 14.761425 25.000000 12.000000 25.000000 c
9.238577 25.000000 7.000000 22.761423 7.000000 20.000000 c
7.000000 17.238577 9.238577 15.000000 12.000000 15.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
656
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 32.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000746 00000 n
0000000768 00000 n
0000000941 00000 n
0000001015 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1074
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Union.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

View file

@ -0,0 +1,139 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
private final class AttachmentFileControllerArguments {
let openGallery: () -> Void
let openFiles: () -> Void
init(openGallery: @escaping () -> Void, openFiles: @escaping () -> Void) {
self.openGallery = openGallery
self.openFiles = openFiles
}
}
private enum AttachmentFileSection: Int32 {
case select
case recent
}
private enum AttachmentFileEntry: ItemListNodeEntry {
case selectFromGallery(PresentationTheme, String)
case selectFromFiles(PresentationTheme, String)
case recentHeader(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .selectFromGallery, .selectFromFiles:
return AttachmentFileSection.select.rawValue
case .recentHeader:
return AttachmentFileSection.recent.rawValue
}
}
var stableId: Int32 {
switch self {
case .selectFromGallery:
return 0
case .selectFromFiles:
return 1
case .recentHeader:
return 2
}
}
static func ==(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool {
switch lhs {
case let .selectFromGallery(lhsTheme, lhsText):
if case let .selectFromGallery(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .selectFromFiles(lhsTheme, lhsText):
if case let .selectFromFiles(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .recentHeader(lhsTheme, lhsText):
if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! AttachmentFileControllerArguments
switch self {
case let .selectFromGallery(_, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.imageIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openGallery()
})
case let .selectFromFiles(_, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openFiles()
})
case let .recentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
}
}
}
private func attachmentFileControllerEntries(presentationData: PresentationData) -> [AttachmentFileEntry] {
var entries: [AttachmentFileEntry] = []
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
entries.append(.recentHeader(presentationData.theme, "RECENTLY SENT FILES".uppercased()))
return entries
}
public func attachmentFileController(context: AccountContext, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void) -> ViewController {
let actionsDisposable = DisposableSet()
var dismissImpl: (() -> Void)?
let arguments = AttachmentFileControllerArguments(openGallery: {
presentGallery()
}, openFiles: {
presentFiles()
})
let signal = context.sharedContext.presentationData
|> deliverOnMainQueue
|> map { presentationData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Attachment_File), leftNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
}), rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
return controller
}

View file

@ -68,6 +68,7 @@ import CalendarMessageScreen
import ReactionSelectionNode
import LottieMeshSwift
import ReactionListContextMenuContent
import AttachmentUI
#if DEBUG
import os.signpost
@ -2988,7 +2989,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
strongSelf.presentPollCreation(isQuiz: isQuiz)
if let controller = strongSelf.configurePollCreation(isQuiz: isQuiz) {
strongSelf.effectiveNavigationController?.pushViewController(controller)
}
})
}, displayPollSolution: { [weak self] solution, sourceNode in
self?.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: false)
@ -3174,7 +3177,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
} else {
strongSelf.presentMediaPicker(fileMode: false, editingMedia: true, completion: { signals, _, _ in
strongSelf.presentMediaPicker(fileMode: false, editingMedia: true, present: { [weak self] c in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { signals, _, _ in
self?.interfaceInteraction?.setupEditMessage(messageId, { _ in })
self?.editMessageMediaWithLegacySignals(signals)
})
@ -10235,7 +10240,288 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return inputPanelNode
}
private func openCamera() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
var photoOnly = false
if let callManager = self.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
photoOnly = true
}
let storeEditedPhotos = false
let initialCaption: String = ""
presentedLegacyCamera(context: self.context, peer: peer, chatLocation: self.chatLocation, cameraView: nil, menuController: nil, parentController: self, editingMedia: false, saveCapturedPhotos: storeEditedPhotos, mediaGrouping: true, initialCaption: initialCaption, hasSchedule: self.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
// if editMediaOptions != nil {
// strongSelf.editMessageMediaWithLegacySignals(signals!)
// } else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
// }
// if !inputText.string.isEmpty {
// strongSelf.clearInputText()
// }
}
}, recognizedQRCode: { [weak self] code in
if let strongSelf = self {
if let (host, port, username, password, secret) = parseProxyUrl(code) {
strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil)
}
}
}, presentSchedulePicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
})
}
}, presentTimerPicker: { [weak self] done in
if let strongSelf = self {
strongSelf.presentTimerPicker(style: .media, completion: { time in
done(time)
})
}
}, presentStickers: { [weak self] completion in
if let strongSelf = self {
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect)
return true
})
strongSelf.present(controller, in: .window(.root))
return controller
} else {
return nil
}
}, getCaptionPanelView: { [weak self] in
return self?.getCaptionPanelView()
})
}
private func presentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
self.chatDisplayNode.dismissInput()
let currentLocationController = Atomic<LocationPickerController?>(value: nil)
let attachmentController = AttachmentController(context: self.context)
attachmentController.requestController = { [weak self, weak attachmentController] type, completion in
guard let strongSelf = self else {
return
}
switch type {
case .camera:
completion(nil)
attachmentController?.dismiss(animated: true)
strongSelf.openCamera()
strongSelf.controllerNavigationDisposable.set(nil)
case .gallery:
strongSelf.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { c in
completion(c)
}, completion: { [weak self] signals, silentPosting, scheduleTime in
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
})
strongSelf.controllerNavigationDisposable.set(nil)
case .file:
let controller = attachmentFileController(context: strongSelf.context, presentGallery: { [weak self, weak attachmentController] in
attachmentController?.dismiss(animated: true)
self?.presentFileGallery()
}, presentFiles: { [weak self, weak attachmentController] in
attachmentController?.dismiss(animated: true)
self?.presentICloudFileGallery()
})
completion(controller)
strongSelf.controllerNavigationDisposable.set(nil)
case .location:
strongSelf.controllerNavigationDisposable.set(nil)
let existingController = currentLocationController.with { $0 }
if let controller = existingController {
completion(controller)
return
}
let selfPeerId: PeerId
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
selfPeerId = peer.id
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
selfPeerId = peer.id
} else {
selfPeerId = strongSelf.context.account.peerId
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(selfPeerId)
}
|> deliverOnMainQueue).start(next: { [weak self] selfPeer in
guard let strongSelf = self, let selfPeer = selfPeer else {
return
}
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in
guard let strongSelf = self else {
return
}
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
strongSelf.sendMessages([message])
})
completion(controller)
let _ = currentLocationController.swap(controller)
})
case .contact:
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true))
contactsController.navigationPresentation = .modal
completion(contactsController)
strongSelf.controllerNavigationDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self, let (peers, _) = peers {
if peers.count > 1 {
var enqueueMessages: [EnqueueMessage] = []
for peer in peers {
var media: TelegramMediaContact?
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
continue
}
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let phone = contactData.basicData.phoneNumbers[0].value
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil)
case let .deviceContact(_, basicData):
guard !basicData.phoneNumbers.isEmpty else {
continue
}
let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let phone = contactData.basicData.phoneNumbers[0].value
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil)
}
if let media = media {
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
enqueueMessages.append(message)
}
}
strongSelf.sendMessages(enqueueMessages)
} else if let peer = peers.first {
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
return
}
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let context = strongSelf.context
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
|> take(1)
|> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in
var stableId: String?
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
outer: for (id, data) in basicData {
for phoneNumber in data.phoneNumbers {
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
stableId = id
break outer
}
}
}
if let stableId = stableId {
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|> take(1)
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
return (contact, extendedData)
}
} else {
return .single((contact, contactData))
}
}
case let .deviceContact(id, _):
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|> take(1)
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
return (nil, extendedData)
}
}
strongSelf.controllerNavigationDisposable.set((dataSignal
|> deliverOnMainQueue).start(next: { peerAndContactData in
if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 {
if contactData.isPrimitive {
let phone = contactData.basicData.phoneNumbers[0].value
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil)
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.sendMessages([message])
} else {
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
return
}
let phone = contactData.basicData.phoneNumbers[0].value
if let vCardData = contactData.serializedVCard() {
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData)
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.sendMessages([message])
}
}), completed: nil, cancelled: nil)
strongSelf.effectiveNavigationController?.pushViewController(contactController)
}
}
}))
}
}
}))
case .poll:
let controller = strongSelf.configurePollCreation()
completion(controller)
strongSelf.controllerNavigationDisposable.set(nil)
case .app:
let controller = GameController(context: strongSelf.context, url: "https://wallet.ton.org", message: nil)
completion(controller)
strongSelf.controllerNavigationDisposable.set(nil)
}
}
self.present(attachmentController, in: .window(.root))
}
private func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) {
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
return entry ?? GeneratedMediaStoreSettings.defaultSettings
@ -10284,7 +10570,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if canSendPolls {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.AttachmentMenu_Poll, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
self?.presentPollCreation()
if let controller = self?.configurePollCreation() {
self?.effectiveNavigationController?.pushViewController(controller)
}
}))
}
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Contact, color: .accent, action: { [weak actionSheet] in
@ -10331,7 +10619,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: {
self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in
self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, present: { [weak self] c in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { signals, silentPosting, scheduleTime in
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
@ -10406,7 +10696,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, openContacts: {
self?.presentContactPicker()
}, openPoll: {
self?.presentPollCreation()
if let controller = self?.configurePollCreation() {
self?.effectiveNavigationController?.pushViewController(controller)
}
}, presentSelectionLimitExceeded: {
guard let strongSelf = self else {
return
@ -10512,102 +10804,112 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func presentFileGallery(editingMessage: Bool = false) {
self.presentMediaPicker(fileMode: true, editingMedia: editingMessage, present: { [weak self] c in
self?.effectiveNavigationController?.pushViewController(c)
}, completion: { [weak self] signals, silentPosting, scheduleTime in
if editingMessage {
self?.editMessageMediaWithLegacySignals(signals)
} else {
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
}
})
}
private func presentICloudFileGallery(editingMessage: Bool = false) {
self.present(legacyICloudFilePicker(theme: self.presentationData.theme, completion: { [weak self] urls in
if let strongSelf = self, !urls.isEmpty {
var signals: [Signal<ICloudFileDescription?, NoError>] = []
for url in urls {
signals.append(iCloudFileDescription(url))
}
strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals)
|> deliverOnMainQueue).start(next: { results in
if let strongSelf = self {
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
for item in results {
if let item = item, item.fileSize > 2000 * 1024 * 1024 {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Conversation_UploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
}
var groupingKey: Int64?
var fileTypes: (music: Bool, other: Bool) = (false, false)
if results.count > 1 {
for item in results {
if let item = item {
let pathExtension = (item.fileName as NSString).pathExtension.lowercased()
if ["mp3", "m4a"].contains(pathExtension) {
fileTypes.music = true
} else {
fileTypes.other = true
}
}
}
}
if fileTypes.music != fileTypes.other {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
}
var messages: [EnqueueMessage] = []
for item in results {
if let item = item {
let fileId = Int64.random(in: Int64.min ... Int64.max)
let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension)
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if mimeType.hasPrefix("image/") || mimeType == "application/pdf" {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil))
}
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(.FileName(fileName: item.fileName))
if let audioMetadata = item.audioMetadata {
attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil))
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: attributes)
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey, correlationId: nil)
messages.append(message)
}
if let _ = groupingKey, messages.count % 10 == 0 {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
}
}
if !messages.isEmpty {
if editingMessage {
strongSelf.editMessageMediaWithMessages(messages)
} else {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
strongSelf.sendMessages(messages)
}
}
}
}))
}
}), in: .window(.root))
}
private func presentFileMediaPickerOptions(editingMessage: Bool) {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.presentMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { signals, silentPosting, scheduleTime in
if editingMessage {
self?.editMessageMediaWithLegacySignals(signals)
} else {
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
}
})
strongSelf.presentFileGallery(editingMessage: editingMessage)
}
}),
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FileICloudDrive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.present(legacyICloudFilePicker(theme: strongSelf.presentationData.theme, completion: { urls in
if let strongSelf = self, !urls.isEmpty {
var signals: [Signal<ICloudFileDescription?, NoError>] = []
for url in urls {
signals.append(iCloudFileDescription(url))
}
strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals)
|> deliverOnMainQueue).start(next: { results in
if let strongSelf = self {
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
for item in results {
if let item = item, item.fileSize > 2000 * 1024 * 1024 {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Conversation_UploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
}
var groupingKey: Int64?
var fileTypes: (music: Bool, other: Bool) = (false, false)
if results.count > 1 {
for item in results {
if let item = item {
let pathExtension = (item.fileName as NSString).pathExtension.lowercased()
if ["mp3", "m4a"].contains(pathExtension) {
fileTypes.music = true
} else {
fileTypes.other = true
}
}
}
}
if fileTypes.music != fileTypes.other {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
}
var messages: [EnqueueMessage] = []
for item in results {
if let item = item {
let fileId = Int64.random(in: Int64.min ... Int64.max)
let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension)
var previewRepresentations: [TelegramMediaImageRepresentation] = []
if mimeType.hasPrefix("image/") || mimeType == "application/pdf" {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil))
}
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(.FileName(fileName: item.fileName))
if let audioMetadata = item.audioMetadata {
attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil))
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: attributes)
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey, correlationId: nil)
messages.append(message)
}
if let _ = groupingKey, messages.count % 10 == 0 {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
}
}
if !messages.isEmpty {
if editingMessage {
strongSelf.editMessageMediaWithMessages(messages)
} else {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
strongSelf.sendMessages(messages)
}
}
}
}))
}
}), in: .window(.root))
strongSelf.presentICloudFileGallery(editingMessage: editingMessage)
}
})
]), ActionSheetItemGroup(items: [
@ -10619,7 +10921,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(actionSheet, in: .window(.root))
}
private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) {
private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, present: @escaping (ViewController) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) {
let postbox = self.context.account.postbox
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, SearchBotsConfiguration), NoError> in
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
@ -10748,7 +11050,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
strongSelf.chatDisplayNode.dismissInput()
strongSelf.effectiveNavigationController?.pushViewController(legacyController)
present(legacyController)
}
})
})
@ -11266,41 +11568,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(tooltipScreen, in: .current)
}
private func presentPollCreation(isQuiz: Bool? = nil) {
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
self.effectiveNavigationController?.pushViewController(createPollController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), isQuiz: isQuiz, completion: { [weak self] poll in
guard let strongSelf = self else {
return
}
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
let message: EnqueueMessage = .message(
text: "",
attributes: [],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
publicity: poll.publicity,
kind: poll.kind,
text: poll.text,
options: poll.options,
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
)),
replyToMessageId: nil,
localGroupingKey: nil,
correlationId: nil
)
strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)])
}))
private func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return nil
}
return createPollController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), isQuiz: isQuiz, completion: { [weak self] poll in
guard let strongSelf = self else {
return
}
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)
let message: EnqueueMessage = .message(
text: "",
attributes: [],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
publicity: poll.publicity,
kind: poll.kind,
text: poll.text,
options: poll.options,
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
)),
replyToMessageId: nil,
localGroupingKey: nil,
correlationId: nil
)
strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)])
})
}
func transformEnqueueMessages(_ messages: [EnqueueMessage]) -> [EnqueueMessage] {

View file

@ -292,6 +292,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
}
} else if let type = webpage.type {
switch type {
case "photo":
if webpage.displayUrl.hasPrefix("t.me/") {
actionTitle = item.presentationData.strings.Conversation_ViewMessage
}
case "telegram_user":
actionTitle = item.presentationData.strings.Conversation_UserSendMessage
case "telegram_channel_request":

View file

@ -91,6 +91,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.title = self.titleProducer(self.presentationData.strings)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.scrollToTop = { [weak self] in
if let strongSelf = self {
@ -115,10 +116,12 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
}
})
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
if !params.multipleSelection {
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
}
if params.multipleSelection {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.beginSelection))

View file

@ -25,7 +25,7 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat, camera: previewView.camera, previewView: previewView, intent: photoOnly ? TGCameraControllerGenericPhotoOnlyIntent : TGCameraControllerGenericIntent)
controller.inhibitMultipleCapture = editingMedia
} else {
controller = TGCameraController()
controller = TGCameraController(context: legacyController.context, saveEditedPhotos: saveCapturedPhotos && !isSecretChat, saveCapturedMedia: saveCapturedPhotos && !isSecretChat)
}
controller.presentScheduleController = { done in