import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import AccountContext import TelegramPresentationData import ComponentFlow import PhotoResources private final class MediaPreviewView: UIView { private let context: AccountContext private let message: EngineMessage private let media: EngineMedia private let imageView: TransformImageView private var requestedImage: Bool = false private var disposable: Disposable? init(context: AccountContext, message: EngineMessage, media: EngineMedia) { self.context = context self.message = message self.media = media self.imageView = TransformImageView() super.init(frame: CGRect()) self.addSubview(self.imageView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.disposable?.dispose() } func updateLayout(size: CGSize, synchronousLoads: Bool) { var dimensions = CGSize(width: 100.0, height: 100.0) if case let .image(image) = self.media { if let largest = largestImageRepresentation(image.representations) { dimensions = largest.dimensions.cgSize if !self.requestedImage { self.requestedImage = true let signal = mediaGridMessagePhoto(account: self.context.account, photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads) self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads) } } } else if case let .file(file) = self.media { if let mediaDimensions = file.dimensions { dimensions = mediaDimensions.cgSize if !self.requestedImage { self.requestedImage = true let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, videoReference: .message(message: MessageReference(self.message._asMessage()), media: file), synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, useMiniThumbnailIfAvailable: true) self.imageView.setSignal(signal, attemptSynchronously: synchronousLoads) } } } let makeLayout = self.imageView.asyncLayout() self.imageView.frame = CGRect(origin: CGPoint(), size: size) let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: size.width / 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() } } private func monthName(index: Int, strings: PresentationStrings) -> String { switch index { case 0: return strings.Month_GenJanuary case 1: return strings.Month_GenFebruary case 2: return strings.Month_GenMarch case 3: return strings.Month_GenApril case 4: return strings.Month_GenMay case 5: return strings.Month_GenJune case 6: return strings.Month_GenJuly case 7: return strings.Month_GenAugust case 8: return strings.Month_GenSeptember case 9: return strings.Month_GenOctober case 10: return strings.Month_GenNovember case 11: return strings.Month_GenDecember default: return "" } } private func dayName(index: Int, strings: PresentationStrings) -> String { let _ = strings //TODO:localize switch index { case 0: return "M" case 1: return "T" case 2: return "W" case 3: return "T" case 4: return "F" case 5: return "S" case 6: return "S" default: return "" } } private class Scroller: UIScrollView { override init(frame: CGRect) { super.init(frame: frame) if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.contentInsetAdjustmentBehavior = .never } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func touchesShouldCancel(in view: UIView) -> Bool { return true } @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } } private final class ImageCache: Equatable { static func ==(lhs: ImageCache, rhs: ImageCache) -> Bool { return lhs === rhs } private struct FilledCircle: Hashable { var diameter: CGFloat var color: UInt32 } private struct Text: Hashable { var fontSize: CGFloat var isSemibold: Bool var color: UInt32 var string: String } private var items: [AnyHashable: UIImage] = [:] func filledCircle(diameter: CGFloat, color: UIColor) -> UIImage { let key = AnyHashable(FilledCircle(diameter: diameter, color: color.argb)) if let image = self.items[key] { return image } let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) })!.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) self.items[key] = image return image } func text(fontSize: CGFloat, isSemibold: Bool, color: UIColor, string: String) -> UIImage { let key = AnyHashable(Text(fontSize: fontSize, isSemibold: isSemibold, color: color.argb, string: string)) if let image = self.items[key] { return image } let font: UIFont if isSemibold { font = Font.semibold(fontSize) } else { font = Font.regular(fontSize) } let attributedString = NSAttributedString(string: string, font: font, textColor: color) let rect = attributedString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), options: .usesLineFragmentOrigin, context: nil) let image = generateImage(CGSize(width: ceil(rect.width), height: ceil(rect.height)), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) attributedString.draw(in: rect) UIGraphicsPopContext() })! self.items[key] = image return image } } private final class DayComponent: Component { typealias EnvironmentType = ImageCache let title: String let isCurrent: Bool let isEnabled: Bool let theme: PresentationTheme let context: AccountContext let media: DayMedia? let action: () -> Void init( title: String, isCurrent: Bool, isEnabled: Bool, theme: PresentationTheme, context: AccountContext, media: DayMedia?, action: @escaping () -> Void ) { self.title = title self.isCurrent = isCurrent self.isEnabled = isEnabled self.theme = theme self.context = context self.media = media self.action = action } static func ==(lhs: DayComponent, rhs: DayComponent) -> Bool { if lhs.title != rhs.title { return false } if lhs.isCurrent != rhs.isCurrent { return false } if lhs.isEnabled != rhs.isEnabled { return false } if lhs.theme !== rhs.theme { return false } if lhs.context !== rhs.context { return false } if lhs.media != rhs.media { return false } return true } final class View: UIView { private let button: HighlightableButton private let highlightView: UIImageView private let titleView: UIImageView private var mediaPreviewView: MediaPreviewView? private var action: (() -> Void)? private var currentMedia: DayMedia? init() { self.button = HighlightableButton() self.highlightView = UIImageView() self.titleView = UIImageView() super.init(frame: CGRect()) self.button.addSubview(self.highlightView) self.button.addSubview(self.titleView) self.addSubview(self.button) self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } @objc private func pressed() { self.action?() } func update(component: DayComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.action = component.action let diameter = min(availableSize.width, availableSize.height) let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: floor((availableSize.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) let imageCache = environment[ImageCache.self] if component.media != nil { self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: UIColor(white: 0.0, alpha: 0.2)) } else if component.isCurrent { self.highlightView.image = imageCache.value.filledCircle(diameter: diameter, color: component.theme.list.itemAccentColor) } else { self.highlightView.image = nil } if self.currentMedia != component.media { self.currentMedia = component.media if let mediaPreviewView = self.mediaPreviewView { self.mediaPreviewView = nil mediaPreviewView.removeFromSuperview() } if let media = component.media { let mediaPreviewView = MediaPreviewView(context: component.context, message: media.message, media: media.media) self.mediaPreviewView = mediaPreviewView self.button.insertSubview(mediaPreviewView, belowSubview: self.highlightView) } } let titleColor: UIColor let titleFontSize: CGFloat let titleFontIsSemibold: Bool if component.isCurrent || component.media != nil { titleColor = component.theme.list.itemCheckColors.foregroundColor titleFontSize = 17.0 titleFontIsSemibold = true } else if component.isEnabled { titleColor = component.theme.list.itemPrimaryTextColor titleFontSize = 17.0 titleFontIsSemibold = false } else { titleColor = component.theme.list.itemDisabledTextColor titleFontSize = 17.0 titleFontIsSemibold = false } let titleImage = imageCache.value.text(fontSize: titleFontSize, isSemibold: titleFontIsSemibold, color: titleColor, string: component.title) self.titleView.image = titleImage let titleSize = titleImage.size transition.setFrame(view: self.highlightView, frame: contentFrame) self.titleView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) self.button.frame = CGRect(origin: CGPoint(), size: availableSize) self.button.isEnabled = component.isEnabled && component.media != nil if let mediaPreviewView = self.mediaPreviewView { mediaPreviewView.frame = contentFrame mediaPreviewView.updateLayout(size: contentFrame.size, synchronousLoads: false) } return availableSize } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } private final class MonthComponent: CombinedComponent { typealias EnvironmentType = ImageCache let context: AccountContext let model: MonthModel let foregroundColor: UIColor let strings: PresentationStrings let theme: PresentationTheme let navigateToDay: (Int32) -> Void init( context: AccountContext, model: MonthModel, foregroundColor: UIColor, strings: PresentationStrings, theme: PresentationTheme, navigateToDay: @escaping (Int32) -> Void ) { self.context = context self.model = model self.foregroundColor = foregroundColor self.strings = strings self.theme = theme self.navigateToDay = navigateToDay } static func ==(lhs: MonthComponent, rhs: MonthComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.model != rhs.model { return false } if lhs.foregroundColor != rhs.foregroundColor { return false } if lhs.strings !== rhs.strings { return false } if lhs.theme !== rhs.theme { return false } return true } static var body: Body { let title = Child(Text.self) let weekdayTitles = ChildMap(environment: Empty.self, keyedBy: Int.self) let days = ChildMap(environment: ImageCache.self, keyedBy: Int.self) return { context in let sideInset: CGFloat = 14.0 let titleWeekdaysSpacing: CGFloat = 18.0 let weekdayDaySpacing: CGFloat = 14.0 let weekdaySize: CGFloat = 46.0 let weekdaySpacing: CGFloat = 6.0 let usableWeekdayWidth = floor((context.availableSize.width - sideInset * 2.0 - weekdaySpacing * 6.0) / 7.0) let weekdayWidth = floor((context.availableSize.width - sideInset * 2.0) / 7.0) let title = title.update( component: Text( text: "\(monthName(index: context.component.model.index - 1, strings: context.component.strings)) \(context.component.model.year)", font: Font.semibold(17.0), color: .black ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), transition: .immediate ) let updatedWeekdayTitles = (0 ..< 7).map { index in return weekdayTitles[index].update( component: AnyComponent(Text( text: dayName(index: index, strings: context.component.strings), font: Font.regular(10.0), color: .black )), availableSize: CGSize(width: 100.0, height: 100.0), transition: .immediate ) } let updatedDays = (0 ..< context.component.model.numberOfDays).map { index -> _UpdatedChildComponent in let dayOfMonth = index + 1 let isCurrent = context.component.model.currentYear == context.component.model.year && context.component.model.currentMonth == context.component.model.index && context.component.model.currentDayOfMonth == dayOfMonth var isEnabled = true if context.component.model.currentYear == context.component.model.year { if context.component.model.currentMonth == context.component.model.index { if dayOfMonth > context.component.model.currentDayOfMonth { isEnabled = false } } else if context.component.model.index > context.component.model.currentMonth { isEnabled = false } } else if context.component.model.year > context.component.model.currentYear { isEnabled = false } let dayTimestamp = Int32(context.component.model.firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(index) let navigateToDay = context.component.navigateToDay return days[index].update( component: AnyComponent(DayComponent( title: "\(dayOfMonth)", isCurrent: isCurrent, isEnabled: isEnabled, theme: context.component.theme, context: context.component.context, media: context.component.model.mediaByDay[index], action: { navigateToDay(dayTimestamp) } )), environment: { context.environment[ImageCache.self] }, availableSize: CGSize(width: usableWeekdayWidth, height: weekdaySize), transition: .immediate ) } let titleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - title.size.width) / 2.0), y: 0.0), size: title.size) context.add(title .position(CGPoint(x: titleFrame.midX, y: titleFrame.midY)) ) let baseWeekdayTitleY = titleFrame.maxY + titleWeekdaysSpacing var maxWeekdayY = baseWeekdayTitleY for i in 0 ..< updatedWeekdayTitles.count { let weekdaySize = updatedWeekdayTitles[i].size let weekdayFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * weekdayWidth + floor((weekdayWidth - weekdaySize.width) / 2.0), y: baseWeekdayTitleY), size: weekdaySize) maxWeekdayY = max(maxWeekdayY, weekdayFrame.maxY) context.add(updatedWeekdayTitles[i] .position(CGPoint(x: weekdayFrame.midX, y: weekdayFrame.midY)) ) } let baseDayY = maxWeekdayY + weekdayDaySpacing var maxDayY = baseDayY for i in 0 ..< updatedDays.count { let gridIndex = (context.component.model.firstDayWeekday - 1) + i let gridX = sideInset + CGFloat(gridIndex % 7) * weekdayWidth let gridY = baseDayY + CGFloat(gridIndex / 7) * (weekdaySize + weekdaySpacing) let dayItemSize = updatedDays[i].size let dayFrame = CGRect(origin: CGPoint(x: gridX + floor((weekdayWidth - dayItemSize.width) / 2.0), y: gridY + floor((weekdaySize - dayItemSize.height) / 2.0)), size: dayItemSize) maxDayY = max(maxDayY, gridY + weekdaySize) context.add(updatedDays[i] .position(CGPoint(x: dayFrame.midX, y: dayFrame.midY)) ) } return CGSize(width: context.availableSize.width, height: maxDayY) } } } private struct DayMedia: Equatable { var message: EngineMessage var media: EngineMedia static func ==(lhs: DayMedia, rhs: DayMedia) -> Bool { if lhs.message.id != rhs.message.id { return false } return true } } private struct MonthModel: Equatable { var year: Int var index: Int var numberOfDays: Int var firstDay: Date var firstDayWeekday: Int var currentYear: Int var currentMonth: Int var currentDayOfMonth: Int var mediaByDay: [Int: DayMedia] init( year: Int, index: Int, numberOfDays: Int, firstDay: Date, firstDayWeekday: Int, currentYear: Int, currentMonth: Int, currentDayOfMonth: Int, mediaByDay: [Int: DayMedia] ) { self.year = year self.index = index self.numberOfDays = numberOfDays self.firstDay = firstDay self.firstDayWeekday = firstDayWeekday self.currentYear = currentYear self.currentMonth = currentMonth self.currentDayOfMonth = currentDayOfMonth self.mediaByDay = mediaByDay } } private func monthMetadata(calendar: Calendar, for baseDate: Date, currentYear: Int, currentMonth: Int, currentDayOfMonth: Int) -> MonthModel? { guard let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: baseDate)?.count, let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else { return nil } let year = calendar.component(.year, from: firstDayOfMonth) let month = calendar.component(.month, from: firstDayOfMonth) let firstDayWeekday = calendar.component(.weekday, from: firstDayOfMonth) return MonthModel( year: year, index: month, numberOfDays: numberOfDaysInMonth, firstDay: firstDayOfMonth, firstDayWeekday: firstDayWeekday, currentYear: currentYear, currentMonth: currentMonth, currentDayOfMonth: currentDayOfMonth, mediaByDay: [:] ) } public final class CalendarMessageScreen: ViewController { private final class Node: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private let peerId: PeerId private let navigateToDay: (Int32) -> Void private var presentationData: PresentationData private var scrollView: Scroller private var initialMonthIndex: Int = 0 private var months: [MonthModel] = [] private var monthViews: [Int: ComponentHostView] = [:] private let imageCache = ImageCache() private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? private var scrollLayout: (width: CGFloat, contentHeight: CGFloat, frames: [Int: CGRect])? init(context: AccountContext, peerId: PeerId, initialTimestamp: Int32, navigateToDay: @escaping (Int32) -> Void) { self.context = context self.peerId = peerId self.navigateToDay = navigateToDay self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.scrollView = Scroller() self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.scrollsToTop = false self.scrollView.delaysContentTouches = false self.scrollView.canCancelContentTouches = true if #available(iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } self.scrollView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.scrollView.disablesInteractiveModalDismiss = true super.init() let calendar = Calendar(identifier: .gregorian) let baseDate = Date() let currentYear = calendar.component(.year, from: baseDate) let currentMonth = calendar.component(.month, from: baseDate) let currentDayOfMonth = calendar.component(.day, from: baseDate) let initialDate = Date(timeIntervalSince1970: TimeInterval(initialTimestamp)) for i in 0 ..< 12 * 20 { guard let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: baseDate)) else { break } guard let monthBaseDate = calendar.date(byAdding: .month, value: -i, to: firstDayOfMonth) else { break } guard let monthModel = monthMetadata(calendar: calendar, for: monthBaseDate, currentYear: currentYear, currentMonth: currentMonth, currentDayOfMonth: currentDayOfMonth) else { break } if monthModel.year < 2013 { break } if monthModel.year == 2013 { if monthModel.index < 8 { break } } self.months.append(monthModel) } if self.months.count > 1 { for i in 0 ..< self.months.count - 1 { if initialDate >= self.months[i].firstDay { self.initialMonthIndex = i break } } } self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.scrollView.delegate = self self.view.addSubview(self.scrollView) self.reloadMediaInfo() } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.validLayout == nil self.validLayout = (layout, navigationHeight) if self.updateScrollLayoutIfNeeded() { } if isFirstLayout, let frame = self.scrollLayout?.frames[self.initialMonthIndex] { var contentOffset = floor(frame.midY - self.scrollView.bounds.height / 2.0) if contentOffset < 0 { contentOffset = 0 } if contentOffset > self.scrollView.contentSize.height - self.scrollView.bounds.height { contentOffset = self.scrollView.contentSize.height - self.scrollView.bounds.height } self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) } updateMonthViews() } func scrollViewDidScroll(_ scrollView: UIScrollView) { if let indicator = scrollView.value(forKey: "_verticalScrollIndicator") as? UIView { indicator.transform = CGAffineTransform(scaleX: -1.0, y: 1.0) } self.updateMonthViews() } func updateScrollLayoutIfNeeded() -> Bool { guard let (layout, navigationHeight) = self.validLayout else { return false } if self.scrollLayout?.width == layout.size.width { return false } var contentHeight: CGFloat = layout.intrinsicInsets.bottom var frames: [Int: CGRect] = [:] let measureView = ComponentHostView() let imageCache = ImageCache() for i in 0 ..< self.months.count { let monthSize = measureView.update( transition: .immediate, component: AnyComponent(MonthComponent( context: self.context, model: self.months[i], foregroundColor: .black, strings: self.presentationData.strings, theme: self.presentationData.theme, navigateToDay: { _ in } )), environment: { imageCache }, containerSize: CGSize(width: layout.size.width, height: 10000.0 )) let monthFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: monthSize) contentHeight += monthSize.height if i != self.months.count { contentHeight += 16.0 } frames[i] = monthFrame } self.scrollLayout = (layout.size.width, contentHeight, frames) self.scrollView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationHeight)) self.scrollView.contentSize = CGSize(width: layout.size.width, height: contentHeight) self.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: layout.intrinsicInsets.bottom, left: 0.0, bottom: 0.0, right: layout.size.width - 3.0 - 6.0) return true } func updateMonthViews() { guard let (width, _, frames) = self.scrollLayout else { return } let visibleRect = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0) var validMonths = Set() for i in 0 ..< self.months.count { guard let monthFrame = frames[i] else { continue } if !visibleRect.intersects(monthFrame) { continue } validMonths.insert(i) let monthView: ComponentHostView if let current = self.monthViews[i] { monthView = current } else { monthView = ComponentHostView() monthView.layer.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.monthViews[i] = monthView self.scrollView.addSubview(monthView) } let _ = monthView.update( transition: .immediate, component: AnyComponent(MonthComponent( context: self.context, model: self.months[i], foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor, strings: self.presentationData.strings, theme: self.presentationData.theme, navigateToDay: { [weak self] timestamp in guard let strongSelf = self else { return } strongSelf.navigateToDay(timestamp) } )), environment: { self.imageCache }, containerSize: CGSize(width: width, height: 10000.0 )) monthView.frame = monthFrame } var removeMonths: [Int] = [] for (index, view) in self.monthViews { if !validMonths.contains(index) { view.removeFromSuperview() removeMonths.append(index) } } for index in removeMonths { self.monthViews.removeValue(forKey: index) } } private func reloadMediaInfo() { let peerId = self.peerId let months = self.months let _ = (self.context.account.postbox.transaction { transaction -> [Int: [Int: DayMedia]] in var updatedMedia: [Int: [Int: DayMedia]] = [:] for i in 0 ..< months.count { for day in 0 ..< months[i].numberOfDays { let dayTimestamp = Int32(months[i].firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(day) let nextDayTimestamp = Int32(months[i].firstDay.timeIntervalSince1970) + 24 * 60 * 60 * Int32(day - 1) if let message = transaction.firstMessageInRange(peerId: peerId, namespace: Namespaces.Message.Cloud, tag: .photoOrVideo, timestampMax: dayTimestamp, timestampMin: nextDayTimestamp - 1) { /*if message.timestamp < nextDayTimestamp { continue }*/ if updatedMedia[i] == nil { updatedMedia[i] = [:] } mediaLoop: for media in message.media { switch media { case _ as TelegramMediaImage, _ as TelegramMediaFile: updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media)) break mediaLoop default: break } } } } } return updatedMedia } |> deliverOnMainQueue).start(next: { [weak self] updatedMedia in guard let strongSelf = self else { return } for (monthIndex, mediaByDay) in updatedMedia { strongSelf.months[monthIndex].mediaByDay = mediaByDay } strongSelf.updateMonthViews() }) } } private var node: Node { return self.displayNode as! Node } private let context: AccountContext private let peerId: PeerId private let initialTimestamp: Int32 private let navigateToDay: (CalendarMessageScreen, Int32) -> Void public init(context: AccountContext, peerId: PeerId, initialTimestamp: Int32, navigateToDay: @escaping (CalendarMessageScreen, Int32) -> Void) { self.context = context self.peerId = peerId self.initialTimestamp = initialTimestamp self.navigateToDay = navigateToDay let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) self.navigationPresentation = .modal self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false) //TODO:localize self.navigationItem.setTitle("Calendar", animated: false) } required public init(coder aDecoder: NSCoder) { preconditionFailure() } @objc private func dismissPressed() { self.dismiss() } override public func loadDisplayNode() { self.displayNode = Node(context: self.context, peerId: self.peerId, initialTimestamp: self.initialTimestamp, navigateToDay: { [weak self] timestamp in guard let strongSelf = self else { return } strongSelf.navigateToDay(strongSelf, timestamp) }) self.displayNodeDidLoad() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.node.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } }