mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
1043 lines
41 KiB
Swift
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()
|
|
}
|
|
}
|
|
}
|