diff --git a/Telegram-iOS.xcworkspace/contents.xcworkspacedata b/Telegram-iOS.xcworkspace/contents.xcworkspacedata index b294c9cb24..176a24b7fc 100644 --- a/Telegram-iOS.xcworkspace/contents.xcworkspacedata +++ b/Telegram-iOS.xcworkspace/contents.xcworkspacedata @@ -551,6 +551,9 @@ + + { get } + var updateTransitionWhenPresentedAsModal: ((CGFloat, ContainedViewLayoutTransition) -> Void)? { get set } func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations var deferScreenEdgeGestures: UIRectEdge { get } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) + func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) func viewWillAppear(_ animated: Bool) func viewWillDisappear(_ animated: Bool) diff --git a/submodules/Display/Display/NavigationController.swift b/submodules/Display/Display/NavigationController.swift index e5245c5089..92ec8f82e5 100644 --- a/submodules/Display/Display/NavigationController.swift +++ b/submodules/Display/Display/NavigationController.swift @@ -124,6 +124,8 @@ public enum MasterDetailLayoutBlackout : Equatable { open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate { public var isOpaqueWhenInOverlay: Bool = true public var blocksBackgroundWhenInOverlay: Bool = true + public var isModalWhenInOverlay: Bool = false + public var updateTransitionWhenPresentedAsModal: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private let _ready = Promise(true) open var ready: Promise { @@ -551,7 +553,7 @@ open class NavigationController: UINavigationController, ContainableController, self.controllerView.containerView.addSubview(record.controller.view) record.controller.setIgnoreAppearanceMethodInvocations(false) - if let _ = previousControllers.index(where: { $0.controller === record.controller }) { + if let _ = previousControllers.firstIndex(where: { $0.controller === record.controller }) { //previousControllers[index].transition = .appearance let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: previousController.view, topNavigationBar: (previousController as? ViewController)?.navigationBar, bottomView: record.controller.view, bottomNavigationBar: (record.controller as? ViewController)?.navigationBar) self.navigationTransitionCoordinator = navigationTransitionCoordinator @@ -571,7 +573,7 @@ open class NavigationController: UINavigationController, ContainableController, } }) } else { - if let index = self._viewControllers.index(where: { $0.controller === previousController }) { + if let index = self._viewControllers.firstIndex(where: { $0.controller === previousController }) { self._viewControllers[index].transition = .appearance } let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Push, container: self.controllerView.containerView, topView: record.controller.view, topNavigationBar: (record.controller as? ViewController)?.navigationBar, bottomView: previousController.view, bottomNavigationBar: (previousController as? ViewController)?.navigationBar) @@ -580,7 +582,7 @@ open class NavigationController: UINavigationController, ContainableController, self.controllerView.inTransition = true navigationTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in if let strongSelf = self { - if let index = strongSelf._viewControllers.index(where: { $0.controller === previousController }) { + if let index = strongSelf._viewControllers.firstIndex(where: { $0.controller === previousController }) { strongSelf._viewControllers[index].transition = .none } strongSelf.navigationTransitionCoordinator = nil @@ -713,7 +715,7 @@ open class NavigationController: UINavigationController, ContainableController, self.loadView() } self.validLayout = layout - transition.updateFrame(view: self.view, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) + //transition.updateFrame(view: self.view, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) self.updateControllerLayouts(previousControllers: self._viewControllers, layout: layout, transition: transition) @@ -732,6 +734,33 @@ open class NavigationController: UINavigationController, ContainableController, } } + private var modalTransition: CGFloat = 0.0 + + public func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + if self.modalTransition == value { + return + } + let scale = (self.view.bounds.width - 20.0 * 2.0) / self.view.bounds.width + let cornerRadius = value * 10.0 / scale + switch transition { + case let .animated(duration, curve): + let previous = self.displayNode.layer.cornerRadius + self.displayNode.layer.cornerRadius = cornerRadius + if !cornerRadius.isZero { + self.displayNode.clipsToBounds = true + } + self.displayNode.layer.animate(from: previous as NSNumber, to: cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, completion: { [weak self] _ in + if cornerRadius.isZero { + self?.displayNode.clipsToBounds = false + } + }) + case .immediate: + self.displayNode.layer.cornerRadius = cornerRadius + self.displayNode.clipsToBounds = !cornerRadius.isZero + } + self.modalTransition = value + } + public func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) { for record in self._viewControllers { if let controller = record.controller as? ContainableController { diff --git a/submodules/Display/Display/PresentationContext.swift b/submodules/Display/Display/PresentationContext.swift index 6421425520..3f81254d34 100644 --- a/submodules/Display/Display/PresentationContext.swift +++ b/submodules/Display/Display/PresentationContext.swift @@ -34,9 +34,8 @@ public final class PresentationContext { } } - weak var volumeControlStatusBarNodeView: UIView? - var updateIsInteractionBlocked: ((Bool) -> Void)? + var updateHasBlocked: ((Bool) -> Void)? var updateHasOpaqueOverlay: ((Bool) -> Void)? private(set) var hasOpaqueOverlay: Bool = false { @@ -47,6 +46,9 @@ public final class PresentationContext { } } + private var modalPresentationValue: CGFloat = 0.0 + var updateModalTransition: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + private var layout: ContainerViewLayout? private var ready: Bool { @@ -120,6 +122,18 @@ public final class PresentationContext { } } + private func layoutForController(containerLayout: ContainerViewLayout, controller: ContainableController) -> (ContainerViewLayout, CGRect) { + if controller.isModalWhenInOverlay { + let topInset = (containerLayout.statusBarHeight ?? 0.0) + 20.0 + var updatedLayout = containerLayout + updatedLayout.statusBarHeight = nil + updatedLayout.size.height -= topInset + return (updatedLayout, CGRect(origin: CGPoint(x: 0.0, y: topInset), size: updatedLayout.size)) + } else { + return (containerLayout, CGRect(origin: CGPoint(), size: containerLayout.size)) + } + } + public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void) { let controllerReady = controller.ready.get() |> filter({ $0 }) @@ -140,8 +154,9 @@ public final class PresentationContext { controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: orientations, compactSize: orientations) } } - controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size) - controller.containerLayoutUpdated(initialLayout, transition: .immediate) + let (controllerLayout, controllerFrame) = self.layoutForController(containerLayout: initialLayout, controller: controller) + controller.view.frame = controllerFrame + controller.containerLayoutUpdated(controllerLayout, transition: .immediate) var blockInteractionToken: Int? if blockInteraction { blockInteractionToken = self.addBlockInteraction() @@ -170,37 +185,32 @@ public final class PresentationContext { } strongSelf.controllers.insert((controller, level), at: insertIndex ?? strongSelf.controllers.count) if let view = strongSelf.view, let layout = strongSelf.layout { + let (updatedControllerLayout, updatedControllerFrame) = strongSelf.layoutForController(containerLayout: layout, controller: controller) + (controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in if let strongSelf = self, let controller = controller { strongSelf.dismiss(controller) } }, rootController: nil) (controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(true) - if layout != initialLayout { - controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) + if updatedControllerLayout != controllerLayout { + controller.view.frame = updatedControllerFrame if let topLevelSubview = strongSelf.topLevelSubview(for: level) { view.insertSubview(controller.view, belowSubview: topLevelSubview) } else { - if let volumeControlStatusBarNodeView = strongSelf.volumeControlStatusBarNodeView { - view.insertSubview(controller.view, belowSubview: volumeControlStatusBarNodeView) - } else { - view.addSubview(controller.view) - } + view.addSubview(controller.view) } - controller.containerLayoutUpdated(layout, transition: .immediate) + controller.containerLayoutUpdated(updatedControllerLayout, transition: .immediate) } else { if let topLevelSubview = strongSelf.topLevelSubview(for: level) { view.insertSubview(controller.view, belowSubview: topLevelSubview) } else { - if let volumeControlStatusBarNodeView = strongSelf.volumeControlStatusBarNodeView { - view.insertSubview(controller.view, belowSubview: volumeControlStatusBarNodeView) - } else { - view.addSubview(controller.view) - } + view.addSubview(controller.view) } } (controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(false) view.layer.invalidateUpTheTree() + strongSelf.updateViews() controller.viewWillAppear(false) if let controller = controller as? PresentableController { controller.viewDidAppear(completion: { [weak self] in @@ -211,7 +221,6 @@ public final class PresentationContext { strongSelf.notifyAccessibilityScreenChanged() } } - strongSelf.updateViews() } })) } else { @@ -225,7 +234,7 @@ public final class PresentationContext { } private func dismiss(_ controller: ContainableController) { - if let index = self.controllers.index(where: { $0.0 === controller }) { + if let index = self.controllers.firstIndex(where: { $0.0 === controller }) { self.controllers.remove(at: index) controller.viewWillDisappear(false) controller.view.removeFromSuperview() @@ -242,7 +251,9 @@ public final class PresentationContext { self.readyChanged(wasReady: wasReady) } else if self.ready { for (controller, _) in self.controllers { - controller.containerLayoutUpdated(layout, transition: transition) + let (controllerLayout, controllerFrame) = self.layoutForController(containerLayout: layout, controller: controller) + controller.view.frame = controllerFrame + controller.containerLayoutUpdated(controllerLayout, transition: transition) } } } @@ -262,14 +273,11 @@ public final class PresentationContext { if let topLevelSubview = self.topLevelSubview { view.insertSubview(controller.view, belowSubview: topLevelSubview) } else { - if let volumeControlStatusBarNodeView = self.volumeControlStatusBarNodeView { - view.insertSubview(controller.view, belowSubview: volumeControlStatusBarNodeView) - } else { - view.addSubview(controller.view) - } + view.addSubview(controller.view) } - controller.view.frame = CGRect(origin: CGPoint(), size: layout.size) - controller.containerLayoutUpdated(layout, transition: .immediate) + let (controllerLayout, controllerFrame) = self.layoutForController(containerLayout: layout, controller: controller) + controller.view.frame = controllerFrame + controller.containerLayoutUpdated(controllerLayout, transition: .immediate) if let controller = controller as? PresentableController { controller.viewDidAppear(completion: { [weak self] in self?.notifyAccessibilityScreenChanged() @@ -291,10 +299,18 @@ public final class PresentationContext { } } + private weak var currentModalController: ContainableController? + private func updateViews() { self.hasOpaqueOverlay = self.currentlyBlocksBackgroundWhenInOverlay + var modalController: ContainableController? var topHasOpaque = false for (controller, _) in self.controllers.reversed() { + if controller.isModalWhenInOverlay { + if modalController == nil { + modalController = controller + } + } if topHasOpaque { controller.displayNode.accessibilityElementsHidden = true } else { @@ -304,16 +320,47 @@ public final class PresentationContext { controller.displayNode.accessibilityElementsHidden = false } } + + if self.currentModalController !== modalController { + if let currentModalController = self.currentModalController { + currentModalController.updateTransitionWhenPresentedAsModal = nil + if #available(iOSApplicationExtension 11.0, *) { + currentModalController.displayNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + currentModalController.displayNode.layer.cornerRadius = 0.0 + } + self.currentModalController = modalController + if let modalController = modalController { + if #available(iOSApplicationExtension 11.0, *) { + modalController.displayNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + modalController.displayNode.layer.cornerRadius = 10.0 + modalController.updateTransitionWhenPresentedAsModal = { [weak self, weak modalController] value, transition in + guard let strongSelf = self, let modalController = modalController, modalController === strongSelf.currentModalController else { + return + } + if strongSelf.modalPresentationValue != value { + strongSelf.modalPresentationValue = value + strongSelf.updateModalTransition?(value, transition) + } + } + } else { + if self.modalPresentationValue != 0.0 { + self.modalPresentationValue = 0.0 + self.updateModalTransition?(0.0, .animated(duration: 0.3, curve: .spring)) + } + } + } } private func notifyAccessibilityScreenChanged() { UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil) } - func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + func hitTest(view: UIView, point: CGPoint, with event: UIEvent?) -> UIView? { for (controller, _) in self.controllers.reversed() { if controller.isViewLoaded { - if let result = controller.view.hitTest(point, with: event) { + if let result = controller.view.hitTest(view.convert(point, to: controller.view), with: event) { return result } } diff --git a/submodules/Display/Display/StatusBarHost.swift b/submodules/Display/Display/StatusBarHost.swift index 35a7299d4e..a6653246e3 100644 --- a/submodules/Display/Display/StatusBarHost.swift +++ b/submodules/Display/Display/StatusBarHost.swift @@ -3,7 +3,7 @@ import SwiftSignalKit public protocol StatusBarHost { var statusBarFrame: CGRect { get } - var statusBarStyle: UIStatusBarStyle { get set } + var statusBarStyle: UIStatusBarStyle { get } var statusBarWindow: UIView? { get } var statusBarView: UIView? { get } @@ -11,4 +11,6 @@ public protocol StatusBarHost { var keyboardView: UIView? { get } var handleVolumeControl: Signal { get } + + func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) } diff --git a/submodules/Display/Display/StatusBarManager.swift b/submodules/Display/Display/StatusBarManager.swift index b051951bfd..1bcb1efe08 100644 --- a/submodules/Display/Display/StatusBarManager.swift +++ b/submodules/Display/Display/StatusBarManager.swift @@ -76,7 +76,7 @@ class StatusBarManager { private let volumeControlStatusBarNode: VolumeControlStatusBarNode private var surfaces: [StatusBarSurface] = [] - private var validParams: (withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool)? + private var validParams: (withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, UIStatusBarStyle?)? var inCallNavigate: (() -> Void)? @@ -111,8 +111,8 @@ class StatusBarManager { self?.volumeControlStatusBarNode.allowsGroupOpacity = false }) } - if let (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows) = self.validParams { - self.updateSurfaces(self.surfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: false, alphaTransition: .animated(duration: 0.2, curve: .easeInOut)) + if let (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows, forceAppearance) = self.validParams { + self.updateSurfaces(self.surfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: false, forceAppearance: forceAppearance, alphaTransition: .animated(duration: 0.2, curve: .easeInOut)) } } @@ -126,24 +126,24 @@ class StatusBarManager { } }) self.volumeTimer = nil - if let (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows) = self.validParams { - self.updateSurfaces(self.surfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: false, alphaTransition: .animated(duration: 0.2, curve: .easeInOut)) + if let (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows, forceAppearance) = self.validParams { + self.updateSurfaces(self.surfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: false, forceAppearance: forceAppearance, alphaTransition: .animated(duration: 0.2, curve: .easeInOut)) } } - func updateState(surfaces: [StatusBarSurface], withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool) { + func updateState(surfaces: [StatusBarSurface], withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, forceAppearance: UIStatusBarStyle?, animated: Bool) { let previousSurfaces = self.surfaces self.surfaces = surfaces - self.updateSurfaces(previousSurfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: animated, alphaTransition: .immediate) + self.updateSurfaces(previousSurfaces, withSafeInsets: withSafeInsets, forceInCallStatusBarText: forceInCallStatusBarText, forceHiddenBySystemWindows: forceHiddenBySystemWindows, animated: animated, forceAppearance: forceAppearance, alphaTransition: .immediate) } - private func updateSurfaces(_ previousSurfaces: [StatusBarSurface], withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool, alphaTransition: ContainedViewLayoutTransition) { + private func updateSurfaces(_ previousSurfaces: [StatusBarSurface], withSafeInsets: Bool, forceInCallStatusBarText: String?, forceHiddenBySystemWindows: Bool, animated: Bool, forceAppearance: UIStatusBarStyle?, alphaTransition: ContainedViewLayoutTransition) { let statusBarFrame = self.host.statusBarFrame guard let statusBarView = self.host.statusBarView else { return } - self.validParams = (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows) + self.validParams = (withSafeInsets, forceInCallStatusBarText, forceHiddenBySystemWindows, forceAppearance) if self.host.statusBarWindow?.isUserInteractionEnabled != (forceInCallStatusBarText == nil) { self.host.statusBarWindow?.isUserInteractionEnabled = (forceInCallStatusBarText == nil) @@ -278,6 +278,17 @@ class StatusBarManager { } self.volumeControlStatusBarNode.isDark = isDark + if let forceAppearance = forceAppearance { + let style: StatusBarStyle + switch forceAppearance { + case .lightContent: + style = .White + default: + style = .Black + } + globalStatusBar = (style, 1.0, 0.0) + } + if let globalStatusBar = globalStatusBar, !forceHiddenBySystemWindows { let statusBarStyle: UIStatusBarStyle if forceInCallStatusBarText != nil { @@ -286,7 +297,7 @@ class StatusBarManager { statusBarStyle = globalStatusBar.0 == .Black ? .default : .lightContent } if self.host.statusBarStyle != statusBarStyle { - self.host.statusBarStyle = statusBarStyle + self.host.setStatusBarStyle(statusBarStyle, animated: animated) } if let statusBarWindow = self.host.statusBarWindow { alphaTransition.updateAlpha(layer: statusBarView.layer, alpha: globalStatusBar.1) diff --git a/submodules/Display/Display/ViewController.swift b/submodules/Display/Display/ViewController.swift index 3d7dc74efb..f2a54e65a8 100644 --- a/submodules/Display/Display/ViewController.swift +++ b/submodules/Display/Display/ViewController.swift @@ -88,6 +88,14 @@ open class ViewControllerPresentationArguments { public final var isOpaqueWhenInOverlay: Bool = false public final var blocksBackgroundWhenInOverlay: Bool = false public final var automaticallyControlPresentationContextLayout: Bool = true + public final var isModalWhenInOverlay: Bool = false { + didSet { + if self.isNodeLoaded { + self.displayNode.clipsToBounds = true + } + } + } + public var updateTransitionWhenPresentedAsModal: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { return self.supportedOrientations @@ -190,7 +198,7 @@ open class ViewControllerPresentationArguments { open var visualNavigationInsetHeight: CGFloat { if let navigationBar = self.navigationBar { - var height = navigationBar.frame.maxY + let height = navigationBar.frame.maxY if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode { //height += contentNode.height } @@ -233,7 +241,7 @@ open class ViewControllerPresentationArguments { private func updateScrollToTopView() { if self.scrollToTop != nil { if let displayNode = self._displayNode , self.scrollToTopView == nil { - let scrollToTopView = ScrollToTopView(frame: CGRect(x: 0.0, y: -1.0, width: displayNode.frame.size.width, height: 1.0)) + let scrollToTopView = ScrollToTopView(frame: CGRect(x: 0.0, y: -1.0, width: displayNode.bounds.size.width, height: 1.0)) scrollToTopView.action = { [weak self] in if let scrollToTop = self?.scrollToTop { scrollToTop() @@ -293,16 +301,16 @@ open class ViewControllerPresentationArguments { private func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0 - let navigationBarHeight: CGFloat = max(20.0, statusBarHeight) + (self.navigationBar?.contentHeight ?? 44.0) + let navigationBarHeight: CGFloat = statusBarHeight + (self.navigationBar?.contentHeight ?? 44.0) let navigationBarOffset: CGFloat if statusBarHeight.isZero { - navigationBarOffset = -20.0 + navigationBarOffset = 0.0 } else { navigationBarOffset = 0.0 } var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: navigationBarHeight)) if layout.statusBarHeight == nil { - navigationBarFrame.size.height = (self.navigationBar?.contentHeight ?? 44.0) + 20.0 + //navigationBarFrame.size.height = (self.navigationBar?.contentHeight ?? 44.0) + 20.0 } if !self.displayNavigationBar { @@ -332,7 +340,7 @@ open class ViewControllerPresentationArguments { if !self.isViewLoaded { self.loadView() } - transition.updateFrame(node: self.displayNode, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) + //transition.updateFrame(node: self.displayNode, frame: CGRect(origin: self.view.frame.origin, size: layout.size)) if let _ = layout.statusBarHeight { self.statusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0)) } @@ -348,6 +356,10 @@ open class ViewControllerPresentationArguments { } } + open func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + + } + open func navigationStackConfigurationUpdated(next: [ViewController]) { } @@ -377,6 +389,10 @@ open class ViewControllerPresentationArguments { self.blocksBackgroundWhenInOverlay = true self.isOpaqueWhenInOverlay = true } + + if self.isModalWhenInOverlay { + self.displayNode.clipsToBounds = true + } } public func requestLayout(transition: ContainedViewLayoutTransition) { @@ -580,7 +596,7 @@ private func traceViewVisibility(view: UIView, rect: CGRect) -> Bool { if view.window == nil { return false } - if let index = siblings.index(where: { $0 === view.layer }) { + if let index = siblings.firstIndex(where: { $0 === view.layer }) { let viewFrame = view.convert(rect, to: superview) for i in (index + 1) ..< siblings.count { if siblings[i].frame.contains(viewFrame) { diff --git a/submodules/Display/Display/WindowContent.swift b/submodules/Display/Display/WindowContent.swift index 181fd6f4ca..cf0342181e 100644 --- a/submodules/Display/Display/WindowContent.swift +++ b/submodules/Display/Display/WindowContent.swift @@ -344,10 +344,6 @@ public class Window1 { private var isInteractionBlocked = false - /*private var accessibilityElements: [Any]? { - return self.viewController?.view.accessibilityElements - }*/ - public init(hostView: WindowHostView, statusBarHost: StatusBarHost?) { self.hostView = hostView @@ -377,6 +373,8 @@ public class Window1 { self.presentationContext = PresentationContext() self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.aboveStatusBarView) + self.presentationContextContainerView = UIView() + self.presentationContext.updateIsInteractionBlocked = { [weak self] value in self?.isInteractionBlocked = value } @@ -385,6 +383,10 @@ public class Window1 { self?._rootController?.displayNode.accessibilityElementsHidden = value } + self.presentationContext.updateModalTransition = { [weak self] value, transition in + self?.updateModalTransition(value, transition: transition) + } + self.hostView.present = { [weak self] controller, level, blockInteraction, completion in self?.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) } @@ -440,12 +442,7 @@ public class Window1 { }) } - /*self.hostView.getAccessibilityElements = { [weak self] in - return self?.accessibilityElements - }*/ - - self.presentationContext.view = self.hostView.containerView - self.presentationContext.volumeControlStatusBarNodeView = self.volumeControlStatusBarNode.view + self.presentationContext.view = self.presentationContextContainerView self.presentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate) self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate) @@ -567,6 +564,8 @@ public class Window1 { self.windowPanRecognizer = recognizer self.hostView.containerView.addGestureRecognizer(recognizer) + self.hostView.containerView.addSubview(self.presentationContextContainerView) + self.hostView.containerView.addSubview(self.volumeControlStatusBar) self.hostView.containerView.addSubview(self.volumeControlStatusBarNode.view) } @@ -668,7 +667,7 @@ public class Window1 { } } - if let result = self.presentationContext.hitTest(point, with: event) { + if let result = self.presentationContext.hitTest(view: self.hostView.containerView, point: point, with: event) { return result } return self.viewController?.view.hitTest(point, with: event) @@ -697,7 +696,19 @@ public class Window1 { if let rootController = self._rootController { if !self.windowLayout.size.width.isZero && !self.windowLayout.size.height.isZero { - rootController.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation), transition: .immediate) + let rootLayout = containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation) + var positionOffset: CGFloat = 0.0 + if !self.appliedModalLevel.isZero { + let scale = ((rootLayout.size.width - 20.0 * 2.0) / rootLayout.size.width) + rootController.displayNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + let transformedUpperBound = (self.windowLayout.size.height - rootLayout.size.height * scale) / 2.0 + let targetBound = (self.windowLayout.statusBarHeight ?? 0.0) + 20.0 - 10.0 + positionOffset = targetBound - transformedUpperBound + } + rootController.displayNode.position = CGPoint(x: self.windowLayout.size.width / 2.0, y: self.windowLayout.size.height / 2.0 + positionOffset) + rootController.displayNode.bounds = CGRect(origin: CGPoint(), size: rootLayout.size) + rootController.containerLayoutUpdated(rootLayout, transition: .immediate) + rootController.updateModalTransition(self.appliedModalLevel, transition: .immediate) } self.hostView.containerView.insertSubview(rootController.view, at: 0) @@ -707,6 +718,26 @@ public class Window1 { } } + private func insertContentViewAtTop(_ view: UIView) { + if let dimView = self.dimView { + self.hostView.containerView.insertSubview(view, belowSubview: dimView) + } else { + self.hostView.containerView.insertSubview(view, belowSubview: self.presentationContextContainerView) + } + } + + private func insertCoveringView(_ view: UIView) { + self.hostView.containerView.insertSubview(view, belowSubview: self.volumeControlStatusBarNode.view) + } + + private func insertDimView(_ view: UIView) { + if let coveringView = self.coveringView { + self.hostView.containerView.insertSubview(view, belowSubview: coveringView) + } else { + self.hostView.containerView.insertSubview(view, belowSubview: self.presentationContextContainerView) + } + } + private var _topLevelOverlayControllers: [ContainableController] = [] public var topLevelOverlayControllers: [ContainableController] { get { @@ -721,18 +752,17 @@ public class Window1 { let layout = containedLayoutForWindowLayout(self.windowLayout, hasOnScreenNavigation: self.hostView.hasOnScreenNavigation) for controller in self._topLevelOverlayControllers { controller.containerLayoutUpdated(layout, transition: .immediate) - - if let coveringView = self.coveringView { - self.hostView.containerView.insertSubview(controller.view, belowSubview: coveringView) - } else { - self.hostView.containerView.insertSubview(controller.view, belowSubview: self.volumeControlStatusBarNode.view) - } + self.insertContentViewAtTop(controller.view) } self.presentationContext.topLevelSubview = self._topLevelOverlayControllers.first?.view } } + private var dimView: UIView? = nil + + private let presentationContextContainerView: UIView + public var coveringView: WindowCoveringView? { didSet { if self.coveringView !== oldValue { @@ -746,7 +776,7 @@ public class Window1 { coveringView.layer.removeAnimation(forKey: "opacity") coveringView.layer.allowsGroupOpacity = false coveringView.alpha = 1.0 - self.hostView.containerView.insertSubview(coveringView, belowSubview: self.volumeControlStatusBarNode.view) + self.insertCoveringView(coveringView) if !self.windowLayout.size.width.isZero { coveringView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) coveringView.updateLayout(self.windowLayout.size) @@ -756,6 +786,8 @@ public class Window1 { } } + private var appliedModalLevel: CGFloat = 0.0 + private func layoutSubviews() { var hasPreview = false var updatedHasPreview = false @@ -771,11 +803,19 @@ public class Window1 { updatedHasPreview = true } - if self.tracingStatusBarsInvalidated || updatedHasPreview, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager { + var modalLevelUpdated = false + if self.appliedModalLevel != self.modalTransition { + modalLevelUpdated = self.appliedModalLevel.isZero != self.modalTransition.isZero + self.appliedModalLevel = self.modalTransition + } + + if self.tracingStatusBarsInvalidated || updatedHasPreview || modalLevelUpdated, let statusBarManager = statusBarManager, let keyboardManager = keyboardManager { self.tracingStatusBarsInvalidated = false if self.statusBarHidden { - statusBarManager.updateState(surfaces: [], withSafeInsets: false, forceInCallStatusBarText: nil, forceHiddenBySystemWindows: false, animated: false) + statusBarManager.updateState(surfaces: [], withSafeInsets: false, forceInCallStatusBarText: nil, forceHiddenBySystemWindows: false, forceAppearance: nil, animated: false) + } else if !self.modalTransition.isZero || self.isInTransitionOutOfModal { + statusBarManager.updateState(surfaces: [], withSafeInsets: false, forceInCallStatusBarText: nil, forceHiddenBySystemWindows: false, forceAppearance: self.isInTransitionOutOfModal ? .default : .lightContent, animated: modalLevelUpdated) } else { var statusBarSurfaces: [StatusBarSurface] = [] for layers in self.hostView.containerView.layer.traceableLayerSurfaces(withTag: WindowTracingTags.statusBar) { @@ -796,7 +836,7 @@ public class Window1 { } } self.cachedWindowSubviewCount = self.hostView.containerView.window?.subviews.count ?? 0 - statusBarManager.updateState(surfaces: statusBarSurfaces, withSafeInsets: !self.windowLayout.safeInsets.top.isZero, forceInCallStatusBarText: self.forceInCallStatusBarText, forceHiddenBySystemWindows: hasPreview, animated: animatedUpdate) + statusBarManager.updateState(surfaces: statusBarSurfaces, withSafeInsets: !self.windowLayout.safeInsets.top.isZero, forceInCallStatusBarText: self.forceInCallStatusBarText, forceHiddenBySystemWindows: hasPreview, forceAppearance: nil, animated: animatedUpdate || modalLevelUpdated) } var keyboardSurfaces: [KeyboardSurface] = [] @@ -969,13 +1009,31 @@ public class Window1 { let childLayoutUpdated = self.updatedContainerLayout != childLayout self.updatedContainerLayout = childLayout + updatingLayout.transition.updateFrame(view: self.presentationContextContainerView, frame: CGRect(origin: CGPoint(), size: self.windowLayout.size)) + if let dimView = self.dimView { + updatingLayout.transition.updateFrame(view: dimView, frame: CGRect(origin: CGPoint(), size: self.windowLayout.size)) + } + if childLayoutUpdated { var rootLayout = childLayout let rootTransition = updatingLayout.transition if self.presentationContext.isCurrentlyOpaque { rootLayout.inputHeight = nil } - self._rootController?.containerLayoutUpdated(rootLayout, transition: rootTransition) + var positionOffset: CGFloat = 0.0 + if let rootController = self._rootController { + if !self.modalTransition.isZero { + let scale = (rootLayout.size.width - 20.0 * 2.0) / rootLayout.size.width + rootTransition.updateTransformScale(node: rootController.displayNode, scale: scale) + + let transformedUpperBound = (self.windowLayout.size.height - rootLayout.size.height * scale) / 2.0 + let targetBound = (self.windowLayout.statusBarHeight ?? 0.0) + 20.0 - 10.0 + positionOffset = targetBound - transformedUpperBound + } + rootTransition.updatePosition(node: rootController.displayNode, position: CGPoint(x: self.windowLayout.size.width / 2.0, y: self.windowLayout.size.height / 2.0 + positionOffset)) + rootTransition.updateBounds(node: rootController.displayNode, bounds: CGRect(origin: CGPoint(), size: rootLayout.size)) + rootController.containerLayoutUpdated(rootLayout, transition: rootTransition) + } self.presentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) self.overlayPresentationContext.containerLayoutUpdated(childLayout, transition: updatingLayout.transition) @@ -1008,6 +1066,62 @@ public class Window1 { } } + private var modalTransition: CGFloat = 0.0 + private var defaultBackgroundColor: UIColor? + private var isInTransitionOutOfModal: Bool = false + + private func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + self.modalTransition = value + if !value.isZero { + if self.dimView == nil { + let dimView = UIView() + dimView.frame = CGRect(origin: CGPoint(), size: self.windowLayout.size) + dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.dimView = dimView + self.insertDimView(dimView) + dimView.alpha = 0.0 + transition.updateAlpha(layer: dimView.layer, alpha: 1.0) + + self.defaultBackgroundColor = self.hostView.containerView.backgroundColor + self.hostView.containerView.backgroundColor = .black + + var positionOffset: CGFloat = 0.0 + if let rootController = self._rootController { + let rootSize = rootController.displayNode.bounds.size + let scale = (rootSize.width - 20.0 * 2.0) / rootSize.width + transition.updateTransformScale(node: rootController.displayNode, scale: scale) + + let transformedUpperBound = (self.windowLayout.size.height - rootSize.height * scale) / 2.0 + let targetBound = (self.windowLayout.statusBarHeight ?? 0.0) + 20.0 - 10.0 + positionOffset = targetBound - transformedUpperBound + + transition.updatePosition(node: rootController.displayNode, position: CGPoint(x: self.windowLayout.size.width / 2.0, y: self.windowLayout.size.height / 2.0 + positionOffset)) + rootController.updateModalTransition(value, transition: transition) + } + self.layoutSubviews() + } + } else { + if let dimView = self.dimView { + self.dimView = nil + self.isInTransitionOutOfModal = true + transition.updateAlpha(layer: dimView.layer, alpha: 0.0, completion: { [weak self, weak dimView] _ in + dimView?.removeFromSuperview() + if let strongSelf = self { + strongSelf.hostView.containerView.backgroundColor = strongSelf.defaultBackgroundColor + strongSelf.isInTransitionOutOfModal = false + strongSelf.layoutSubviews() + } + }) + if let rootController = self._rootController { + transition.updateTransformScale(node: rootController.displayNode, scale: 1.0) + transition.updatePosition(node: rootController.displayNode, position: CGPoint(x: self.windowLayout.size.width / 2.0, y: self.windowLayout.size.height / 2.0)) + rootController.updateModalTransition(0.0, transition: transition) + } + self.layoutSubviews() + } + } + } + public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void = {}) { self.presentationContext.present(controller, on: level, blockInteraction: blockInteraction, completion: completion) } diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 81344e6c05..25ebf981ea 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -230,6 +230,7 @@ open class ItemListController: ViewController, KeyShor super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: theme), strings: NavigationBarStrings(presentationStrings: strings))) self.isOpaqueWhenInOverlay = true + self.isModalWhenInOverlay = true self.blocksBackgroundWhenInOverlay = true self.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style @@ -478,6 +479,7 @@ open class ItemListController: ViewController, KeyShor presentationArguments.completion?() completion() }) + self.updateTransitionWhenPresentedAsModal?(1.0, .animated(duration: 0.5, curve: .spring)) } else { completion() } @@ -506,6 +508,7 @@ open class ItemListController: ViewController, KeyShor if !self.isDismissed { self.isDismissed = true (self.displayNode as! ItemListControllerNode).animateOut(completion: completion) + self.updateTransitionWhenPresentedAsModal?(0.0, .animated(duration: 0.2, curve: .easeInOut)) } } diff --git a/submodules/MessageReactionListUI/Info.plist b/submodules/MessageReactionListUI/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/MessageReactionListUI/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/MessageReactionListUI/MessageReactionListUI_Xcode.xcodeproj/project.pbxproj b/submodules/MessageReactionListUI/MessageReactionListUI_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..63e8bf9027 --- /dev/null +++ b/submodules/MessageReactionListUI/MessageReactionListUI_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,571 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D072F36823154C230009E66F /* MessageReactionListUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D072F36623154C230009E66F /* MessageReactionListUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D072F37423154E150009E66F /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F37323154E150009E66F /* AsyncDisplayKit.framework */; }; + D072F37623154E180009E66F /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F37523154E180009E66F /* SwiftSignalKit.framework */; }; + D072F37823154E1B0009E66F /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F37723154E1B0009E66F /* Display.framework */; }; + D072F37A23154E1E0009E66F /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F37923154E1E0009E66F /* Postbox.framework */; }; + D072F37C23154E220009E66F /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F37B23154E220009E66F /* TelegramCore.framework */; }; + D072F37E23154E290009E66F /* AccountContext.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F37D23154E290009E66F /* AccountContext.framework */; }; + D072F38023154E390009E66F /* MessageReactionListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072F37F23154E390009E66F /* MessageReactionListController.swift */; }; + D072F38223154EA90009E66F /* TelegramPresentationData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F38123154EA90009E66F /* TelegramPresentationData.framework */; }; + D072F38623156B450009E66F /* MessageReactionCategoryNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072F38523156B450009E66F /* MessageReactionCategoryNode.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D072F36323154C230009E66F /* MessageReactionListUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageReactionListUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F36623154C230009E66F /* MessageReactionListUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageReactionListUI.h; sourceTree = ""; }; + D072F36723154C230009E66F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D072F37323154E150009E66F /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F37523154E180009E66F /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F37723154E1B0009E66F /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F37923154E1E0009E66F /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F37B23154E220009E66F /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F37D23154E290009E66F /* AccountContext.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AccountContext.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F37F23154E390009E66F /* MessageReactionListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReactionListController.swift; sourceTree = ""; }; + D072F38123154EA90009E66F /* TelegramPresentationData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramPresentationData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D072F38523156B450009E66F /* MessageReactionCategoryNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReactionCategoryNode.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D072F36023154C230009E66F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D072F38223154EA90009E66F /* TelegramPresentationData.framework in Frameworks */, + D072F37E23154E290009E66F /* AccountContext.framework in Frameworks */, + D072F37C23154E220009E66F /* TelegramCore.framework in Frameworks */, + D072F37A23154E1E0009E66F /* Postbox.framework in Frameworks */, + D072F37823154E1B0009E66F /* Display.framework in Frameworks */, + D072F37623154E180009E66F /* SwiftSignalKit.framework in Frameworks */, + D072F37423154E150009E66F /* AsyncDisplayKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D072F35923154C230009E66F = { + isa = PBXGroup; + children = ( + D072F36723154C230009E66F /* Info.plist */, + D072F36523154C230009E66F /* Sources */, + D072F36423154C230009E66F /* Products */, + D072F37223154E150009E66F /* Frameworks */, + ); + sourceTree = ""; + }; + D072F36423154C230009E66F /* Products */ = { + isa = PBXGroup; + children = ( + D072F36323154C230009E66F /* MessageReactionListUI.framework */, + ); + name = Products; + sourceTree = ""; + }; + D072F36523154C230009E66F /* Sources */ = { + isa = PBXGroup; + children = ( + D072F36623154C230009E66F /* MessageReactionListUI.h */, + D072F37F23154E390009E66F /* MessageReactionListController.swift */, + D072F38523156B450009E66F /* MessageReactionCategoryNode.swift */, + ); + path = Sources; + sourceTree = ""; + }; + D072F37223154E150009E66F /* Frameworks */ = { + isa = PBXGroup; + children = ( + D072F38123154EA90009E66F /* TelegramPresentationData.framework */, + D072F37D23154E290009E66F /* AccountContext.framework */, + D072F37B23154E220009E66F /* TelegramCore.framework */, + D072F37923154E1E0009E66F /* Postbox.framework */, + D072F37723154E1B0009E66F /* Display.framework */, + D072F37523154E180009E66F /* SwiftSignalKit.framework */, + D072F37323154E150009E66F /* AsyncDisplayKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D072F35E23154C230009E66F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D072F36823154C230009E66F /* MessageReactionListUI.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D072F36223154C230009E66F /* MessageReactionListUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = D072F36B23154C230009E66F /* Build configuration list for PBXNativeTarget "MessageReactionListUI" */; + buildPhases = ( + D072F35E23154C230009E66F /* Headers */, + D072F35F23154C230009E66F /* Sources */, + D072F36023154C230009E66F /* Frameworks */, + D072F36123154C230009E66F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MessageReactionListUI; + productName = MessageReactionListUI; + productReference = D072F36323154C230009E66F /* MessageReactionListUI.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D072F35A23154C230009E66F /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "Telegram Messenger LLP"; + TargetAttributes = { + D072F36223154C230009E66F = { + CreatedOnToolsVersion = 10.3; + LastSwiftMigration = 1030; + }; + }; + }; + buildConfigurationList = D072F35D23154C230009E66F /* Build configuration list for PBXProject "MessageReactionListUI_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D072F35923154C230009E66F; + productRefGroup = D072F36423154C230009E66F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D072F36223154C230009E66F /* MessageReactionListUI */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D072F36123154C230009E66F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D072F35F23154C230009E66F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D072F38623156B450009E66F /* MessageReactionCategoryNode.swift in Sources */, + D072F38023154E390009E66F /* MessageReactionListController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D072F36923154C230009E66F /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStoreLLC; + }; + D072F36A23154C230009E66F /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStoreLLC; + }; + D072F36C23154C230009E66F /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MessageReactionListUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugAppStoreLLC; + }; + D072F36D23154C230009E66F /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MessageReactionListUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseAppStoreLLC; + }; + D072F36E23154C450009E66F /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D072F36F23154C450009E66F /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MessageReactionListUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugHockeyapp; + }; + D072F37023154C530009E66F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyappInternal; + }; + D072F37123154C530009E66F /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MessageReactionListUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseHockeyappInternal; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D072F35D23154C230009E66F /* Build configuration list for PBXProject "MessageReactionListUI_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D072F36923154C230009E66F /* DebugAppStoreLLC */, + D072F36E23154C450009E66F /* DebugHockeyapp */, + D072F36A23154C230009E66F /* ReleaseAppStoreLLC */, + D072F37023154C530009E66F /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStoreLLC; + }; + D072F36B23154C230009E66F /* Build configuration list for PBXNativeTarget "MessageReactionListUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D072F36C23154C230009E66F /* DebugAppStoreLLC */, + D072F36F23154C450009E66F /* DebugHockeyapp */, + D072F36D23154C230009E66F /* ReleaseAppStoreLLC */, + D072F37123154C530009E66F /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStoreLLC; + }; +/* End XCConfigurationList section */ + }; + rootObject = D072F35A23154C230009E66F /* Project object */; +} diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionCategoryNode.swift b/submodules/MessageReactionListUI/Sources/MessageReactionCategoryNode.swift new file mode 100644 index 0000000000..379ef364c6 --- /dev/null +++ b/submodules/MessageReactionListUI/Sources/MessageReactionCategoryNode.swift @@ -0,0 +1,103 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TelegramCore + +final class MessageReactionCategoryNode: ASDisplayNode { + let category: MessageReactionListCategory + private let action: () -> Void + + private let buttonNode: HighlightableButtonNode + private let highlightedBackgroundNode: ASImageNode + private let iconNode: ASImageNode + private let emojiNode: ImmediateTextNode + private let countNode: ImmediateTextNode + + var isSelected = false { + didSet { + self.highlightedBackgroundNode.alpha = self.isSelected ? 1.0 : 0.0 + } + } + + init(theme: PresentationTheme, category: MessageReactionListCategory, count: Int, action: @escaping () -> Void) { + self.category = category + self.action = action + + self.buttonNode = HighlightableButtonNode() + + self.highlightedBackgroundNode = ASImageNode() + self.highlightedBackgroundNode.displaysAsynchronously = false + self.highlightedBackgroundNode.displayWithoutProcessing = true + self.highlightedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: UIColor(rgb: 0xe6e6e8)) + self.highlightedBackgroundNode.alpha = 1.0 + + self.iconNode = ASImageNode() + + self.emojiNode = ImmediateTextNode() + self.emojiNode.displaysAsynchronously = false + let emojiText: String + switch category { + case .all: + emojiText = "" + self.iconNode.image = PresentationResourcesChat.chatInputTextFieldTimerImage(theme) + case let .reaction(value): + emojiText = value + } + self.emojiNode.attributedText = NSAttributedString(string: emojiText, font: Font.regular(18.0), textColor: .black) + + self.countNode = ImmediateTextNode() + self.countNode.displaysAsynchronously = false + self.countNode.attributedText = NSAttributedString(string: "\(count)", font: Font.regular(16.0), textColor: .black) + + super.init() + + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.iconNode) + self.addSubnode(self.emojiNode) + self.addSubnode(self.countNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout() -> CGSize { + let sideInset: CGFloat = 6.0 + let spacing: CGFloat = 2.0 + let emojiSize = self.emojiNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + let iconSize = self.iconNode.image?.size ?? CGSize() + let countSize = self.countNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + + let height: CGFloat = 60.0 + let backgroundHeight: CGFloat = 36.0 + + self.emojiNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - emojiSize.height) / 2.0)), size: emojiSize) + self.iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floor((height - iconSize.height) / 2.0)), size: iconSize) + + let iconFrame: CGRect + if self.iconNode.image != nil { + iconFrame = self.iconNode.frame + } else { + iconFrame = self.emojiNode.frame + } + + self.countNode.frame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((height - countSize.height) / 2.0)), size: countSize) + let contentWidth = sideInset * 2.0 + spacing + iconFrame.width + countSize.width + self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - backgroundHeight) / 2.0)), size: CGSize(width: contentWidth, height: backgroundHeight)) + + let size = CGSize(width: contentWidth, height: height) + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + return size + } + + @objc private func buttonPressed() { + self.action() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.buttonNode.frame.contains(point) { + return self.buttonNode.view + } + return nil + } +} diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift new file mode 100644 index 0000000000..c61df42470 --- /dev/null +++ b/submodules/MessageReactionListUI/Sources/MessageReactionListController.swift @@ -0,0 +1,418 @@ +import Foundation +import Display +import AccountContext +import TelegramPresentationData +import Postbox +import TelegramCore +import SwiftSignalKit +import MergeLists +import ItemListPeerItem + +public final class MessageReactionListController: ViewController { + private let context: AccountContext + private let messageId: MessageId + private let presentatonData: PresentationData + private let initialReactions: [MessageReaction] + + private var controllerNode: MessageReactionListControllerNode { + return self.displayNode as! MessageReactionListControllerNode + } + + private var animatedIn: Bool = false + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + public init(context: AccountContext, messageId: MessageId, initialReactions: [MessageReaction]) { + self.context = context + self.messageId = messageId + self.presentatonData = context.sharedContext.currentPresentationData.with { $0 } + self.initialReactions = initialReactions + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = MessageReactionListControllerNode(context: self.context, presentatonData: self.presentatonData, messageId: messageId, initialReactions: initialReactions, dismiss: { [weak self] in + self?.dismiss() + }) + + super.displayNodeDidLoad() + + self._ready.set(self.controllerNode.isReady.get()) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout: layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } +} + +private struct MessageReactionListTransaction { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private struct MessageReactionListEntry: Comparable, Identifiable { + let index: Int + let item: MessageReactionListCategoryItem + + var stableId: PeerId { + return self.item.peer.id + } + + static func <(lhs: MessageReactionListEntry, rhs: MessageReactionListEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, presentationData: PresentationData) -> ListViewItem { + return ItemListPeerItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, account: context.account, peer: self.item.peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .none, label: .text(self.item.reaction), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: { + + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, noInsets: true, tag: nil) + } +} + +private func preparedTransition(from fromEntries: [MessageReactionListEntry], to toEntries: [MessageReactionListEntry], context: AccountContext, presentationData: PresentationData) -> MessageReactionListTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData), directionHint: nil) } + + return MessageReactionListTransaction(deletions: deletions, insertions: insertions, updates: updates) +} + +private let headerHeight: CGFloat = 60.0 +private let itemHeight: CGFloat = 50.0 + +private func topInsetForLayout(layout: ContainerViewLayout, itemCount: Int) -> CGFloat { + let contentHeight = CGFloat(itemCount) * itemHeight + let minimumItemHeights: CGFloat = contentHeight + + return max(layout.size.height - layout.intrinsicInsets.bottom - minimumItemHeights, headerHeight) +} + +private final class MessageReactionListControllerNode: ViewControllerTracingNode { + private let context: AccountContext + private let presentatonData: PresentationData + private let dismiss: () -> Void + + private let listContext: MessageReactionListContext + + private let dimNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentHeaderContainerNode: ASDisplayNode + private let contentHeaderContainerBackgroundNode: ASImageNode + private var categoryItemNodes: [MessageReactionCategoryNode] = [] + private let categoryScrollNode: ASScrollNode + private let listNode: ListView + + private var validLayout: ContainerViewLayout? + + private var currentCategory: MessageReactionListCategory = .all + private var currentState: MessageReactionListState? + + private var enqueuedTransactions: [MessageReactionListTransaction] = [] + + private let disposable = MetaDisposable() + + let isReady = Promise() + + private var forceHeaderTransition: ContainedViewLayoutTransition? + + init(context: AccountContext, presentatonData: PresentationData, messageId: MessageId, initialReactions: [MessageReaction], dismiss: @escaping () -> Void) { + self.context = context + self.presentatonData = presentatonData + self.dismiss = dismiss + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = self.presentatonData.theme.actionSheet.opaqueItemBackgroundColor + + self.contentHeaderContainerNode = ASDisplayNode() + self.contentHeaderContainerBackgroundNode = ASImageNode() + self.contentHeaderContainerBackgroundNode.displaysAsynchronously = false + + self.categoryScrollNode = ASScrollNode() + self.contentHeaderContainerBackgroundNode.displayWithoutProcessing = true + self.contentHeaderContainerBackgroundNode.image = generateImage(CGSize(width: 10.0, height: 10.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentatonData.theme.rootController.navigationBar.backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: CGSize(width: size.width, height: size.height / 2.0))) + })?.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) + + self.listNode = ListView() + self.listNode.limitHitTestToNodes = true + + self.listContext = MessageReactionListContext(postbox: self.context.account.postbox, network: self.context.account.network, messageId: messageId, initialReactions: initialReactions) + + super.init() + + self.addSubnode(self.dimNode) + self.addSubnode(self.backgroundNode) + + self.listNode.stackFromBottom = false + self.addSubnode(self.listNode) + + self.addSubnode(self.contentHeaderContainerNode) + self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerBackgroundNode) + self.contentHeaderContainerNode.addSubnode(self.categoryScrollNode) + + self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in + guard let strongSelf = self, let layout = strongSelf.validLayout else { + return + } + + let transition = strongSelf.forceHeaderTransition ?? listTransition + strongSelf.forceHeaderTransition = nil + + let topOffset = offset + transition.updateFrame(node: strongSelf.contentHeaderContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight), size: CGSize(width: layout.size.width, height: headerHeight))) + transition.updateFrame(node: strongSelf.contentHeaderContainerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: headerHeight))) + transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight / 2.0), size: CGSize(width: layout.size.width, height: layout.size.height + 300.0))) + } + + self.disposable.set((self.listContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + self?.updateState(state) + })) + } + + deinit { + self.disposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapGesture))) + } + + func containerLayoutUpdated(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.validLayout == nil + self.validLayout = layout + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + //transition.updateBounds(node: self.listNode, bounds: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)) + //transition.updatePosition(node: self.listNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + var currentCategoryItemCount = 0 + if let currentState = self.currentState { + for (category, categoryState) in currentState.states { + if category == self.currentCategory { + currentCategoryItemCount = categoryState.count + break + } + } + } + + var insets = UIEdgeInsets() + insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount) + insets.bottom = layout.intrinsicInsets.bottom + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut, .custom: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default(duration: duration) + } + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let sideInset: CGFloat = 12.0 + let spacing: CGFloat = 6.0 + var leftX = sideInset + for itemNode in self.categoryItemNodes { + let itemSize = itemNode.updateLayout() + itemNode.frame = CGRect(origin: CGPoint(x: leftX, y: 0.0), size: itemSize) + leftX += spacing + itemSize.width + } + leftX += sideInset + self.categoryScrollNode.view.contentSize = CGSize(width: leftX, height: 60.0) + self.categoryScrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 60.0)) + + if isFirstLayout { + while !self.enqueuedTransactions.isEmpty { + self.dequeueTransaction() + } + } + } + + func animateIn() { + self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.dimNode.layer.animatePosition(from: CGPoint(x: self.dimNode.position.x, y: self.dimNode.position.y - self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + }) + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void) { + self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.dimNode.layer.animatePosition(from: self.dimNode.position, to: CGPoint(x: self.dimNode.position.x, y: self.dimNode.position.y - self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func updateState(_ state: MessageReactionListState) { + if self.currentState != state { + self.currentState = state + + self.updateItems() + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(layout: validLayout, transition: .immediate) + } + } + } + + private var currentEntries: [MessageReactionListEntry]? + private func updateItems() { + var entries: [MessageReactionListEntry] = [] + + var index = 0 + let states = self.currentState?.states ?? [] + for (category, categoryState) in states { + if self.categoryItemNodes.count <= index { + let itemNode = MessageReactionCategoryNode(theme: self.presentatonData.theme, category: category, count: categoryState.count, action: { [weak self] in + self?.setCategory(category) + }) + self.categoryItemNodes.append(itemNode) + self.categoryScrollNode.addSubnode(itemNode) + if category == self.currentCategory { + itemNode.isSelected = true + } else { + itemNode.isSelected = false + } + } + + if category == self.currentCategory { + for item in categoryState.items { + entries.append(MessageReactionListEntry(index: entries.count, item: item)) + } + if !categoryState.items.isEmpty { + self.isReady.set(.single(true)) + } + } + index += 1 + } + let transaction = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: self.presentatonData) + self.currentEntries = entries + + self.enqueuedTransactions.append(transaction) + self.dequeueTransaction() + } + + func setCategory(_ category: MessageReactionListCategory) { + if self.currentCategory != category { + self.currentCategory = category + + for itemNode in self.categoryItemNodes { + itemNode.isSelected = category == itemNode.category + } + + self.forceHeaderTransition = .animated(duration: 0.3, curve: .spring) + if let validLayout = self.validLayout { + self.containerLayoutUpdated(layout: validLayout, transition: .animated(duration: 0.3, curve: .spring)) + } + + self.updateItems() + } + } + + private func dequeueTransaction() { + guard let layout = self.validLayout, let transaction = self.enqueuedTransactions.first else { + return + } + + self.enqueuedTransactions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.Synchronous) + //options.insert(.AnimateTopItemPosition) + //options.insert(.AnimateCrossfade) + options.insert(.PreferSynchronousResourceLoading) + + var currentCategoryItemCount = 0 + if let currentState = self.currentState { + for (category, categoryState) in currentState.states { + if category == self.currentCategory { + currentCategoryItemCount = categoryState.count + break + } + } + } + + var insets = UIEdgeInsets() + insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount) + insets.bottom = layout.intrinsicInsets.bottom + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: self.listNode.bounds.size, insets: insets, duration: 0.0, curve: .Default(duration: nil)) + + self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { _ in + }) + } + + @objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for itemNode in self.categoryItemNodes { + if let result = itemNode.hitTest(self.view.convert(point, to: itemNode.view), with: event) { + return result + } + } + return super.hitTest(point, with: event) + } +} diff --git a/submodules/MessageReactionListUI/Sources/MessageReactionListUI.h b/submodules/MessageReactionListUI/Sources/MessageReactionListUI.h new file mode 100644 index 0000000000..6460ffc97e --- /dev/null +++ b/submodules/MessageReactionListUI/Sources/MessageReactionListUI.h @@ -0,0 +1,19 @@ +// +// MessageReactionListUI.h +// MessageReactionListUI +// +// Created by Peter on 8/27/19. +// Copyright © 2019 Telegram Messenger LLP. All rights reserved. +// + +#import + +//! Project version number for MessageReactionListUI. +FOUNDATION_EXPORT double MessageReactionListUIVersionNumber; + +//! Project version string for MessageReactionListUI. +FOUNDATION_EXPORT const unsigned char MessageReactionListUIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/submodules/TelegramCore/TelegramCore/MessageReactionList.swift b/submodules/TelegramCore/TelegramCore/MessageReactionList.swift new file mode 100644 index 0000000000..85a6b34d8b --- /dev/null +++ b/submodules/TelegramCore/TelegramCore/MessageReactionList.swift @@ -0,0 +1,216 @@ +import Foundation +#if os(macOS) +import PostboxMac +import SwiftSignalKitMac +import MtProtoKitMac +import TelegramApiMac +#else +import Postbox +import SwiftSignalKit +import TelegramApi +#if BUCK +import MtProtoKit +#else +import MtProtoKitDynamic +#endif +#endif + +public enum MessageReactionListCategory: Hashable { + case all + case reaction(String) +} + +public final class MessageReactionListCategoryItem: Equatable { + public let peer: Peer + public let reaction: String + + init(peer: Peer, reaction: String) { + self.peer = peer + self.reaction = reaction + } + + public static func ==(lhs: MessageReactionListCategoryItem, rhs: MessageReactionListCategoryItem) -> Bool { + if lhs.peer.id != rhs.peer.id { + return false + } + if lhs.reaction != rhs.reaction { + return false + } + return true + } +} + +public struct MessageReactionListCategoryState: Equatable { + public var count: Int + public var completed: Bool + public var items: [MessageReactionListCategoryItem] + public var loadingMore: Bool + fileprivate var nextOffset: String? +} + +private enum LoadReactionsError { + case generic +} + +private final class MessageReactionCategoryContext { + private let postbox: Postbox + private let network: Network + private let messageId: MessageId + private let category: MessageReactionListCategory + private var state: MessageReactionListCategoryState + var statePromise: ValuePromise + + private let loadingDisposable = MetaDisposable() + + init(postbox: Postbox, network: Network, messageId: MessageId, category: MessageReactionListCategory, initialState: MessageReactionListCategoryState) { + self.postbox = postbox + self.network = network + self.messageId = messageId + self.category = category + self.state = initialState + self.statePromise = ValuePromise(initialState) + } + + deinit { + self.loadingDisposable.dispose() + } + + func loadMore() { + if self.state.completed || self.state.loadingMore { + return + } + self.state.loadingMore = true + self.statePromise.set(self.state) + + var flags: Int32 = 0 + var reaction: String? + switch self.category { + case .all: + break + case let .reaction(value): + flags |= 1 << 0 + reaction = value + } + let messageId = self.messageId + let offset = self.state.nextOffset + let request = self.postbox.transaction { transaction -> Api.InputPeer? in + let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + return inputPeer + } + |> introduceError(LoadReactionsError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .fail(.generic) + } + return self.network.request(Api.functions.messages.getMessageReactionsList(flags: flags, peer: inputPeer, id: messageId.id, reaction: reaction, offset: offset, limit: 64)) + |> mapError { _ -> LoadReactionsError in + return .generic + } + } + self.loadingDisposable.set((request + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + let currentState = strongSelf.state + let _ = (strongSelf.postbox.transaction { transaction -> MessageReactionListCategoryState in + var mergedItems = currentState.items + var currentIds = Set(mergedItems.lazy.map { $0.peer.id }) + switch result { + case let .messageReactionsList(_, count, reactions, users, nextOffset): + var peers: [Peer] = [] + for user in users { + let parsedUser = TelegramUser(user: user) + peers.append(parsedUser) + } + updatePeers(transaction: transaction, peers: peers, update: { _, updated in updated }) + for reaction in reactions { + switch reaction { + case let .messageUserReaction(userId, reaction): + if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)) { + if !currentIds.contains(peer.id) { + currentIds.insert(peer.id) + mergedItems.append(MessageReactionListCategoryItem(peer: peer, reaction: reaction)) + } + } + } + } + return MessageReactionListCategoryState(count: max(mergedItems.count, Int(count)), completed: nextOffset == nil, items: mergedItems, loadingMore: false, nextOffset: nextOffset) + } + } + |> deliverOnMainQueue).start(next: { state in + guard let strongSelf = self else { + return + } + strongSelf.state = state + strongSelf.statePromise.set(state) + }) + }, error: { _ in + + })) + } +} + +public struct MessageReactionListState: Equatable { + public var states: [(MessageReactionListCategory, MessageReactionListCategoryState)] + + public static func ==(lhs: MessageReactionListState, rhs: MessageReactionListState) -> Bool { + if lhs.states.count != rhs.states.count { + return false + } + for i in 0 ..< lhs.states.count { + if lhs.states[i].0 != rhs.states[i].0 { + return false + } + if lhs.states[i].1 != rhs.states[i].1 { + return false + } + } + return true + } +} + +public final class MessageReactionListContext { + private let postbox: Postbox + private let network: Network + + private var categoryContexts: [MessageReactionListCategory: MessageReactionCategoryContext] = [:] + + private let _state = Promise() + public var state: Signal { + return self._state.get() + } + + public init(postbox: Postbox, network: Network, messageId: MessageId, initialReactions: [MessageReaction]) { + self.postbox = postbox + self.network = network + + var allState = MessageReactionListCategoryState(count: 0, completed: false, items: [], loadingMore: false, nextOffset: nil) + var signals: [Signal<(MessageReactionListCategory, MessageReactionListCategoryState), NoError>] = [] + for reaction in initialReactions { + allState.count += Int(reaction.count) + let context = MessageReactionCategoryContext(postbox: postbox, network: network, messageId: messageId, category: .reaction(reaction.value), initialState: MessageReactionListCategoryState(count: Int(reaction.count), completed: false, items: [], loadingMore: false, nextOffset: nil)) + signals.append(context.statePromise.get() |> map { value -> (MessageReactionListCategory, MessageReactionListCategoryState) in + return (.reaction(reaction.value), value) + }) + self.categoryContexts[.reaction(reaction.value)] = context + context.loadMore() + } + let allContext = MessageReactionCategoryContext(postbox: postbox, network: network, messageId: messageId, category: .all, initialState: allState) + signals.insert(allContext.statePromise.get() |> map { value -> (MessageReactionListCategory, MessageReactionListCategoryState) in + return (.all, value) + }, at: 0) + self.categoryContexts[.all] = allContext + + self._state.set(combineLatest(queue: .mainQueue(), signals) + |> map { states in + return MessageReactionListState(states: states) + }) + + allContext.loadMore() + } + + public func loadMore(category: MessageReactionListCategory) { + self.categoryContexts[category]?.loadMore() + } +} diff --git a/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj b/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj index 63bc08ccc6..e470875ec7 100644 --- a/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj +++ b/submodules/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj @@ -450,6 +450,8 @@ D07047B81F3DF2CD00F6A8D4 /* ManagedConsumePersonalMessagesActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07047B61F3DF2CD00F6A8D4 /* ManagedConsumePersonalMessagesActions.swift */; }; D07047BA1F3DF75500F6A8D4 /* ConsumePersonalMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07047B91F3DF75500F6A8D4 /* ConsumePersonalMessageAction.swift */; }; D07047BB1F3DF75500F6A8D4 /* ConsumePersonalMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07047B91F3DF75500F6A8D4 /* ConsumePersonalMessageAction.swift */; }; + D072F357231542740009E66F /* MessageReactionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072F356231542740009E66F /* MessageReactionList.swift */; }; + D072F358231542740009E66F /* MessageReactionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072F356231542740009E66F /* MessageReactionList.swift */; }; D073CE5D1DCB97F6007511FD /* ForwardSourceInfoAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE5C1DCB97F6007511FD /* ForwardSourceInfoAttribute.swift */; }; D073CE601DCB9D14007511FD /* OutgoingMessageInfoAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE5F1DCB9D14007511FD /* OutgoingMessageInfoAttribute.swift */; }; D073CE6A1DCBCF17007511FD /* ViewCountMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B0CE71D6224AD00955575 /* ViewCountMessageAttribute.swift */; }; @@ -1042,6 +1044,7 @@ D07047B31F3DF1FE00F6A8D4 /* ConsumablePersonalMentionMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsumablePersonalMentionMessageAttribute.swift; sourceTree = ""; }; D07047B61F3DF2CD00F6A8D4 /* ManagedConsumePersonalMessagesActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedConsumePersonalMessagesActions.swift; sourceTree = ""; }; D07047B91F3DF75500F6A8D4 /* ConsumePersonalMessageAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsumePersonalMessageAction.swift; sourceTree = ""; }; + D072F356231542740009E66F /* MessageReactionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReactionList.swift; sourceTree = ""; }; D073CE5C1DCB97F6007511FD /* ForwardSourceInfoAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardSourceInfoAttribute.swift; sourceTree = ""; }; D073CE5F1DCB9D14007511FD /* OutgoingMessageInfoAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingMessageInfoAttribute.swift; sourceTree = ""; }; D0750C8F22B2FD8300BE5F6E /* PeerAccessHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerAccessHash.swift; sourceTree = ""; }; @@ -1606,6 +1609,7 @@ D01AC91C1DD5DA5E00E8160F /* RequestMessageActionCallback.swift */, D0AB262A21C3CE80008F6685 /* Polls.swift */, D0329EA122FC5A7C00F9F071 /* MessageReactions.swift */, + D072F356231542740009E66F /* MessageReactionList.swift */, D01AC9201DD5E7E500E8160F /* RequestEditMessage.swift */, D0DC354D1DE368F7000195EB /* RequestChatContextResults.swift */, D0DC354F1DE36900000195EB /* ChatContextResult.swift */, @@ -2453,6 +2457,7 @@ D054649120738653002ECC1E /* SecureIdIDCardValue.swift in Sources */, D018EE052045E95000CBB130 /* CheckPeerChatServiceActions.swift in Sources */, D0F3A8A51E82C94C00B4C64C /* SynchronizeableChatInputState.swift in Sources */, + D072F357231542740009E66F /* MessageReactionList.swift in Sources */, D03B0CD71D62245300955575 /* TelegramGroup.swift in Sources */, D02609BF20C6EC08006C34AC /* Crypto.m in Sources */, D0B8438C1DA7CF50005F29E1 /* BotInfo.swift in Sources */, @@ -2783,6 +2788,7 @@ D05A32E81E6F0B5C002760B4 /* RecentAccountSession.swift in Sources */, D0F7B1E31E045C7B007EB8A5 /* RichText.swift in Sources */, D0575C2E22B922DF00A71A0E /* DeleteAccount.swift in Sources */, + D072F358231542740009E66F /* MessageReactionList.swift in Sources */, D0FA8BB11E1FEC7E001E855B /* SecretChatEncryptionConfig.swift in Sources */, D0B418AA1D7E0597004562A4 /* Download.swift in Sources */, D001F3F41E128A1C007A8C60 /* UpdatesApiUtils.swift in Sources */, diff --git a/submodules/TelegramUI/TelegramUI/AppDelegate.swift b/submodules/TelegramUI/TelegramUI/AppDelegate.swift index faf983cda1..7cadafdf72 100644 --- a/submodules/TelegramUI/TelegramUI/AppDelegate.swift +++ b/submodules/TelegramUI/TelegramUI/AppDelegate.swift @@ -57,12 +57,13 @@ private class ApplicationStatusBarHost: StatusBarHost { return self.application.statusBarFrame } var statusBarStyle: UIStatusBarStyle { - get { - return self.application.statusBarStyle - } set(value) { - self.application.setStatusBarStyle(value, animated: false) - } + return self.application.statusBarStyle } + + func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) { + self.application.setStatusBarStyle(style, animated: animated) + } + var statusBarWindow: UIView? { return self.application.value(forKey: "statusBarWindow") as? UIView } @@ -237,7 +238,7 @@ final class SharedApplicationContext { let (window, hostView, aboveStatusbarWindow) = nativeWindowHostView() self.mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost) self.aboveStatusbarWindow = aboveStatusbarWindow - window.backgroundColor = UIColor.white + hostView.containerView.backgroundColor = UIColor.white self.window = window self.nativeWindow = window @@ -656,7 +657,7 @@ final class SharedApplicationContext { } |> deliverOnMainQueue |> mapToSignal { accountManager, initialPresentationDataAndSettings -> Signal<(SharedApplicationContext, LoggingSettings), NoError> in - self.window?.backgroundColor = initialPresentationDataAndSettings.presentationData.theme.chatList.backgroundColor + self.mainWindow?.hostView.containerView.backgroundColor = initialPresentationDataAndSettings.presentationData.theme.chatList.backgroundColor let legacyBasePath = appGroupUrl.path let legacyCache = LegacyCache(path: legacyBasePath + "/Caches") diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 4a367294dc..2a55b64dab 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -44,6 +44,7 @@ import PeerInfoUI import RaiseToListen import UrlHandling import ReactionSelectionNode +import MessageReactionListUI public enum ChatControllerPeekActions { case standard @@ -286,6 +287,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var updateSlowmodeStatusDisposable = MetaDisposable() private var updateSlowmodeStatusTimerValue: Int32? + + private var isDismissed = false public override var customData: Any? { return self.chatLocation @@ -343,6 +346,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) self.blocksBackgroundWhenInOverlay = true + if let subject = subject, case .scheduledMessages = subject { + self.isModalWhenInOverlay = true + } self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -1499,8 +1505,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleTime) if !strongSelf.presentationInterfaceState.isScheduledMessages { - let controller = ChatControllerImpl(context: strongSelf.context, chatLocation: strongSelf.chatLocation, subject: .scheduledMessages) - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + strongSelf.openScheduledMessages() } } }) @@ -1581,6 +1586,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, reaction: reaction).start() + }, openMessageReactions: { [weak self] messageId in + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(messageId) + } + |> deliverOnMainQueue).start(next: { message in + guard let strongSelf = self, let message = message else { + return + } + var initialReactions: [MessageReaction] = [] + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + initialReactions = attribute.reactions + } + } + + if !initialReactions.isEmpty { + strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root)) + } + }) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -3856,8 +3883,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, openScheduledMessages: { [weak self] in if let strongSelf = self { - let controller = ChatControllerImpl(context: strongSelf.context, chatLocation: strongSelf.chatLocation, subject: .scheduledMessages) - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + strongSelf.openScheduledMessages() } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) @@ -4213,6 +4239,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + + if let subject = self.subject, case .scheduledMessages = subject { + self.chatDisplayNode.animateIn() + self.updateTransitionWhenPresentedAsModal?(1.0, .animated(duration: 0.5, curve: .spring)) + } } override public func viewWillDisappear(_ animated: Bool) { @@ -4571,7 +4602,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G // // } - if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: self.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { if self.leftNavigationButton != button { var animated = transition.isAnimated if let currentButton = self.leftNavigationButton?.action, currentButton == button.action { @@ -4671,115 +4702,117 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func navigationButtonAction(_ action: ChatNavigationButtonAction) { switch action { - case .cancelMessageSelection: - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - case .clearHistory: - if case let .peer(peerId) = self.chatLocation { - guard let peer = self.presentationInterfaceState.renderedPeer, let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else { + case .cancelMessageSelection: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + case .clearHistory: + if case let .peer(peerId) = self.chatLocation { + guard let peer = self.presentationInterfaceState.renderedPeer, let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else { + return + } + + let text: String + if peerId == self.context.account.peerId { + text = self.presentationData.strings.Conversation_ClearSelfHistory + } else if peerId.namespace == Namespaces.Peer.SecretChat { + text = self.presentationData.strings.Conversation_ClearSecretHistory + } else if peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { + text = self.presentationData.strings.Conversation_ClearGroupHistory + } else { + text = self.presentationData.strings.Conversation_ClearPrivateHistory + } + + var canRemoveGlobally = false + let limitsConfiguration = self.context.currentLimitsConfiguration.with { $0 } + if peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId { + if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever { + canRemoveGlobally = true + } + } + if let user = chatPeer as? TelegramUser, user.botInfo != nil { + canRemoveGlobally = false + } + + let account = self.context.account + + let beginClear: (InteractiveHistoryClearingType) -> Void = { [weak self] type in + guard let strongSelf = self else { return } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + strongSelf.chatDisplayNode.historyNode.historyAppearsCleared = true - let text: String - if peerId == self.context.account.peerId { - text = self.presentationData.strings.Conversation_ClearSelfHistory - } else if peerId.namespace == Namespaces.Peer.SecretChat { - text = self.presentationData.strings.Conversation_ClearSecretHistory - } else if peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel { - text = self.presentationData.strings.Conversation_ClearGroupHistory + let statusText: String + if strongSelf.presentationInterfaceState.isScheduledMessages { + statusText = strongSelf.presentationData.strings.Undo_ScheduledMessagesCleared + } else if case .forEveryone = type { + statusText = strongSelf.presentationData.strings.Undo_ChatClearedForBothSides } else { - text = self.presentationData.strings.Conversation_ClearPrivateHistory + statusText = strongSelf.presentationData.strings.Undo_ChatCleared } - var canRemoveGlobally = false - let limitsConfiguration = self.context.currentLimitsConfiguration.with { $0 } - if peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId { - if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever { - canRemoveGlobally = true - } - } - if let user = chatPeer as? TelegramUser, user.botInfo != nil { - canRemoveGlobally = false - } - - let account = self.context.account - - let beginClear: (InteractiveHistoryClearingType) -> Void = { [weak self] type in - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - strongSelf.chatDisplayNode.historyNode.historyAppearsCleared = true - - let statusText: String - if strongSelf.presentationInterfaceState.isScheduledMessages { - statusText = strongSelf.presentationData.strings.Undo_ScheduledMessagesCleared - } else if case .forEveryone = type { - statusText = strongSelf.presentationData.strings.Undo_ChatClearedForBothSides - } else { - statusText = strongSelf.presentationData.strings.Undo_ChatCleared - } - - strongSelf.present(UndoOverlayController(context: strongSelf.context, content: .removedChat(text: statusText), elevatedLayout: true, action: { shouldCommit in - if shouldCommit { - let _ = clearHistoryInteractively(postbox: account.postbox, peerId: peerId, type: type).start(completed: { - self?.chatDisplayNode.historyNode.historyAppearsCleared = false - }) - } else { + strongSelf.present(UndoOverlayController(context: strongSelf.context, content: .removedChat(text: statusText), elevatedLayout: true, action: { shouldCommit in + if shouldCommit { + let _ = clearHistoryInteractively(postbox: account.postbox, peerId: peerId, type: type).start(completed: { self?.chatDisplayNode.historyNode.historyAppearsCleared = false - } - }), in: .window(.root)) - } - - let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) - var items: [ActionSheetItem] = [] - - if self.presentationInterfaceState.isScheduledMessages { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - beginClear(.scheduledMessages) - })) - } else if canRemoveGlobally { - items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: self.presentationData.strings)) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in - beginClear(.forEveryone) - actionSheet?.dismissAnimated() - })) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - beginClear(.forLocalPeer) - })) - } else { - items.append(ActionSheetTextItem(title: text)) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - beginClear(.forLocalPeer) - })) - } + }) + } else { + self?.chatDisplayNode.historyNode.historyAppearsCleared = false + } + }), in: .window(.root)) + } + + let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + var items: [ActionSheetItem] = [] + + if self.presentationInterfaceState.isScheduledMessages { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + beginClear(.scheduledMessages) + })) + } else if canRemoveGlobally { + items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: self.presentationData.strings)) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in + beginClear(.forEveryone) + actionSheet?.dismissAnimated() + })) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + beginClear(.forLocalPeer) + })) + } else { + items.append(ActionSheetTextItem(title: text)) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + beginClear(.forLocalPeer) + })) + } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) - } - case .openChatInfo: - switch self.chatLocationInfoData { - case let .peer(peerView): - self.navigationActionDisposable.set((peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios") == nil && !strongSelf.presentationInterfaceState.isNotAccessible { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { - (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) - } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + } + case .openChatInfo: + switch self.chatLocationInfoData { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios") == nil && !strongSelf.presentationInterfaceState.isNotAccessible { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } - })) - } - case .search: - self.interfaceInteraction?.beginMessageSearch(.everything, "") + } + })) + } + case .search: + self.interfaceInteraction?.beginMessageSearch(.everything, "") + case .dismiss: + self.dismiss() } } @@ -4944,8 +4977,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { done(time) if !strongSelf.presentationInterfaceState.isScheduledMessages { - let controller = ChatControllerImpl(context: strongSelf.context, chatLocation: strongSelf.chatLocation, subject: .scheduledMessages) - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + strongSelf.openScheduledMessages() } } }) @@ -4986,8 +5018,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { done(time) if !strongSelf.presentationInterfaceState.isScheduledMessages { - let controller = ChatControllerImpl(context: strongSelf.context, chatLocation: strongSelf.chatLocation, subject: .scheduledMessages) - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + strongSelf.openScheduledMessages() } } }) @@ -5171,8 +5202,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { done(time) if !strongSelf.presentationInterfaceState.isScheduledMessages { - let controller = ChatControllerImpl(context: strongSelf.context, chatLocation: strongSelf.chatLocation, subject: .scheduledMessages) - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + strongSelf.openScheduledMessages() } } }) @@ -7535,4 +7565,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateSlowmodeStatusDisposable.set(nil) } } + + private func openScheduledMessages() { + let controller = ChatControllerImpl(context: self.context, chatLocation: self.chatLocation, subject: .scheduledMessages) + self.present(controller, in: .window(.root)) + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + self.chatDisplayNode.animateOut(completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + self.updateTransitionWhenPresentedAsModal?(0.0, .animated(duration: 0.2, curve: .easeInOut)) + } + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift index c36a16e51d..e1a7ce7cbc 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift @@ -95,6 +95,7 @@ public final class ChatControllerInteraction { let editScheduledMessagesTime: ([MessageId]) -> Void let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void let updateMessageReaction: (MessageId, String) -> Void + let openMessageReactions: (MessageId) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -109,7 +110,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: String? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, openMessageReactions: @escaping (MessageId) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -158,6 +159,7 @@ public final class ChatControllerInteraction { self.editScheduledMessagesTime = editScheduledMessagesTime self.performTextSelectionAction = performTextSelectionAction self.updateMessageReaction = updateMessageReaction + self.openMessageReactions = openMessageReactions self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -193,6 +195,7 @@ public final class ChatControllerInteraction { }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 9a77946636..1c2438c721 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -2100,4 +2100,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } } + + func animateIn(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + completion?() + }) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + completion?() + }) + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 8fc3cd71c8..aee93f6cd7 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -3,12 +3,14 @@ import UIKit import Postbox import TelegramCore import TelegramPresentationData +import AccountContext enum ChatNavigationButtonAction { case openChatInfo case clearHistory case cancelMessageSelection case search + case dismiss } struct ChatNavigationButton: Equatable { @@ -20,7 +22,7 @@ struct ChatNavigationButton: Equatable { } } -func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { +func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, subject: ChatControllerSubject?, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { if let _ = presentationInterfaceState.interfaceState.selectionState { if let currentButton = currentButton, currentButton.action == .clearHistory { return currentButton @@ -45,6 +47,13 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha } } } + if let subject = subject, case .scheduledMessages = subject { + if let currentButton = currentButton, currentButton.action == .dismiss { + return currentButton + } else { + return ChatNavigationButton(action: .dismiss, buttonItem: UIBarButtonItem(title: strings.Common_Done, style: .plain, target: target, action: selector)) + } + } return nil } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift index bf13645ce6..a9203282a4 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -98,10 +98,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private var impressionIcon: ASImageNode? private var reactionNodes: [StatusReactionNode] = [] private var reactionCountNode: TextNode? + private var reactionButtonNode: HighlightTrackingButtonNode? private var type: ChatMessageDateAndStatusType? private var theme: ChatPresentationThemeData? + var openReactions: (() -> Void)? + override init() { self.dateNode = TextNode() self.dateNode.isUserInteractionEnabled = false @@ -109,8 +112,6 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { super.init() - self.isUserInteractionEnabled = false - self.addSubnode(self.dateNode) } @@ -561,7 +562,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } - node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1, y: backgroundInsets.top + 1.0 + offset), size: layout.size) + node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset), size: layout.size) + reactionOffset += 1.0 + layout.size.width } else if let reactionCountNode = strongSelf.reactionCountNode { strongSelf.reactionCountNode = nil if animated { @@ -572,6 +574,27 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionCountNode.removeFromSupernode() } } + + if !strongSelf.reactionNodes.isEmpty { + if strongSelf.reactionButtonNode == nil { + let reactionButtonNode = HighlightTrackingButtonNode() + strongSelf.reactionButtonNode = reactionButtonNode + strongSelf.addSubnode(reactionButtonNode) + reactionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.reactionButtonPressed), forControlEvents: .touchUpInside) + reactionButtonNode.highligthedChanged = { [weak strongSelf] highlighted in + guard let strongSelf = strongSelf else { + return + } + if highlighted { + strongSelf.reactionButtonPressed() + } + } + } + strongSelf.reactionButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset - reactionInset + backgroundInsets.left - 5.0, y: backgroundInsets.top + 1.0 + offset - 5.0), size: CGSize(width: reactionOffset + 5.0 * 2.0, height: 20.0)) + } else if let reactionButtonNode = strongSelf.reactionButtonNode { + strongSelf.reactionButtonNode = nil + reactionButtonNode.removeFromSupernode() + } } }) } @@ -605,4 +628,17 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } return nil } + + @objc private func reactionButtonPressed() { + self.openReactions?() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let reactionButtonNode = self.reactionButtonNode { + if reactionButtonNode.frame.contains(point) { + return reactionButtonNode.view + } + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift index 28e189b189..4a2c5d3a67 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -65,6 +65,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textAccessibilityOverlayNode.openUrl = { [weak self] url in self?.item?.controllerInteraction.openUrl(url, false, false) } + + self.statusNode.openReactions = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.openMessageReactions(item.message.id) + } } required init?(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index 3cdbbaf05d..dcc4f15462 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -411,6 +411,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift index 76554ee22c..f3cd2f7bfe 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift @@ -113,6 +113,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 4702f224f8..f22b6c8c5d 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -287,6 +287,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, editScheduledMessagesTime: { _ in }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj index 994c9fab79..e46f7751ad 100644 --- a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj +++ b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj @@ -178,6 +178,7 @@ D06BB8821F58994B0084FC30 /* LegacyInstantVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */; }; D06E0F8E1F79ABFB003CF3DD /* ChatLoadingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */; }; D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */; }; + D072F38423155EAF0009E66F /* MessageReactionListUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D072F38323155EAF0009E66F /* MessageReactionListUI.framework */; }; D0750C7822B2A13300BE5F6E /* UniversalMediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0750C7722B2A13300BE5F6E /* UniversalMediaPlayer.framework */; }; D0750C7A22B2A14300BE5F6E /* DeviceAccess.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0750C7922B2A14300BE5F6E /* DeviceAccess.framework */; }; D0750C7C22B2A14300BE5F6E /* TelegramPresentationData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0750C7B22B2A14300BE5F6E /* TelegramPresentationData.framework */; }; @@ -812,6 +813,7 @@ D06BB8811F58994B0084FC30 /* LegacyInstantVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyInstantVideoController.swift; sourceTree = ""; }; D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLoadingNode.swift; sourceTree = ""; }; D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistorySearchContainerNode.swift; sourceTree = ""; }; + D072F38323155EAF0009E66F /* MessageReactionListUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MessageReactionListUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = ""; }; D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = ""; }; D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = ""; }; @@ -1169,6 +1171,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D072F38423155EAF0009E66F /* MessageReactionListUI.framework in Frameworks */, D03E495D230868DF0049C28B /* PersistentStringHash.framework in Frameworks */, D03E493C2308679D0049C28B /* InstantPageCache.framework in Frameworks */, D03E4910230866280049C28B /* GridMessageSelectionNode.framework in Frameworks */, @@ -1773,6 +1776,7 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D072F38323155EAF0009E66F /* MessageReactionListUI.framework */, D03E495C230868DF0049C28B /* PersistentStringHash.framework */, D03E493B2308679D0049C28B /* InstantPageCache.framework */, D03E490F230866280049C28B /* GridMessageSelectionNode.framework */,