diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6e084c4d66..858c36322d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD new file mode 100644 index 0000000000..500fea157c --- /dev/null +++ b/submodules/AttachmentUI/BUILD @@ -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", + ], +) diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift new file mode 100644 index 0000000000..081151df44 --- /dev/null +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -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 + } +} diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift new file mode 100644 index 0000000000..cf0fc64b2d --- /dev/null +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -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) + } +} diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift new file mode 100644 index 0000000000..1671ce87d0 --- /dev/null +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -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, 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] = [:] + + 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() + + 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 + if let current = self.buttonViews[i] { + buttonView = current + } else { + buttonTransition = .immediate + buttonView = ComponentHostView() + 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) + } +} diff --git a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift index 881c14d844..d5f40e0280 100644 --- a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift +++ b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift @@ -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 { diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index a860ce7f95..a518cf5ee1 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -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 diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index a69a7bf576..a6e832f3c9 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -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: diff --git a/submodules/Display/Source/KeyboardManager.swift b/submodules/Display/Source/KeyboardManager.swift index 62cbd4b903..83fd35fb88 100644 --- a/submodules/Display/Source/KeyboardManager.swift +++ b/submodules/Display/Source/KeyboardManager.swift @@ -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 } diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index 6aa0a28880..92e8b3cf9f 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -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 } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 80de7e8eef..3af6ff47d0 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -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) } } diff --git a/submodules/GameUI/Sources/GameController.swift b/submodules/GameUI/Sources/GameController.swift index b3913f61de..2fa0e015ac 100644 --- a/submodules/GameUI/Sources/GameController.swift +++ b/submodules/GameUI/Sources/GameController.swift @@ -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" } } diff --git a/submodules/GameUI/Sources/GameControllerNode.swift b/submodules/GameUI/Sources/GameControllerNode.swift index 6b22a0c21b..551c9fcd88 100644 --- a/submodules/GameUI/Sources/GameControllerNode.swift +++ b/submodules/GameUI/Sources/GameControllerNode.swift @@ -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) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index 3606de013b..1bb571e473 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -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 { diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 194154f69a..3610c24fe3 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -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) diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index a46eaaf720..6ca2df7596 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -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) { diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m index 1d1d74287c..0e2ec297fc 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m @@ -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]]) diff --git a/submodules/LegacyComponents/Sources/TGNavigationBar.m b/submodules/LegacyComponents/Sources/TGNavigationBar.m index fb338ba368..7f765de697 100644 --- a/submodules/LegacyComponents/Sources/TGNavigationBar.m +++ b/submodules/LegacyComponents/Sources/TGNavigationBar.m @@ -326,7 +326,7 @@ static id _musicPlayerProvider; } [super setFrame:frame]; - + if (_statusBarBackgroundView != nil && _statusBarBackgroundView.superview != nil) { _statusBarBackgroundView.frame = CGRectMake(0, -self.frame.origin.y, self.frame.size.width, 20); diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 7d7570ee92..9b389c5d5c 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -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 diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 65c11c9f0d..57e5326505 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -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 { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 7263d527bf..66360a870a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -65,6 +65,8 @@ public enum PresentationResourceKey: Int32 { case itemListBlockDestructiveIcon case itemListAddDeviceIcon case itemListResetIcon + case itemListImageIcon + case itemListCloudIcon case itemListVoiceCallIcon case itemListVideoCallIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 9c342181d0..361f10141e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -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 diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index b18b86672a..fb22f22727 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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, diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Contents.json new file mode 100644 index 0000000000..806935b949 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Property 1=Camera.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Property 1=Camera.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Property 1=Camera.pdf new file mode 100644 index 0000000000..429bee41ab --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Camera.imageset/Property 1=Camera.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Cloud.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Cloud.imageset/Contents.json new file mode 100644 index 0000000000..8b6a066c0c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Cloud.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cloud_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Cloud.imageset/cloud_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Cloud.imageset/cloud_30.pdf new file mode 100644 index 0000000000..3fb0c04ca6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Cloud.imageset/cloud_30.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contact.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contact.imageset/Contents.json new file mode 100644 index 0000000000..a406bd6cae --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contact.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Union-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contact.imageset/Union-2.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contact.imageset/Union-2.pdf new file mode 100644 index 0000000000..707375b99e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contact.imageset/Union-2.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/File.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/File.imageset/Contents.json new file mode 100644 index 0000000000..3b0c6903e1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/File.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Subtract.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/File.imageset/Subtract.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/File.imageset/Subtract.pdf new file mode 100644 index 0000000000..ac014ededf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/File.imageset/Subtract.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Gallery.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Gallery.imageset/Contents.json new file mode 100644 index 0000000000..4171612590 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Gallery.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Union.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Gallery.imageset/Union.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Gallery.imageset/Union.pdf new file mode 100644 index 0000000000..f4bcf9be8b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Gallery.imageset/Union.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Image.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Image.imageset/Contents.json new file mode 100644 index 0000000000..e11ef0c09b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Image.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Image.imageset/image_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Image.imageset/image_30.pdf new file mode 100644 index 0000000000..5674e8680a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Image.imageset/image_30.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json new file mode 100644 index 0000000000..730276db9c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Subtract-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Subtract-2.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Subtract-2.pdf new file mode 100644 index 0000000000..a72085cb96 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Subtract-2.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Contents.json new file mode 100644 index 0000000000..1fab2f6057 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Union.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Union.png b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Union.png new file mode 100644 index 0000000000..728dd1f6ac Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Poll.imageset/Union.png differ diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift new file mode 100644 index 0000000000..a11e45e455 --- /dev/null +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -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 +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5d36a6bc7e..4d9c24ed39 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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(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: "_$!!$_", 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: "_$!!$_", 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] = [] + 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] = [] - 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] { diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 761f4ab5e1..eafa243f3e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -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": diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 42332314f1..1a5dc443c6 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -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)) diff --git a/submodules/TelegramUI/Sources/LegacyCamera.swift b/submodules/TelegramUI/Sources/LegacyCamera.swift index ede0ddfc9f..5bce5a0ba2 100644 --- a/submodules/TelegramUI/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Sources/LegacyCamera.swift @@ -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