Reaction list

This commit is contained in:
Peter 2019-08-27 18:36:11 +04:00
parent 2442653541
commit 6784f1bc02
28 changed files with 1901 additions and 197 deletions

View File

@ -551,6 +551,9 @@
<FileRef
location = "group:submodules/SettingsUI/SettingsUI_Xcode.xcodeproj">
</FileRef>
<FileRef
location = "group:/Users/peter/build/telegram-temp/telegram-ios/submodules/MessageReactionListUI/MessageReactionListUI_Xcode.xcodeproj">
</FileRef>
</Group>
<Group
location = "container:"

View File

@ -513,6 +513,7 @@ public func createPollController(context: AccountContext, peerId: PeerId, comple
}
}
controller.isOpaqueWhenInOverlay = true
controller.isModalWhenInOverlay = true
controller.blocksBackgroundWhenInOverlay = true
controller.experimentalSnapScrollToItem = true

View File

@ -11,14 +11,17 @@ public protocol ContainableController: class {
var displayNode: ASDisplayNode { get }
var isViewLoaded: Bool { get }
var isOpaqueWhenInOverlay: Bool { get }
var isModalWhenInOverlay: Bool { get }
var blocksBackgroundWhenInOverlay: Bool { get }
var ready: Promise<Bool> { 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)

View File

@ -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<Bool>(true)
open var ready: Promise<Bool> {
@ -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 {

View File

@ -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
}
}

View File

@ -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<Bool, NoError> { get }
func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool)
}

View File

@ -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)

View File

@ -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) {

View File

@ -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)
}

View File

@ -230,6 +230,7 @@ open class ItemListController<Entry: ItemListNodeEntry>: 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<Entry: ItemListNodeEntry>: ViewController, KeyShor
presentationArguments.completion?()
completion()
})
self.updateTransitionWhenPresentedAsModal?(1.0, .animated(duration: 0.5, curve: .spring))
} else {
completion()
}
@ -506,6 +508,7 @@ open class ItemListController<Entry: ItemListNodeEntry>: ViewController, KeyShor
if !self.isDismissed {
self.isDismissed = true
(self.displayNode as! ItemListControllerNode<Entry>).animateOut(completion: completion)
self.updateTransitionWhenPresentedAsModal?(0.0, .animated(duration: 0.2, curve: .easeInOut))
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -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 = "<group>"; };
D072F36723154C230009E66F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
};
D072F36423154C230009E66F /* Products */ = {
isa = PBXGroup;
children = (
D072F36323154C230009E66F /* MessageReactionListUI.framework */,
);
name = Products;
sourceTree = "<group>";
};
D072F36523154C230009E66F /* Sources */ = {
isa = PBXGroup;
children = (
D072F36623154C230009E66F /* MessageReactionListUI.h */,
D072F37F23154E390009E66F /* MessageReactionListController.swift */,
D072F38523156B450009E66F /* MessageReactionCategoryNode.swift */,
);
path = Sources;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 */;
}

View File

@ -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
}
}

View File

@ -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<Bool>()
override public var ready: Promise<Bool> {
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<Bool>()
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)
}
}

View File

@ -0,0 +1,19 @@
//
// MessageReactionListUI.h
// MessageReactionListUI
//
// Created by Peter on 8/27/19.
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
//
#import <UIKit/UIKit.h>
//! 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 <MessageReactionListUI/PublicHeader.h>

View File

@ -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<MessageReactionListCategoryState>
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<Api.MessageReactionsList, LoadReactionsError> 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<MessageReactionListState>()
public var state: Signal<MessageReactionListState, NoError> {
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()
}
}

View File

@ -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 = "<group>"; };
D07047B61F3DF2CD00F6A8D4 /* ManagedConsumePersonalMessagesActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedConsumePersonalMessagesActions.swift; sourceTree = "<group>"; };
D07047B91F3DF75500F6A8D4 /* ConsumePersonalMessageAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsumePersonalMessageAction.swift; sourceTree = "<group>"; };
D072F356231542740009E66F /* MessageReactionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReactionList.swift; sourceTree = "<group>"; };
D073CE5C1DCB97F6007511FD /* ForwardSourceInfoAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardSourceInfoAttribute.swift; sourceTree = "<group>"; };
D073CE5F1DCB9D14007511FD /* OutgoingMessageInfoAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingMessageInfoAttribute.swift; sourceTree = "<group>"; };
D0750C8F22B2FD8300BE5F6E /* PeerAccessHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerAccessHash.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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")

View File

@ -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))
}
}
}

View File

@ -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<MessageId>()
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,

View File

@ -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?()
})
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -411,6 +411,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _ in
}, updateMessageReaction: { _, _ in
}, openMessageReactions: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -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))

View File

@ -287,6 +287,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _ in
}, updateMessageReaction: { _, _ in
}, openMessageReactions: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -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 = "<group>"; };
D06E0F8D1F79ABFB003CF3DD /* ChatLoadingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLoadingNode.swift; sourceTree = "<group>"; };
D06F1EA31F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHistorySearchContainerNode.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = "<group>"; };
D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = "<group>"; };
@ -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 */,