Telegram-iOS/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift
2026-05-05 14:57:06 +02:00

1043 lines
41 KiB
Swift

import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import TelegramStringFormatting
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import ButtonComponent
import BundleIconComponent
import GlassBarButtonComponent
public enum ChatTimerScreenStyle {
case `default`
case media
}
public enum ChatTimerScreenMode {
case sendTimer
case autoremove
case mute
}
private protocol TimerPickerView: UIView {
}
private final class TimerCustomPickerView: UIPickerView, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor, subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews where subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
private final class TimerDatePickerView: UIDatePicker, TimerPickerView {
var selectorColor: UIColor? = nil {
didSet {
for subview in self.subviews {
if subview.bounds.height <= 1.0 {
subview.backgroundColor = self.selectorColor
}
}
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if let selectorColor = self.selectorColor, subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let selectorColor = self.selectorColor {
for subview in self.subviews where subview.bounds.height <= 1.0 {
subview.backgroundColor = selectorColor
}
}
}
}
private let digitsCharacterSet = CharacterSet(charactersIn: "0123456789")
private let nondigitsCharacterSet = CharacterSet(charactersIn: "0123456789").inverted
private final class TimerPickerItemView: UIView {
let valueLabel = UILabel()
let unitLabel = UILabel()
var textColor: UIColor? = nil {
didSet {
self.valueLabel.textColor = self.textColor
self.unitLabel.textColor = self.textColor
}
}
var value: (Int32, String)? {
didSet {
if let (value, string) = self.value {
let components = string.components(separatedBy: " ")
if value == viewOnceTimeout || string.rangeOfCharacter(from: digitsCharacterSet) == nil {
self.valueLabel.text = string
self.unitLabel.text = ""
} else if components.count > 1 {
self.valueLabel.text = components[0]
self.unitLabel.text = components[1]
} else {
self.valueLabel.text = string.trimmingCharacters(in: nondigitsCharacterSet)
self.unitLabel.text = string.trimmingCharacters(in: digitsCharacterSet)
}
}
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
self.valueLabel.backgroundColor = nil
self.valueLabel.isOpaque = false
self.valueLabel.font = Font.regular(24.0)
self.unitLabel.backgroundColor = nil
self.unitLabel.isOpaque = false
self.unitLabel.font = Font.medium(16.0)
super.init(frame: frame)
self.addSubview(self.valueLabel)
self.addSubview(self.unitLabel)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.valueLabel.sizeToFit()
self.unitLabel.sizeToFit()
if self.unitLabel.text?.isEmpty ?? false {
self.valueLabel.frame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((self.frame.width - self.valueLabel.frame.size.width) / 2.0),
y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)
),
size: self.valueLabel.frame.size
)
} else {
self.valueLabel.frame = CGRect(
origin: CGPoint(
x: self.frame.width / 2.0 - 28.0 - self.valueLabel.frame.size.width,
y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)
),
size: self.valueLabel.frame.size
)
self.unitLabel.frame = CGRect(
origin: CGPoint(
x: self.frame.width / 2.0 - 20.0,
y: floor((self.frame.height - self.unitLabel.frame.height) / 2.0) + 2.0
),
size: self.unitLabel.frame.size
)
}
}
}
private let timerValues: [Int32] = {
var values: [Int32] = []
for i in 1 ..< 20 {
values.append(Int32(i))
}
for i in 0 ..< 9 {
values.append(Int32(20 + i * 5))
}
return values
}()
private let autoremoveTimerValues: [Int32] = [
1 * 24 * 60 * 60 as Int32,
2 * 24 * 60 * 60 as Int32,
3 * 24 * 60 * 60 as Int32,
4 * 24 * 60 * 60 as Int32,
5 * 24 * 60 * 60 as Int32,
6 * 24 * 60 * 60 as Int32,
1 * 7 * 24 * 60 * 60 as Int32,
2 * 7 * 24 * 60 * 60 as Int32,
3 * 7 * 24 * 60 * 60 as Int32,
1 * 31 * 24 * 60 * 60 as Int32,
2 * 30 * 24 * 60 * 60 as Int32,
3 * 31 * 24 * 60 * 60 as Int32,
4 * 30 * 24 * 60 * 60 as Int32,
5 * 31 * 24 * 60 * 60 as Int32,
6 * 30 * 24 * 60 * 60 as Int32,
365 * 24 * 60 * 60 as Int32
]
private final class ChatTimerSheetContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let configuration: ChatTimerScreen.Configuration
let dismiss: () -> Void
init(
configuration: ChatTimerScreen.Configuration,
dismiss: @escaping () -> Void
) {
self.configuration = configuration
self.dismiss = dismiss
}
static func ==(lhs: ChatTimerSheetContentComponent, rhs: ChatTimerSheetContentComponent) -> Bool {
return lhs.configuration == rhs.configuration
}
final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate {
private let closeButton = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let primaryButton = ComponentView<Empty>()
private let secondaryButton = ComponentView<Empty>()
private var component: ChatTimerSheetContentComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
private var pickerView: TimerPickerView?
private var isCompleting = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func pickerTextColor(configuration: ChatTimerScreen.Configuration, environment: EnvironmentType) -> UIColor {
if let pickerTextColor = configuration.pickerTextColor {
return pickerTextColor(environment.theme)
}
switch configuration.style {
case .default:
return environment.theme.actionSheet.primaryTextColor
case .media:
return .white
}
}
private func mapPickerTimestamp(_ timestamp: Int32, mapping: ChatTimerScreen.Configuration.PickerValueMapping) -> Int32 {
switch mapping {
case .rawTimestamp:
return timestamp
case .roundDateToDaysUTC:
return roundDateToDays(timestamp)
case .secondsFromMidnightGMT:
return timestamp
}
}
private func selectedValue() -> Int32? {
guard let component = self.component, let pickerView = self.pickerView else {
return nil
}
switch component.configuration.picker {
case let .fixedValues(values, _, _):
guard let pickerView = pickerView as? TimerCustomPickerView else {
return nil
}
let row = pickerView.selectedRow(inComponent: 0)
guard row >= 0, row < values.count else {
return nil
}
return values[row]
case .timeOfDay, .date, .dateTime:
guard let pickerView = pickerView as? TimerDatePickerView else {
return nil
}
return self.mapPickerTimestamp(Int32(pickerView.date.timeIntervalSince1970), mapping: component.configuration.pickerValueMapping)
}
}
private func fixedValueSelectionIndex(
values: [Int32],
selectedValue: Int32,
strategy: ChatTimerScreen.Configuration.FixedSelectionStrategy
) -> Int {
switch strategy {
case .exact:
return values.firstIndex(of: selectedValue) ?? 0
case .closestLowerOrEqual:
var index = 0
for i in 0 ..< values.count {
if values[i] <= selectedValue {
index = i
}
}
return index
case .firstGreaterOrEqual:
var index = max(0, values.count - 1)
for i in 0 ..< values.count {
if selectedValue <= values[i] {
index = i
break
}
}
return index
}
}
private func setupPickerView(configuration: ChatTimerScreen.Configuration, environment: EnvironmentType) {
let previousSelectedValue = self.selectedValue()
let previousDate = (self.pickerView as? TimerDatePickerView)?.date
self.pickerView?.removeFromSuperview()
switch configuration.picker {
case let .fixedValues(values, selectionStrategy, _):
let pickerView = TimerCustomPickerView()
pickerView.dataSource = self
pickerView.delegate = self
pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18)
self.addSubview(pickerView)
self.pickerView = pickerView
if let selectedValue = previousSelectedValue ?? configuration.currentValue {
let index = self.fixedValueSelectionIndex(values: values, selectedValue: selectedValue, strategy: selectionStrategy)
pickerView.selectRow(index, inComponent: 0, animated: false)
}
case .timeOfDay:
let pickerView = TimerDatePickerView()
pickerView.datePickerMode = .time
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
pickerView.locale = Locale.current
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.setValue(self.pickerTextColor(configuration: configuration, environment: environment), forKey: "textColor")
pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18)
pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged)
let initialTimestamp: Int32
if let currentValue = configuration.currentValue {
initialTimestamp = self.mapPickerTimestamp(currentValue, mapping: configuration.pickerValueMapping)
} else {
initialTimestamp = 0
}
let date = previousDate ?? Date(timeIntervalSince1970: Double(initialTimestamp))
pickerView.date = date
self.addSubview(pickerView)
self.pickerView = pickerView
case .date:
let pickerView = TimerDatePickerView()
pickerView.datePickerMode = .date
pickerView.timeZone = TimeZone(secondsFromGMT: 0)
pickerView.locale = localeWithStrings(environment.strings)
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.minimumDate = configuration.minimumDate
pickerView.maximumDate = configuration.maximumDate ?? Date(timeIntervalSince1970: Double(Int32.max - 1))
pickerView.setValue(self.pickerTextColor(configuration: configuration, environment: environment), forKey: "textColor")
pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18)
pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged)
let initialTimestamp: Int32
if let currentValue = configuration.currentValue {
initialTimestamp = self.mapPickerTimestamp(currentValue, mapping: configuration.pickerValueMapping)
} else {
initialTimestamp = Int32(Date().timeIntervalSince1970)
}
var initialDate = previousDate ?? Date(timeIntervalSince1970: Double(initialTimestamp))
if let minimumDate = pickerView.minimumDate, initialDate < minimumDate {
initialDate = minimumDate
}
if let maximumDate = pickerView.maximumDate, initialDate > maximumDate {
initialDate = maximumDate
}
pickerView.date = initialDate
self.addSubview(pickerView)
self.pickerView = pickerView
case .dateTime:
let pickerView = TimerDatePickerView()
pickerView.datePickerMode = .dateAndTime
pickerView.locale = localeWithStrings(environment.strings)
pickerView.minimumDate = configuration.minimumDate ?? Date()
pickerView.maximumDate = configuration.maximumDate
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.setValue(self.pickerTextColor(configuration: configuration, environment: environment), forKey: "textColor")
pickerView.setValue(false, forKey: "highlightsToday")
pickerView.selectorColor = self.pickerTextColor(configuration: configuration, environment: environment).withMultipliedAlpha(0.18)
pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged)
var date = previousDate ?? configuration.currentValue.flatMap { Date(timeIntervalSince1970: Double($0)) } ?? Date()
if let minimumDate = pickerView.minimumDate, date < minimumDate {
date = minimumDate
}
if let maximumDate = pickerView.maximumDate, date > maximumDate {
date = maximumDate
}
pickerView.date = date
self.addSubview(pickerView)
self.pickerView = pickerView
}
}
@objc private func datePickerChanged() {
self.state?.updated(transition: .immediate)
}
private func complete(selectedValue: Int32?) {
guard !self.isCompleting else {
return
}
self.isCompleting = true
let transformedValue: Int32?
if let component = self.component {
transformedValue = component.configuration.completionValueTransform(selectedValue)
} else {
transformedValue = nil
}
if let controller = self.environment?.controller() as? ChatTimerScreen {
controller.completion(transformedValue)
}
self.component?.dismiss()
}
func update(
component: ChatTimerSheetContentComponent,
availableSize: CGSize,
state: EmptyComponentState,
environment: Environment<EnvironmentType>,
transition: ComponentTransition
) -> CGSize {
let environment = environment[EnvironmentType.self].value
let previousComponent = self.component
let previousEnvironment = self.environment
let themeUpdated: Bool
let stringsUpdated: Bool
if let previousEnvironment {
themeUpdated = previousEnvironment.theme !== environment.theme
stringsUpdated = previousEnvironment.strings !== environment.strings
} else {
themeUpdated = false
stringsUpdated = false
}
self.component = component
self.environment = environment
self.state = state
if self.pickerView == nil || previousComponent?.configuration != component.configuration || themeUpdated || stringsUpdated {
self.setupPickerView(configuration: component.configuration, environment: environment)
}
let titleColor: UIColor
switch component.configuration.style {
case .default:
titleColor = environment.theme.actionSheet.primaryTextColor
case .media:
titleColor = .white
}
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let closeButtonSize = self.closeButton.update(
transition: transition,
component: AnyComponent(
GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
self?.component?.dismiss()
}
)
),
environment: {},
containerSize: barButtonSize
)
if let closeButtonView = self.closeButton.view {
if closeButtonView.superview == nil {
self.addSubview(closeButtonView)
}
transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize))
}
let titleText = component.configuration.title(environment.strings)
if let titleText, !titleText.isEmpty {
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
Text(text: titleText, font: Font.semibold(17.0), color: titleColor)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 120.0, height: 44.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(
view: titleView,
frame: CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0),
y: floorToScreenPixels(16.0 + (barButtonSize.height - titleSize.height) / 2.0)
),
size: titleSize
)
)
}
} else if let titleView = self.title.view, titleView.superview != nil {
titleView.removeFromSuperview()
}
var contentHeight: CGFloat = 68.0
let pickerHeight: CGFloat = 216.0
if let pickerView = self.pickerView {
transition.setFrame(view: pickerView as UIView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: pickerHeight)))
}
contentHeight += pickerHeight
contentHeight += 17.0
let buttonSideInset: CGFloat = 30.0
let selectedValue = self.selectedValue()
let primaryButtonTitle = component.configuration.primaryActionTitle(environment.strings, environment.dateTimeFormat, selectedValue)
let primaryButtonSize = self.primaryButton.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(primaryButtonTitle), component: AnyComponent(
Text(text: primaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
)),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
self?.complete(selectedValue: self?.selectedValue())
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0)
)
if let primaryButtonView = self.primaryButton.view {
if primaryButtonView.superview == nil {
self.addSubview(primaryButtonView)
}
transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: primaryButtonSize))
}
contentHeight += primaryButtonSize.height
if let secondaryAction = component.configuration.secondaryAction {
contentHeight += 8.0
let foregroundColor: UIColor
switch secondaryAction.style {
case .accent:
foregroundColor = environment.theme.actionSheet.controlAccentColor
case .destructive:
foregroundColor = environment.theme.list.itemDestructiveColor
}
let secondaryButtonTitle = secondaryAction.title(environment.strings)
let secondaryButtonSize = self.secondaryButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: foregroundColor.withMultipliedAlpha(0.1),
foreground: foregroundColor,
pressedColor: foregroundColor.withMultipliedAlpha(0.2)
),
content: AnyComponentWithIdentity(id: AnyHashable(secondaryButtonTitle), component: AnyComponent(
Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: foregroundColor)
)),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
self?.complete(selectedValue: secondaryAction.value())
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0)
)
if let secondaryButtonView = self.secondaryButton.view {
if secondaryButtonView.superview == nil {
self.addSubview(secondaryButtonView)
}
transition.setFrame(view: secondaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: secondaryButtonSize))
}
contentHeight += secondaryButtonSize.height
} else if let secondaryButtonView = self.secondaryButton.view, secondaryButtonView.superview != nil {
secondaryButtonView.removeFromSuperview()
}
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : 15.0
contentHeight += bottomInset
return CGSize(width: availableSize.width, height: contentHeight)
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
guard let configuration = self.component?.configuration else {
return 0
}
switch configuration.picker {
case let .fixedValues(values, _, _):
return values.count
case .timeOfDay, .date, .dateTime:
return 0
}
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent componentIndex: Int, reusing view: UIView?) -> UIView {
guard let configuration = self.component?.configuration, let environment = self.environment else {
return UIView()
}
let itemView: TimerPickerItemView
if let current = view as? TimerPickerItemView {
itemView = current
} else {
itemView = TimerPickerItemView()
}
itemView.textColor = self.pickerTextColor(configuration: configuration, environment: environment)
switch configuration.picker {
case let .fixedValues(values, _, formatter):
let value = values[row]
itemView.value = (value, formatter(environment.strings, value))
case .timeOfDay, .date, .dateTime:
break
}
return itemView
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
self.state?.updated(transition: .immediate)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ChatTimerSheetComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let configuration: ChatTimerScreen.Configuration
init(configuration: ChatTimerScreen.Configuration) {
self.configuration = configuration
}
static func ==(lhs: ChatTimerSheetComponent, rhs: ChatTimerSheetComponent) -> Bool {
return lhs.configuration == rhs.configuration
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
private let sheetAnimateOut = ActionSlot<Action<Void>>()
private var component: ChatTimerSheetComponent?
private var environment: EnvironmentType?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func dismiss() {
self.sheetAnimateOut.invoke(Action { [weak self] _ in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss(completion: nil)
})
}
func update(component: ChatTimerSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.component = component
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
let sheetEnvironment = SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { [weak self] _ in
self?.dismiss()
}
)
let backgroundColor: UIColor
switch component.configuration.style {
case .default:
backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor
case .media:
backgroundColor = UIColor(rgb: 0x1c1c1e)
}
let _ = self.sheet.update(
transition: transition,
component: AnyComponent(SheetComponent(
content: AnyComponent(ChatTimerSheetContentComponent(
configuration: component.configuration,
dismiss: { [weak self] in
self?.dismiss()
}
)),
style: .glass,
backgroundColor: .color(backgroundColor),
followContentSizeChanges: true,
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<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class ChatTimerScreen: ViewControllerComponentContainer {
public final class Configuration: Equatable {
public enum ActionStyle {
case accent
case destructive
}
public enum PickerValueMapping {
case rawTimestamp
case roundDateToDaysUTC
case secondsFromMidnightGMT
}
public enum FixedSelectionStrategy {
case exact
case closestLowerOrEqual
case firstGreaterOrEqual
}
public final class SecondaryAction: Equatable {
public let title: (PresentationStrings) -> String
public let style: ActionStyle
public let value: () -> Int32?
public init(
title: @escaping (PresentationStrings) -> String,
style: ActionStyle,
value: @escaping () -> Int32?
) {
self.title = title
self.style = style
self.value = value
}
public static func ==(lhs: SecondaryAction, rhs: SecondaryAction) -> Bool {
return lhs === rhs
}
}
public enum PickerKind {
case timeOfDay
case date
case dateTime
case fixedValues(
values: [Int32],
selectionStrategy: FixedSelectionStrategy,
formatter: (PresentationStrings, Int32) -> String
)
}
public let style: ChatTimerScreenStyle
public let title: (PresentationStrings) -> String?
public let picker: PickerKind
public let currentValue: Int32?
public let minimumDate: Date?
public let maximumDate: Date?
public let pickerValueMapping: PickerValueMapping
public let primaryActionTitle: (PresentationStrings, PresentationDateTimeFormat, Int32?) -> String
public let secondaryAction: SecondaryAction?
public let completionValueTransform: (Int32?) -> Int32?
public let pickerTextColor: ((PresentationTheme) -> UIColor)?
public init(
style: ChatTimerScreenStyle,
title: @escaping (PresentationStrings) -> String? = { _ in nil },
picker: PickerKind,
currentValue: Int32?,
minimumDate: Date? = nil,
maximumDate: Date? = nil,
pickerValueMapping: PickerValueMapping,
primaryActionTitle: @escaping (PresentationStrings, PresentationDateTimeFormat, Int32?) -> String,
secondaryAction: SecondaryAction? = nil,
completionValueTransform: @escaping (Int32?) -> Int32? = { $0 },
pickerTextColor: ((PresentationTheme) -> UIColor)? = nil
) {
self.style = style
self.title = title
self.picker = picker
self.currentValue = currentValue
self.minimumDate = minimumDate
self.maximumDate = maximumDate
self.pickerValueMapping = pickerValueMapping
self.primaryActionTitle = primaryActionTitle
self.secondaryAction = secondaryAction
self.completionValueTransform = completionValueTransform
self.pickerTextColor = pickerTextColor
}
public static func ==(lhs: Configuration, rhs: Configuration) -> Bool {
return lhs === rhs
}
}
fileprivate let completion: (Int32?) -> Void
private static func legacyConfiguration(
style: ChatTimerScreenStyle,
mode: ChatTimerScreenMode,
currentTime: Int32?
) -> Configuration {
switch mode {
case .sendTimer:
return Configuration(
style: style,
title: { strings in
strings.Conversation_Timer_Title
},
picker: .fixedValues(
values: [viewOnceTimeout] + timerValues,
selectionStrategy: .exact,
formatter: { strings, value in
if value == viewOnceTimeout {
return strings.MediaPicker_Timer_ViewOnce
} else {
return timeIntervalString(strings: strings, value: value)
}
}
),
currentValue: currentTime ?? viewOnceTimeout,
pickerValueMapping: .rawTimestamp,
primaryActionTitle: { strings, _, _ in
strings.Conversation_Timer_Send
},
pickerTextColor: { _ in
.white
}
)
case .autoremove:
return Configuration(
style: style,
title: { strings in
strings.Conversation_DeleteTimer_SetupTitle
},
picker: .fixedValues(
values: autoremoveTimerValues,
selectionStrategy: .closestLowerOrEqual,
formatter: { strings, value in
timeIntervalString(strings: strings, value: value)
}
),
currentValue: currentTime,
pickerValueMapping: .rawTimestamp,
primaryActionTitle: { strings, _, _ in
strings.Conversation_DeleteTimer_Apply
},
secondaryAction: currentTime != nil ? Configuration.SecondaryAction(
title: { strings in
strings.Conversation_DeleteTimer_Disable
},
style: .destructive,
value: {
0
}
) : nil,
pickerTextColor: { theme in
if case .media = style {
return .white
} else {
return theme.list.itemPrimaryTextColor
}
}
)
case .mute:
return Configuration(
style: style,
title: { strings in
strings.Conversation_Mute_SetupTitle
},
picker: .dateTime,
currentValue: currentTime,
minimumDate: Date(),
pickerValueMapping: .rawTimestamp,
primaryActionTitle: { strings, dateTimeFormat, selectedValue in
if let selectedValue {
let now = Int32(Date().timeIntervalSince1970)
let timeInterval = max(0, selectedValue - now)
if timeInterval > 0 {
let timeString = stringForPreciseRelativeTimestamp(
strings: strings,
relativeTimestamp: selectedValue,
relativeTo: now,
dateTimeFormat: dateTimeFormat
)
return strings.Conversation_Mute_ApplyMuteUntil(timeString).string
}
}
return strings.Common_Close
},
completionValueTransform: { selectedValue in
guard let selectedValue else {
return nil
}
return max(0, selectedValue - Int32(Date().timeIntervalSince1970))
},
pickerTextColor: { theme in
if case .media = style {
return .white
} else {
return theme.list.itemPrimaryTextColor
}
}
)
}
}
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
configuration: Configuration,
completion: @escaping (Int32?) -> Void
) {
self.completion = completion
super.init(
context: context,
component: ChatTimerSheetComponent(configuration: configuration),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: configuration.style == .media ? .dark : .default,
updatedPresentationData: updatedPresentationData
)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
public convenience init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
style: ChatTimerScreenStyle,
mode: ChatTimerScreenMode = .sendTimer,
currentTime: Int32? = nil,
completion: @escaping (Int32) -> Void
) {
let configuration = Self.legacyConfiguration(style: style, mode: mode, currentTime: currentTime)
self.init(
context: context,
updatedPresentationData: updatedPresentationData,
configuration: configuration,
completion: { value in
completion(value ?? 0)
}
)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}