import Foundation import UIKit import Display import ComponentFlow import TelegramCore import ViewControllerComponent import TelegramPresentationData import TelegramStringFormatting import AccountContext import SheetComponent import ButtonComponent import PlainButtonComponent import BundleIconComponent import GlassBackgroundComponent import GlassBarButtonComponent import DatePickerNode import UndoUI private let calendar = Calendar(identifier: .gregorian) private final class ChatScheduleTimeSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let mode: ChatScheduleTimeScreen.Mode let dismiss: () -> Void init( context: AccountContext, mode: ChatScheduleTimeScreen.Mode, dismiss: @escaping () -> Void ) { self.context = context self.mode = mode self.dismiss = dismiss } static func ==(lhs: ChatScheduleTimeSheetContentComponent, rhs: ChatScheduleTimeSheetContentComponent) -> Bool { return true } final class View: UIView { private let cancel = ComponentView() private let title = ComponentView() private let button = ComponentView() private var datePicker: DatePickerNode? private let topSeparator = SimpleLayer() private let timeTitle = ComponentView() private let timeValue = ComponentView() private let bottomSeparator = SimpleLayer() private let repeatTitle = ComponentView() private let repeatValue = ComponentView() private let timePicker = ComponentView() private let repeatPicker = ComponentView() private var component: ChatScheduleTimeSheetContentComponent? private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private var date: Date? private var minDate: Date? private var maxDate: Date? private var isPickingTime = false private var isPickingRepeatPeriod = false private var repeatPeriod: Int32? private let dateFormatter: DateFormatter override init(frame: CGRect) { self.dateFormatter = DateFormatter() self.dateFormatter.timeStyle = .none self.dateFormatter.dateStyle = .short self.dateFormatter.timeZone = TimeZone.current super.init(frame: frame) self.layer.addSublayer(self.topSeparator) self.layer.addSublayer(self.bottomSeparator) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func updateMinimumDate(currentTime: Int32? = nil) { let timeZone = TimeZone(secondsFromGMT: 0)! var calendar = Calendar(identifier: .gregorian) calendar.timeZone = timeZone let currentDate = Date() var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: currentDate) components.second = 0 let minute = (components.minute ?? 0) % 5 let next1MinDate = calendar.date(byAdding: .minute, value: 1, to: calendar.date(from: components)!) let next5MinDate = calendar.date(byAdding: .minute, value: 5 - minute, to: calendar.date(from: components)!) if let date = calendar.date(byAdding: .day, value: 365, to: currentDate) { self.maxDate = date } if let next1MinDate = next1MinDate, let next5MinDate = next5MinDate { let minimalTime: Double = 0 //self.minimalTime.flatMap(Double.init) ?? 0.0 self.minDate = max(next1MinDate, Date(timeIntervalSince1970: minimalTime)) if let currentTime = currentTime, Double(currentTime) > max(currentDate.timeIntervalSince1970, minimalTime) { self.date = Date(timeIntervalSince1970: Double(currentTime)) } else { self.date = next5MinDate } } } func update(component: ChatScheduleTimeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[EnvironmentType.self].value self.environment = environment if self.component == nil { self.updateMinimumDate(currentTime: nil) } self.component = component self.state = state let sideInset: CGFloat = 39.0 var contentHeight: CGFloat = 0.0 contentHeight += 30.0 let barButtonSize = CGSize(width: 40.0, height: 40.0) let cancelSize = self.cancel.update( transition: transition, component: AnyComponent( GlassBarButtonComponent( size: barButtonSize, backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, isDark: environment.theme.overallDarkAppearance, state: .generic, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor ) )), action: { [weak self] _ in guard let self, let component = self.component else { return } component.dismiss() } ) ), environment: {}, containerSize: barButtonSize ) let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) if let cancelView = self.cancel.view { if cancelView.superview == nil { self.addSubview(cancelView) } transition.setFrame(view: cancelView, frame: cancelFrame) } let title: String switch component.mode { case .scheduledMessages: title = environment.strings.Conversation_ScheduleMessage_Title case .reminders: title = environment.strings.Conversation_SetReminder_Title } let titleSize = self.title.update( transition: transition, component: AnyComponent( Text(text: title, font: Font.semibold(17.0), color: environment.theme.actionSheet.primaryTextColor) ), environment: {}, containerSize: availableSize ) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 27.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } contentHeight += 62.0 let datePicker: DatePickerNode if let current = self.datePicker { datePicker = current } else { datePicker = DatePickerNode( theme: DatePickerTheme(theme: environment.theme), strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, hasValueRow: false ) datePicker.date = self.date datePicker.valueUpdated = { [weak self] date in if let self { self.date = date self.state?.updated() } } self.addSubview(datePicker.view) self.datePicker = datePicker } datePicker.displayDateSelection = true if let minDate = self.minDate { datePicker.minimumDate = minDate } else { datePicker.minimumDate = Date() } if let maxDate = self.maxDate { datePicker.maximumDate = maxDate } let constrainedWidth = min(390.0, availableSize.width) let cellSize = floor((constrainedWidth - 12.0 * 2.0) / 7.0) let pickerHeight = 59.0 + cellSize * 5.0 let datePickerSize = CGSize(width: availableSize.width - 22.0, height: pickerHeight) datePicker.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - datePickerSize.width) / 2.0), y: contentHeight), size: datePickerSize) datePicker.updateLayout(size: datePickerSize, transition: .immediate) contentHeight += pickerHeight self.topSeparator.frame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel)) self.topSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor let timeTitleSize = self.timeTitle.update( transition: transition, component: AnyComponent( Text(text: "Time", font: Font.regular(17.0), color: environment.theme.actionSheet.primaryTextColor) ), environment: {}, containerSize: availableSize ) let timeTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 16.0), size: timeTitleSize) if let timeTitleView = self.timeTitle.view { if timeTitleView.superview == nil { self.addSubview(timeTitleView) } transition.setFrame(view: timeTitleView, frame: timeTitleFrame) } let date = self.date ?? Date() var t: time_t = Int(date.timeIntervalSince1970) var timeinfo = tm() localtime_r(&t, &timeinfo); let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: environment.dateTimeFormat) let timeValueSize = self.timeValue.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( ButtonContentComponent( theme: environment.theme, text: timeString, isActive: self.isPickingTime, isLocked: false ) ), action: { [weak self] in guard let self else { return } if self.isPickingRepeatPeriod { self.isPickingRepeatPeriod = false } else { self.isPickingTime = !self.isPickingTime } self.state?.updated() }, animateScale: false ) ), environment: { }, containerSize: availableSize ) let timeValueFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - timeValueSize.width, y: contentHeight + 10.0), size: timeValueSize) if let timeValueView = self.timeValue.view { if timeValueView.superview == nil { self.addSubview(timeValueView) } transition.setFrame(view: timeValueView, frame: timeValueFrame) } if self.isPickingTime { let timePickerSize = self.timePicker.update( transition: transition, component: AnyComponent( MenuComponent(component: AnyComponent(TimeMenuComponent( value: self.date ?? Date(), valueUpdated: { [weak self] value in guard let self else { return } self.date = value self.state?.updated() } ))) ), environment: {}, containerSize: availableSize ) let timePickerFrame = CGRect(origin: CGPoint(x: timeValueFrame.maxX - timePickerSize.width + 80.0, y: timeValueFrame.minY - 20.0 - timePickerSize.height + 80.0), size: timePickerSize) if let timePickerView = self.timePicker.view as? MenuComponent.View { if timePickerView.superview == nil { self.addSubview(timePickerView) timePickerView.animateIn() } transition.setFrame(view: timePickerView, frame: timePickerFrame) } } else if let timePicker = self.timePicker.view as? MenuComponent.View, timePicker.superview != nil { timePicker.animateOut(completion: { timePicker.removeFromSuperview() }) } contentHeight += 56.0 self.bottomSeparator.frame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel)) self.bottomSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor let repeatTitleSize = self.repeatTitle.update( transition: transition, component: AnyComponent( Text(text: "Repeat", font: Font.regular(17.0), color: environment.theme.actionSheet.primaryTextColor) ), environment: {}, containerSize: availableSize ) let repeatTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 16.0), size: repeatTitleSize) if let timeTitleView = self.repeatTitle.view { if timeTitleView.superview == nil { self.addSubview(timeTitleView) } transition.setFrame(view: timeTitleView, frame: repeatTitleFrame) } let repeatString: String if let repeatPeriod = self.repeatPeriod { switch repeatPeriod { case 60: repeatString = "Every Minute" case 300: repeatString = "Every 5 Minutes" case 86400: repeatString = "Daily" case 7 * 86400: repeatString = "Weekly" case 14 * 86400: repeatString = "Biweekly" case 30 * 86400: repeatString = "Monthly" case 91 * 86400: repeatString = "Every 3 Months" case 182 * 86400: repeatString = "Every 6 Months" case 365 * 86400: repeatString = "Yearly" default: repeatString = "\(repeatPeriod)" } } else { repeatString = "Never" } let repeatValueSize = self.repeatValue.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( ButtonContentComponent( theme: environment.theme, text: repeatString, isActive: self.isPickingRepeatPeriod, isLocked: !component.context.isPremium ) ), action: { [weak self] in guard let self else { return } if self.isPickingTime { self.isPickingTime = false } else { self.isPickingRepeatPeriod = !self.isPickingRepeatPeriod } self.state?.updated() } ) ), environment: { }, containerSize: availableSize ) let repeatValueFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - repeatValueSize.width, y: contentHeight + 10.0), size: repeatValueSize) if let repeatValueView = self.repeatValue.view { if repeatValueView.superview == nil { self.addSubview(repeatValueView) } transition.setFrame(view: repeatValueView, frame: repeatValueFrame) } if self.isPickingRepeatPeriod { let repeatPickerSize = self.repeatPicker.update( transition: transition, component: AnyComponent( MenuComponent(component: AnyComponent(RepeatMenuComponent( value: self.repeatPeriod, valueUpdated: { [weak self] value in guard let self, let component = self.component, let environment = self.environment else { return } self.isPickingRepeatPeriod = false if component.context.isPremium { self.repeatPeriod = value } else { let toastController = UndoOverlayController( presentationData: component.context.sharedContext.currentPresentationData.with { $0 }, content: .premiumPaywall( title: "Premium Required", text: "Subscribe to **Telegram Premium** to schedule repeating messages.", customUndoText: nil, timeout: nil, linkAction: nil ), elevatedLayout: true, action: { [weak environment] action in if case .info = action { var replaceImpl: ((ViewController) -> Void)? let controller = component.context.sharedContext.makePremiumDemoController(context: component.context, subject: .colors, forceDark: false, action: { let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .nameColor, forceDark: false, dismissed: nil) replaceImpl?(controller) }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } environment?.controller()?.push(controller) } return true } ) environment.controller()?.present(toastController, in: .current) } self.state?.updated() } ))) ), environment: {}, containerSize: availableSize ) let repeatPickerFrame = CGRect(origin: CGPoint(x: repeatValueFrame.maxX - repeatPickerSize.width + 80.0, y: repeatValueFrame.minY - 20.0 - repeatPickerSize.height + 80.0), size: repeatPickerSize) if let repeatPickerView = self.repeatPicker.view as? MenuComponent.View { if repeatPickerView.superview == nil { self.addSubview(repeatPickerView) repeatPickerView.animateIn() } transition.setFrame(view: repeatPickerView, frame: repeatPickerFrame) } } else if let repeatPicker = self.repeatPicker.view as? MenuComponent.View, repeatPicker.superview != nil { repeatPicker.animateOut(completion: { repeatPicker.removeFromSuperview() }) } contentHeight += 70.0 let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: environment.dateTimeFormat) let buttonTitle: String switch component.mode { case .scheduledMessages: if calendar.isDateInToday(date) { buttonTitle = environment.strings.Conversation_ScheduleMessage_SendToday(time).string } else if calendar.isDateInTomorrow(date) { buttonTitle = environment.strings.Conversation_ScheduleMessage_SendTomorrow(time).string } else { buttonTitle = environment.strings.Conversation_ScheduleMessage_SendOn(self.dateFormatter.string(from: date), time).string } case .reminders: if calendar.isDateInToday(date) { buttonTitle = environment.strings.Conversation_SetReminder_RemindToday(time).string } else if calendar.isDateInTomorrow(date) { buttonTitle = environment.strings.Conversation_SetReminder_RemindTomorrow(time).string } else { buttonTitle = environment.strings.Conversation_SetReminder_RemindOn(self.dateFormatter.string(from: date), time).string } } let buttonSideInset: CGFloat = 30.0 let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8), ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( Text(text: buttonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) )), isEnabled: true, displaysProgress: false, action: { [weak self] in guard let self, let component = self.component, let controller = self.environment?.controller() as? ChatScheduleTimeScreen else { return } controller.completion( ChatScheduleTimeScreen.Result( time: Int32(self.date?.timeIntervalSince1970 ?? 0), repeatPeriod: self.repeatPeriod ) ) component.dismiss() } )), environment: {}, containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) ) let buttonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) } transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height let bottomPanelPadding: CGFloat = 15.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding contentHeight += bottomInset // if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { // let sideInset: CGFloat = 0.0 // let bottomInset: CGFloat = max(environment.safeInsets.bottom, contentHeight) //// if case .regular = environment.metrics.widthClass { //// sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0 //// bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight //// } // // let layout = ContainerViewLayout( // size: availableSize, // metrics: environment.metrics, // deviceMetrics: environment.deviceMetrics, // intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), // safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)), // additionalInsets: .zero, // statusBarHeight: environment.statusBarHeight, // inputHeight: nil, // inputHeightIsInteractivellyChanging: false, // inVoiceOver: false // ) // controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) // } return CGSize(width: availableSize.width, height: contentHeight) } } func makeView() -> View { return View(frame: CGRect()) } 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) } } private final class ChatScheduleTimeScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let mode: ChatScheduleTimeScreen.Mode init( context: AccountContext, mode: ChatScheduleTimeScreen.Mode ) { self.context = context self.mode = mode } static func ==(lhs: ChatScheduleTimeScreenComponent, rhs: ChatScheduleTimeScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.mode != rhs.mode { return false } return true } final class View: UIView { private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() private let sheetAnimateOut = ActionSlot>() private var component: ChatScheduleTimeScreenComponent? private var environment: EnvironmentType? override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: ChatScheduleTimeScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment let sheetEnvironment = SheetComponentEnvironment( isDisplaying: environment.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { [weak self] _ in guard let self, let environment = self.environment else { return } self.sheetAnimateOut.invoke(Action { _ in if let controller = environment.controller() { controller.dismiss(completion: nil) } }) } ) let _ = self.sheet.update( transition: transition, component: AnyComponent(SheetComponent( content: AnyComponent(ChatScheduleTimeSheetContentComponent( context: component.context, mode: component.mode, dismiss: { [weak self] in guard let self else { return } self.sheetAnimateOut.invoke(Action { _ in if let controller = environment.controller() { controller.dismiss(completion: nil) } }) } )), style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: self.sheetAnimateOut )), environment: { environment sheetEnvironment }, containerSize: availableSize ) if let sheetView = self.sheet.view { if sheetView.superview == nil { self.addSubview(sheetView) } transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) } return availableSize } } func makeView() -> View { return View(frame: CGRect()) } 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 class ChatScheduleTimeScreen: ViewControllerComponentContainer { public enum Mode: Equatable { case scheduledMessages(sendWhenOnlineAvailable: Bool) case reminders } public struct Result { public let time: Int32 public let repeatPeriod: Int32? } fileprivate let completion: (Result) -> Void public init( context: AccountContext, mode: Mode, completion: @escaping (Result) -> Void ) { self.completion = completion super.init(context: context, component: ChatScheduleTimeScreenComponent( context: context, mode: mode ), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true //self.automaticallyControlPresentationContextLayout = false } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true } } private final class ButtonContentComponent: Component { let theme: PresentationTheme let text: String let isActive: Bool let isLocked: Bool init( theme: PresentationTheme, text: String, isActive: Bool, isLocked: Bool ) { self.theme = theme self.text = text self.isActive = isActive self.isLocked = isLocked } static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.text != rhs.text { return false } if lhs.isActive != rhs.isActive { return false } if lhs.isLocked != rhs.isLocked { return false } return true } final class View: UIView { private var component: ButtonContentComponent? private weak var componentState: EmptyComponentState? private let backgroundLayer = SimpleLayer() private let title = ComponentView() private let icon = ComponentView() override init(frame: CGRect) { super.init(frame: frame) self.layer.addSublayer(self.backgroundLayer) self.backgroundLayer.masksToBounds = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state let backgroundColor: UIColor = component.isActive ? component.theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.1) : component.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.07) let textColor: UIColor = component.isActive ? component.theme.actionSheet.controlAccentColor : component.theme.actionSheet.primaryTextColor let titleSize = self.title.update( transition: transition, component: AnyComponent( Text(text: component.text, font: Font.regular(17.0), color: textColor) ), environment: {}, containerSize: availableSize ) var totalWidth = titleSize.width var iconSize = CGSize() if component.isLocked { iconSize = self.icon.update( transition: .immediate, component: AnyComponent( BundleIconComponent( name: "Media Grid/Lock", tintColor: textColor ) ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) totalWidth += iconSize.width + 2.0 } let padding: CGFloat = 12.0 let size = CGSize(width: totalWidth + padding * 2.0, height: 36.0) let titleFrame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } let iconFrame = CGRect(origin: CGPoint(x: size.width - padding - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) if let iconView = self.icon.view { if iconView.superview == nil { self.addSubview(iconView) } transition.setFrame(view: iconView, frame: iconFrame) } self.backgroundLayer.backgroundColor = backgroundColor.cgColor transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) self.backgroundLayer.cornerRadius = size.height / 2.0 return size } } func makeView() -> View { return View(frame: CGRect()) } 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) } } private final class MenuComponent: Component { public let component: AnyComponent public init( component: AnyComponent ) { self.component = component } public static func ==(lhs: MenuComponent, rhs: MenuComponent) -> Bool { if lhs.component != rhs.component { return false } return true } public final class View: UIView { private let backgroundView: GlassBackgroundView private var componentView: ComponentView? private var component: MenuComponent? public override init(frame: CGRect) { self.backgroundView = GlassBackgroundView() super.init(frame: frame) self.addSubview(self.backgroundView) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func animateIn() { let duration: Double = 0.35 self.layer.removeAllAnimations() self.alpha = 0.0 self.transform = CGAffineTransform(scaleX: 0.001, y: 0.001) self.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0) UIView.animate( withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.75, initialSpringVelocity: 0.6, options: [.curveEaseOut], animations: { self.transform = .identity self.alpha = 1.0 }, completion: nil ) } public func animateOut(duration: TimeInterval = 0.15, completion: (() -> Void)? = nil) { self.layer.removeAllAnimations() self.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0) UIView.animate( withDuration: duration, delay: 0.0, options: [.curveEaseInOut], animations: { self.transform = CGAffineTransform(scaleX: 0.001, y: 0.001) }, completion: { _ in completion?() } ) } func update(component: MenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component var componentView: ComponentView var componentTransition = transition if let current = self.componentView { componentView = current } else { componentTransition = .immediate componentView = ComponentView() self.componentView = componentView } let componentSize = componentView.update( transition: componentTransition, component: component.component, environment: {}, containerSize: availableSize ) let componentFrame = CGRect(origin: CGPoint(x: 80.0, y: 80.0), size: componentSize) if let view = componentView.view { if view.superview == nil { self.addSubview(view) } componentTransition.setFrame(view: view, frame: componentFrame) } let tintColor = GlassBackgroundView.TintColor(kind: .custom, color: UIColor(rgb: 0xf6f7f8)) let backgroundFrame = CGRect(origin: CGPoint(x: 80.0, y: 80.0), size: componentSize) self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: false, tintColor: tintColor, transition: transition) self.backgroundView.frame = backgroundFrame return CGSize(width: componentSize.width + 80.0 * 2.0, height: componentSize.height + 80.0 * 2.0) } public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.backgroundView.frame.contains(point) } } public func makeView() -> View { return View(frame: CGRect()) } public 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) } } private final class RepeatMenuComponent: Component { let value: Int32? let valueUpdated: (Int32?) -> Void init( value: Int32?, valueUpdated: @escaping (Int32?) -> Void ) { self.value = value self.valueUpdated = valueUpdated } public static func ==(lhs: RepeatMenuComponent, rhs: RepeatMenuComponent) -> Bool { if lhs.value != rhs.value { return false } return true } public final class View: UIView { private let backgroundView: GlassBackgroundView private let never = ComponentView() private let separator = SimpleLayer() private var itemViews: [Int32: ComponentView] = [:] private let checkIcon = UIImageView() private var component: RepeatMenuComponent? private let values: [Int32] = [ 86400, 7 * 86400, 14 * 86400, 30 * 86400, 91 * 86400, 182 * 86400, 365 * 86400 ] public override init(frame: CGRect) { self.backgroundView = GlassBackgroundView() self.checkIcon.image = UIImage(bundleImageName: "Media Gallery/Check")?.withRenderingMode(.alwaysTemplate) super.init(frame: frame) self.addSubview(self.backgroundView) self.layer.addSublayer(self.separator) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: RepeatMenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let sideInset: CGFloat = 18.0 let itemInset: CGFloat = 60.0 self.checkIcon.tintColor = .black let neverSize = self.never.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( Text(text: "Never", font: Font.regular(17.0), color: .black) ), action: { [weak self] in guard let self else { return } self.component?.valueUpdated(nil) } ) ), environment: {}, containerSize: availableSize ) let neverFrame = CGRect(origin: CGPoint(x: itemInset, y: 21.0), size: neverSize) if let neverView = self.never.view { if neverView.superview == nil { self.addSubview(neverView) } transition.setFrame(view: neverView, frame: neverFrame) if component.value == nil { neverView.addSubview(self.checkIcon) } } var maxWidth: CGFloat = 0.0 var originY: CGFloat = 83.0 for value in self.values { let itemView: ComponentView if let current = self.itemViews[value] { itemView = current } else { itemView = ComponentView() self.itemViews[value] = itemView } let repeatString: String switch value { case 60: repeatString = "Every Minute" case 300: repeatString = "Every 5 Minutes" case 86400: repeatString = "Daily" case 7 * 86400: repeatString = "Weekly" case 14 * 86400: repeatString = "Biweekly" case 30 * 86400: repeatString = "Monthly" case 91 * 86400: repeatString = "Every 3 Months" case 182 * 86400: repeatString = "Every 6 Months" case 365 * 86400: repeatString = "Yearly" default: repeatString = "\(value)" } let itemSize = itemView.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( Text(text: repeatString, font: Font.regular(17.0), color: .black) ), action: { [weak self] in guard let self else { return } self.component?.valueUpdated(value) } ) ), environment: {}, containerSize: availableSize ) maxWidth = max(maxWidth, itemSize.width) let itemFrame = CGRect(origin: CGPoint(x: itemInset, y: originY), size: itemSize) if let itemView = itemView.view { if itemView.superview == nil { self.addSubview(itemView) } transition.setFrame(view: itemView, frame: itemFrame) if component.value == value { itemView.addSubview(self.checkIcon) } } originY += 42.0 } if let image = self.checkIcon.image { self.checkIcon.frame = CGRect(origin: CGPoint(x: -35.0, y: floorToScreenPixels((12.0 - image.size.height) / 2.0) + 5.0), size: image.size) } let size = CGSize(width: itemInset + maxWidth + 40.0, height: originY) self.separator.backgroundColor = UIColor(rgb: 0xdddddd).cgColor self.separator.frame = CGRect(origin: CGPoint(x: sideInset, y: 62.0), size: CGSize(width: size.width - sideInset * 2.0, height: UIScreenPixel)) return size } } public func makeView() -> View { return View(frame: CGRect()) } public 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) } } private final class TimeMenuComponent: Component { let value: Date let valueUpdated: (Date) -> Void init( value: Date, valueUpdated: @escaping (Date) -> Void ) { self.value = value self.valueUpdated = valueUpdated } public static func == (lhs: TimeMenuComponent, rhs: TimeMenuComponent) -> Bool { return lhs.value == rhs.value } public final class View: UIView { private let picker = UIDatePicker() private var component: TimeMenuComponent? public override init(frame: CGRect) { super.init(frame: frame) self.picker.datePickerMode = .time if #available(iOS 13.4, *) { self.picker.preferredDatePickerStyle = .wheels } self.picker.addTarget(self, action: #selector(valueChanged), for: .valueChanged) self.addSubview(self.picker) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func valueChanged() { guard let component = self.component else { return } component.valueUpdated(self.picker.date) } func update(component: TimeMenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previous = self.component self.component = component if previous == nil || abs(component.value.timeIntervalSince(self.picker.date)) > 0.5 { self.picker.setDate(component.value, animated: false) } let pickerSize = self.picker.sizeThatFits(availableSize) let width = min(availableSize.width, max(pickerSize.width, 230.0)) let height = pickerSize.height > 0 ? pickerSize.height : 216.0 let frame = CGRect(origin: .zero, size: CGSize(width: width, height: height)) transition.setFrame(view: self.picker, frame: frame) return frame.size } } public func makeView() -> View { return View(frame: .zero) } public 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) } }