import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import PresentationDataUtils import AccountContext import ComponentFlow import ViewControllerComponent import MultilineTextComponent import BalancedTextComponent import ListSectionComponent import ListActionItemComponent import BundleIconComponent import LottieComponent import Markdown import LocationUI import TelegramStringFormatting import TimezoneSelectionScreen private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> IndexSet { let mappedRange = (range.lowerBound + dayIndexOffset * 24 * 60) ..< (range.upperBound + dayIndexOffset * 24 * 60) var result = IndexSet() if mappedRange.upperBound > 7 * 24 * 60 { if mappedRange.lowerBound < 7 * 24 * 60 { result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60) } result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60)) } else { result.insert(integersIn: mappedRange) } return result } private func getDayRanges(days: [BusinessHoursSetupScreenComponent.Day], index: Int) -> [BusinessHoursSetupScreenComponent.WorkingHourRange] { let day = days[index] if let ranges = day.ranges { if ranges.isEmpty { return [BusinessHoursSetupScreenComponent.WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)] } else { return ranges } } else { return [] } } final class BusinessHoursSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let initialValue: TelegramBusinessHours? let completion: (TelegramBusinessHours?) -> Void init( context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void ) { self.context = context self.initialValue = initialValue self.completion = completion } static func ==(lhs: BusinessHoursSetupScreenComponent, rhs: BusinessHoursSetupScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.initialValue != rhs.initialValue { return false } return true } private final class ScrollView: UIScrollView { override func touchesShouldCancel(in view: UIView) -> Bool { return true } } struct WorkingHourRange: Equatable { var id: Int var startMinute: Int var endMinute: Int init(id: Int, startMinute: Int, endMinute: Int) { self.id = id self.startMinute = startMinute self.endMinute = endMinute } } struct DayRangeIndex: Hashable { var day: Int var id: Int init(day: Int, id: Int) { self.day = day self.id = id } } struct Day: Equatable { var ranges: [WorkingHourRange]? init(ranges: [WorkingHourRange]?) { self.ranges = ranges } } struct DaysState: Equatable { enum ValidationError: Error { case intersectingRanges } var timezoneId: String private(set) var days: [Day] private(set) var intersectingRanges = Set() init(timezoneId: String, days: [Day]) { self.timezoneId = timezoneId self.days = days self.validate() } init(businessHours: TelegramBusinessHours) { self.timezoneId = businessHours.timezoneId self.days = businessHours.splitIntoWeekDays().map { day in switch day { case .closed: return Day(ranges: nil) case .open: return Day(ranges: []) case let .intervals(intervals): var nextIntervalId = 0 return Day(ranges: intervals.map { interval in let intervalId = nextIntervalId nextIntervalId += 1 return WorkingHourRange(id: intervalId, startMinute: interval.startMinute, endMinute: interval.endMinute) }) } } if let value = try? self.asBusinessHours() { if value != businessHours { assertionFailure("Inconsistent representation") } } self.validate() } mutating func validate() { self.intersectingRanges.removeAll() for dayIndex in 0 ..< self.days.count { var otherDaysMinutes = IndexSet() inner: for otherDayIndex in 0 ..< self.days.count { if dayIndex == otherDayIndex { continue inner } for range in getDayRanges(days: self.days, index: otherDayIndex) { otherDaysMinutes.formUnion(wrappedMinuteRange(range: range.startMinute ..< range.endMinute, dayIndexOffset: otherDayIndex)) } } let dayRanges = getDayRanges(days: self.days, index: dayIndex) for i in 0 ..< dayRanges.count { var currentDayOtherMinutes = IndexSet() inner: for j in 0 ..< dayRanges.count { if i == j { continue inner } currentDayOtherMinutes.formUnion(wrappedMinuteRange(range: dayRanges[j].startMinute ..< dayRanges[j].endMinute, dayIndexOffset: dayIndex)) } let currentDayIndices = wrappedMinuteRange(range: dayRanges[i].startMinute ..< dayRanges[i].endMinute, dayIndexOffset: dayIndex) if !otherDaysMinutes.intersection(currentDayIndices).isEmpty || !currentDayOtherMinutes.intersection(currentDayIndices).isEmpty { self.intersectingRanges.insert(DayRangeIndex(day: dayIndex, id: dayRanges[i].id)) } } } } mutating func update(days: [Day]) { self.days = days self.validate() } func asBusinessHours() throws -> TelegramBusinessHours { var mappedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = [] var filledMinutes = IndexSet() for i in 0 ..< self.days.count { let dayStartMinute = i * 24 * 60 guard var effectiveRanges = self.days[i].ranges else { continue } if effectiveRanges.isEmpty { effectiveRanges = [WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)] } for range in effectiveRanges { let minuteRange: Range = (dayStartMinute + range.startMinute) ..< (dayStartMinute + range.endMinute) let wrappedMinutes = wrappedMinuteRange(range: minuteRange) if !filledMinutes.intersection(wrappedMinutes).isEmpty { throw ValidationError.intersectingRanges } filledMinutes.formUnion(wrappedMinutes) mappedIntervals.append(TelegramBusinessHours.WorkingTimeInterval(startMinute: minuteRange.lowerBound, endMinute: minuteRange.upperBound)) } } var mergedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = [] for interval in mappedIntervals { if mergedIntervals.isEmpty { mergedIntervals.append(interval) } else { if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute { mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute) } else { mergedIntervals.append(interval) } } } return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mergedIntervals) } } final class View: UIView, UIScrollViewDelegate { private let topOverscrollLayer = SimpleLayer() private let scrollView: ScrollView private let navigationTitle = ComponentView() private let icon = ComponentView() private let subtitle = ComponentView() private let generalSection = ComponentView() private let daysSection = ComponentView() private let timezoneSection = ComponentView() private var ignoreScrolling: Bool = false private var isUpdating: Bool = false private var component: BusinessHoursSetupScreenComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private var showHours: Bool = false private var daysState = DaysState(timezoneId: "", days: []) private var timeZoneList: TimeZoneList? private var timezonesDisposable: Disposable? private var keepTimezonesUpdatedDisposable: Disposable? override init(frame: CGRect) { self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.scrollsToTop = false self.scrollView.delaysContentTouches = false self.scrollView.canCancelContentTouches = true self.scrollView.contentInsetAdjustmentBehavior = .never if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } self.scrollView.alwaysBounceVertical = true super.init(frame: frame) self.scrollView.delegate = self self.addSubview(self.scrollView) self.scrollView.layer.addSublayer(self.topOverscrollLayer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.timezonesDisposable?.dispose() self.keepTimezonesUpdatedDisposable?.dispose() } func scrollToTop() { self.scrollView.setContentOffset(CGPoint(), animated: true) } func attemptNavigation(complete: @escaping () -> Void) -> Bool { guard let component = self.component, let environment = self.environment else { return true } if self.showHours { do { let businessHours = try self.daysState.asBusinessHours() let _ = component.context.engine.accountData.updateAccountBusinessHours(businessHours: businessHours).startStandalone() return true } catch _ { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_Text, actions: [ TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: environment.strings.BusinessHoursSetup_ErrorIntersectingDays_ResetAction, action: { [weak self] in guard let self else { return } let _ = self complete() }) ]), in: .window(.root)) return false } } else { if component.initialValue != nil { let _ = component.context.engine.accountData.updateAccountBusinessHours(businessHours: nil).startStandalone() return true } else { return true } } } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } var scrolledUp = true private func updateScrolling(transition: ComponentTransition) { let navigationRevealOffsetY: CGFloat = 0.0 let navigationAlphaDistance: CGFloat = 16.0 let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) } var scrolledUp = false if navigationAlpha < 0.5 { scrolledUp = true } else if navigationAlpha > 0.5 { scrolledUp = false } if self.scrolledUp != scrolledUp { self.scrolledUp = scrolledUp if !self.isUpdating { self.state?.updated() } } if let navigationTitleView = self.navigationTitle.view { transition.setAlpha(view: navigationTitleView, alpha: 1.0) } } func update(component: BusinessHoursSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } if self.component == nil { if let initialValue = component.initialValue { self.showHours = true self.daysState = DaysState(businessHours: initialValue) } else { self.showHours = false self.daysState.timezoneId = TimeZone.current.identifier self.daysState.update(days: (0 ..< 7).map { _ in return Day(ranges: []) }) } self.timezonesDisposable = (component.context.engine.accountData.cachedTimeZoneList() |> deliverOnMainQueue).start(next: { [weak self] timeZoneList in guard let self else { return } self.timeZoneList = timeZoneList self.state?.updated(transition: .immediate) }) self.keepTimezonesUpdatedDisposable = component.context.engine.accountData.keepCachedTimeZoneListUpdated().startStrict() } let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment self.component = component self.state = state if themeUpdated { self.backgroundColor = environment.theme.list.blocksBackgroundColor } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let navigationTitleSize = self.navigationTitle.update( transition: transition, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.BusinessHoursSetup_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) if let navigationTitleView = self.navigationTitle.view { if navigationTitleView.superview == nil { if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { navigationBar.view.addSubview(navigationTitleView) } } transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) } let bottomContentInset: CGFloat = 24.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sectionSpacing: CGFloat = 30.0 let _ = bottomContentInset let _ = sectionSpacing var contentHeight: CGFloat = 0.0 contentHeight += environment.navigationHeight let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "BusinessHoursEmoji"), loop: false )), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) ) let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 10.0), size: iconSize) if let iconView = self.icon.view as? LottieComponent.View { if iconView.superview == nil { self.scrollView.addSubview(iconView) iconView.playOnce() } transition.setPosition(view: iconView, position: iconFrame.center) iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) } contentHeight += 126.0 let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.BusinessHoursSetup_Text, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), linkAttribute: { attributes in return ("URL", "") }), textAlignment: .center )) let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( text: .plain(subtitleString), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.25, highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") } else { return nil } }, tapAction: { [weak self] _, _ in guard let self, let component = self.component else { return } let _ = component } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) ) let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) if let subtitleView = self.subtitle.view { if subtitleView.superview == nil { self.scrollView.addSubview(subtitleView) } transition.setPosition(view: subtitleView, position: subtitleFrame.center) subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) } contentHeight += subtitleSize.height contentHeight += 27.0 let generalSectionSize = self.generalSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: nil, footer: nil, items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.BusinessHoursSetup_MainToggle, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.showHours, action: { [weak self] _ in guard let self else { return } self.showHours = !self.showHours self.state?.updated(transition: .spring(duration: 0.4)) })), action: nil ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize) if let generalSectionView = self.generalSection.view { if generalSectionView.superview == nil { self.scrollView.addSubview(generalSectionView) } transition.setFrame(view: generalSectionView, frame: generalSectionFrame) } contentHeight += generalSectionSize.height contentHeight += sectionSpacing var daysContentHeight: CGFloat = 0.0 var daysSectionItems: [AnyComponentWithIdentity] = [] for day in self.daysState.days { let dayIndex = daysSectionItems.count let title: String switch dayIndex { case 0: title = environment.strings.Weekday_Monday case 1: title = environment.strings.Weekday_Tuesday case 2: title = environment.strings.Weekday_Wednesday case 3: title = environment.strings.Weekday_Thursday case 4: title = environment.strings.Weekday_Friday case 5: title = environment.strings.Weekday_Saturday case 6: title = environment.strings.Weekday_Sunday default: title = " " } let subtitle = NSMutableAttributedString() var invalidIndices: [Int] = [] let effectiveDayRanges = getDayRanges(days: self.daysState.days, index: dayIndex) for range in effectiveDayRanges { if self.daysState.intersectingRanges.contains(DayRangeIndex(day: dayIndex, id: range.id)) { invalidIndices.append(range.id) } } let subtitleFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)) if let ranges = self.daysState.days[dayIndex].ranges { if ranges.isEmpty { subtitle.append(NSAttributedString(string: environment.strings.BusinessHoursSetup_DayOpen24h, font: subtitleFont, textColor: invalidIndices.contains(0) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor)) } else { for i in 0 ..< ranges.count { let range = ranges[i] let startHours = clipMinutes(range.startMinute) / 60 let startMinutes = clipMinutes(range.startMinute) % 60 let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: presentationData.dateTimeFormat) let endHours = clipMinutes(range.endMinute) / 60 let endMinutes = clipMinutes(range.endMinute) % 60 let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: presentationData.dateTimeFormat) var rangeString = "\(startText)\u{00a0}- \(endText)" if i != ranges.count - 1 { rangeString.append(", ") } subtitle.append(NSAttributedString(string: rangeString, font: subtitleFont, textColor: invalidIndices.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor)) } } } else { subtitle.append(NSAttributedString(string: environment.strings.BusinessHoursSetup_DayClosed, font: subtitleFont, textColor: environment.theme.list.itemAccentColor)) } daysSectionItems.append(AnyComponentWithIdentity(id: dayIndex, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: title, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(subtitle), maximumNumberOfLines: 20 ))) ], alignment: .left, spacing: 3.0)), contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 10.0, right: 0.0), accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: day.ranges != nil, action: { [weak self] _ in guard let self else { return } if dayIndex < self.daysState.days.count { var days = self.daysState.days if days[dayIndex].ranges == nil { days[dayIndex].ranges = [] } else { days[dayIndex].ranges = nil } self.daysState.update(days: days) } self.state?.updated(transition: .immediate) })), action: { [weak self] _ in guard let self, let component = self.component else { return } self.environment?.controller()?.push(BusinessDaySetupScreen( context: component.context, dayIndex: dayIndex, day: self.daysState.days[dayIndex], updateDay: { [weak self] day in guard let self else { return } if self.daysState.days[dayIndex] != day { var days = self.daysState.days days[dayIndex] = day self.daysState.update(days: days) self.state?.updated(transition: .immediate) } } )) } )))) } let daysSectionSize = self.daysSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.BusinessHoursSetup_DaysSectionTitle, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, items: daysSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: daysSectionSize) if let daysSectionView = self.daysSection.view { if daysSectionView.superview == nil { daysSectionView.layer.allowsGroupOpacity = true self.scrollView.addSubview(daysSectionView) } transition.setFrame(view: daysSectionView, frame: daysSectionFrame) let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) alphaTransition.setAlpha(view: daysSectionView, alpha: self.showHours ? 1.0 : 0.0) } daysContentHeight += daysSectionSize.height daysContentHeight += sectionSpacing let timezoneValueText: String if let timeZoneList = self.timeZoneList { if let item = timeZoneList.items.first(where: { $0.id == self.daysState.timezoneId }) { timezoneValueText = item.title } else { timezoneValueText = TimeZone(identifier: self.daysState.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? " " } } else { timezoneValueText = "..." } let timezoneSectionSize = self.timezoneSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: nil, footer: nil, items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.BusinessHoursSetup_TimeZone, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 )), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: timezoneValueText, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 1 )))), accessory: .arrow, action: { [weak self] _ in guard let self, let component = self.component else { return } var completed: ((String) -> Void)? let controller = TimezoneSelectionScreen(context: component.context, completed: { timezoneId in completed?(timezoneId) }) controller.navigationPresentation = .modal self.environment?.controller()?.push(controller) completed = { [weak self, weak controller] timezoneId in guard let self else { controller?.dismiss() return } self.daysState.timezoneId = timezoneId self.state?.updated(transition: .immediate) controller?.dismiss() } } ))) ] )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) ) let timezoneSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: timezoneSectionSize) if let timezoneSectionView = self.timezoneSection.view { if timezoneSectionView.superview == nil { self.scrollView.addSubview(timezoneSectionView) } transition.setFrame(view: timezoneSectionView, frame: timezoneSectionFrame) let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) alphaTransition.setAlpha(view: timezoneSectionView, alpha: self.showHours ? 1.0 : 0.0) } daysContentHeight += timezoneSectionSize.height if self.showHours { contentHeight += daysContentHeight } contentHeight += bottomContentInset contentHeight += environment.safeInsets.bottom self.ignoreScrolling = true let previousBounds = self.scrollView.bounds let contentSize = CGSize(width: availableSize.width, height: contentHeight) if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) } if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) if self.scrollView.scrollIndicatorInsets != scrollInsets { self.scrollView.scrollIndicatorInsets = scrollInsets } self.ignoreScrolling = false if !previousBounds.isEmpty, !transition.animation.isImmediate { let bounds = self.scrollView.bounds if bounds.maxY != previousBounds.maxY { let offsetY = previousBounds.maxY - bounds.maxY transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) } } self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) self.updateScrolling(transition: transition) return availableSize } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class BusinessHoursSetupScreen: ViewControllerComponentContainer { private let context: AccountContext public init(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) { self.context = context super.init(context: context, component: BusinessHoursSetupScreenComponent( context: context, initialValue: initialValue, completion: completion ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.title = "" self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? BusinessHoursSetupScreenComponent.View else { return } componentView.scrollToTop() } self.attemptNavigation = { [weak self] complete in guard let self, let componentView = self.node.hostView.componentView as? BusinessHoursSetupScreenComponent.View else { return true } return componentView.attemptNavigation(complete: complete) } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } @objc private func cancelPressed() { self.dismiss() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) } }