2024-04-02 16:28:01 +04:00

1671 lines
87 KiB
Swift

import Foundation
import UIKit
import Photos
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import BackButtonComponent
import ListSectionComponent
import ListActionItemComponent
import ListTextFieldItemComponent
import BundleIconComponent
import LottieComponent
import Markdown
import PeerListItemComponent
import AvatarNode
import ListItemSliderSelectorComponent
import DateSelectionUI
import PlainButtonComponent
import TelegramStringFormatting
import TimeSelectionActionSheet
private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(1.98)
context.setLineCap(.round)
context.setLineJoin(.round)
context.translateBy(x: 1.0, y: 1.0)
let _ = try? drawSvgPath(context, path: "M0.215053763,4.36080467 L3.31621263,7.70466293 L3.31621263,7.70466293 C3.35339229,7.74475231 3.41603123,7.74711109 3.45612061,7.70993143 C3.45920681,7.70706923 3.46210733,7.70401312 3.46480451,7.70078171 L9.89247312,0 S ")
})!.withRenderingMode(.alwaysTemplate)
}()
final class AutomaticBusinessMessageSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: AutomaticBusinessMessageSetupScreen.InitialData
let mode: AutomaticBusinessMessageSetupScreen.Mode
init(
context: AccountContext,
initialData: AutomaticBusinessMessageSetupScreen.InitialData,
mode: AutomaticBusinessMessageSetupScreen.Mode
) {
self.context = context
self.initialData = initialData
self.mode = mode
}
static func ==(lhs: AutomaticBusinessMessageSetupScreenComponent, rhs: AutomaticBusinessMessageSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
struct AdditionalPeerList {
enum Category: Int {
case newChats = 0
case existingChats = 1
case contacts = 2
case nonContacts = 3
}
struct Peer {
var peer: EnginePeer
var isContact: Bool
init(peer: EnginePeer, isContact: Bool) {
self.peer = peer
self.isContact = isContact
}
}
var categories: Set<Category>
var peers: [Peer]
init(categories: Set<Category>, peers: [Peer]) {
self.categories = categories
self.peers = peers
}
}
private enum Schedule {
case always
case outsideBusinessHours
case custom
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let generalSection = ComponentView<Empty>()
private let messagesSection = ComponentView<Empty>()
private let scheduleSection = ComponentView<Empty>()
private let customScheduleSection = ComponentView<Empty>()
private let sendWhenOfflineSection = ComponentView<Empty>()
private let accessSection = ComponentView<Empty>()
private let excludedSection = ComponentView<Empty>()
private let periodSection = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var isUpdating: Bool = false
private var component: AutomaticBusinessMessageSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var isOn: Bool = false
private var accountPeer: EnginePeer?
private var currentShortcut: ShortcutMessageList.Item?
private var currentShortcutDisposable: Disposable?
private var schedule: Schedule = .always
private var customScheduleStart: Date?
private var customScheduleEnd: Date?
private var sendWhenOffline: Bool = true
private var hasAccessToAllChatsByDefault: Bool = true
private var additionalPeerList = AdditionalPeerList(
categories: Set(),
peers: []
)
private var replyToMessages: Bool = true
private var inactivityDays: Int = 7
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.currentShortcutDisposable?.dispose()
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
guard let component = self.component else {
return true
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if self.isOn {
if !self.hasAccessToAllChatsByDefault && self.additionalPeerList.categories.isEmpty && self.additionalPeerList.peers.isEmpty {
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_Text, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.BusinessMessageSetup_ErrorNoRecipients_ResetAction, action: {
complete()
})
]), in: .window(.root))
return false
}
if case .away = component.mode, case .custom = self.schedule {
var errorText: String?
if let customScheduleStart = self.customScheduleStart, let customScheduleEnd = self.customScheduleEnd {
if customScheduleStart >= customScheduleEnd {
errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleEndTimeBeforeStartTime_Text
}
} else {
if self.customScheduleStart == nil && self.customScheduleEnd == nil {
errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleTimeMissing_Text
} else if self.customScheduleStart == nil {
errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleStartTimeMissing_Text
} else {
errorText = presentationData.strings.BusinessMessageSetup_ErrorScheduleEndTimeMissing_Text
}
}
if let errorText {
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.BusinessMessageSetup_ErrorScheduleTime_ResetAction, action: {
complete()
})
]), in: .window(.root))
return false
}
}
}
var mappedCategories: TelegramBusinessRecipients.Categories = []
if self.additionalPeerList.categories.contains(.existingChats) {
mappedCategories.insert(.existingChats)
}
if self.additionalPeerList.categories.contains(.newChats) {
mappedCategories.insert(.newChats)
}
if self.additionalPeerList.categories.contains(.contacts) {
mappedCategories.insert(.contacts)
}
if self.additionalPeerList.categories.contains(.nonContacts) {
mappedCategories.insert(.nonContacts)
}
let recipients = TelegramBusinessRecipients(
categories: mappedCategories,
additionalPeers: Set(self.additionalPeerList.peers.map(\.peer.id)),
excludePeers: Set(),
exclude: self.hasAccessToAllChatsByDefault
)
switch component.mode {
case .greeting:
var greetingMessage: TelegramBusinessGreetingMessage?
if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id {
greetingMessage = TelegramBusinessGreetingMessage(
shortcutId: shortcutId,
recipients: recipients,
inactivityDays: self.inactivityDays
)
}
let _ = component.context.engine.accountData.updateBusinessGreetingMessage(greetingMessage: greetingMessage).startStandalone()
case .away:
var awayMessage: TelegramBusinessAwayMessage?
if self.isOn, let currentShortcut = self.currentShortcut, let shortcutId = currentShortcut.id {
let mappedSchedule: TelegramBusinessAwayMessage.Schedule
switch self.schedule {
case .always:
mappedSchedule = .always
case .outsideBusinessHours:
mappedSchedule = .outsideWorkingHours
case .custom:
if let customScheduleStart = self.customScheduleStart, let customScheduleEnd = self.customScheduleEnd {
mappedSchedule = .custom(beginTimestamp: Int32(customScheduleStart.timeIntervalSince1970), endTimestamp: Int32(customScheduleEnd.timeIntervalSince1970))
} else {
return false
}
}
awayMessage = TelegramBusinessAwayMessage(
shortcutId: shortcutId,
recipients: recipients,
schedule: mappedSchedule,
sendWhenOffline: self.sendWhenOffline
)
}
let _ = component.context.engine.accountData.updateBusinessAwayMessage(awayMessage: awayMessage).startStandalone()
}
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
var scrolledUp = true
private func updateScrolling(transition: Transition) {
let navigationRevealOffsetY: CGFloat = 0.0
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
var scrolledUp = false
if navigationAlpha < 0.5 {
scrolledUp = true
} else if navigationAlpha > 0.5 {
scrolledUp = false
}
if self.scrolledUp != scrolledUp {
self.scrolledUp = scrolledUp
if !self.isUpdating {
self.state?.updated()
}
}
if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
}
}
private func openAdditionalPeerListSetup() {
guard let component = self.component, let enviroment = self.environment else {
return
}
enum AdditionalCategoryId: Int {
case existingChats
case newChats
case contacts
case nonContacts
}
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple),
title: self.hasAccessToAllChatsByDefault ? enviroment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : enviroment.strings.BusinessMessageSetup_Recipients_CategoryNewChats
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
title: enviroment.strings.BusinessMessageSetup_Recipients_CategoryContacts
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.nonContacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
title: enviroment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts
)
]
var selectedCategories = Set<Int>()
for category in self.additionalPeerList.categories {
switch category {
case .existingChats:
selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue)
case .newChats:
selectedCategories.insert(AdditionalCategoryId.newChats.rawValue)
case .contacts:
selectedCategories.insert(AdditionalCategoryId.contacts.rawValue)
case .nonContacts:
selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue)
}
}
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: self.hasAccessToAllChatsByDefault ? enviroment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : enviroment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle,
searchPlaceholder: enviroment.strings.ChatListFilter_AddChatsSearchPlaceholder,
selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
onlyUsers: true
)), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in
}))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in
guard let self, let component = self.component, case let .result(rawPeerIds, additionalCategoryIds) = result else {
controller?.dismiss()
return
}
let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in
switch id {
case let .peer(id):
return id
case .deviceContact:
return nil
}
}
let _ = (component.context.engine.data.get(
EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
),
EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:))
)
)
|> deliverOnMainQueue).start(next: { [weak self] peerMap, isContactMap in
guard let self else {
return
}
let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in
switch item {
case AdditionalCategoryId.existingChats.rawValue:
return .existingChats
case AdditionalCategoryId.newChats.rawValue:
return .newChats
case AdditionalCategoryId.contacts.rawValue:
return .contacts
case AdditionalCategoryId.nonContacts.rawValue:
return .nonContacts
default:
return nil
}
}
self.additionalPeerList.categories = Set(mappedCategories)
self.additionalPeerList.peers.removeAll()
for id in peerIds {
guard let maybePeer = peerMap[id], let peer = maybePeer else {
continue
}
self.additionalPeerList.peers.append(AdditionalPeerList.Peer(
peer: peer,
isContact: isContactMap[id] ?? false
))
}
self.additionalPeerList.peers.sort(by: { lhs, rhs in
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
})
self.state?.updated(transition: .immediate)
controller?.dismiss()
})
})
self.environment?.controller()?.push(controller)
}
private func openMessageList() {
guard let component = self.component else {
return
}
let shortcutName: String
let shortcutType: ChatQuickReplyShortcutType
switch component.mode {
case .greeting:
shortcutName = "hello"
shortcutType = .greeting
case .away:
shortcutName = "away"
shortcutType = .away
}
let contents = AutomaticBusinessMessageSetupChatContents(
context: component.context,
kind: .quickReplyMessageInput(shortcut: shortcutName, shortcutType: shortcutType),
shortcutId: self.currentShortcut?.id
)
let chatController = component.context.sharedContext.makeChatController(
context: component.context,
chatLocation: .customChatContents,
subject: .customChatContents(contents: contents),
botStart: nil,
mode: .standard(.default)
)
chatController.navigationPresentation = .modal
self.environment?.controller()?.push(chatController)
}
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
guard let component = self.component else {
return
}
let currentValue: Date = (isStartTime ? self.customScheduleStart : self.customScheduleEnd) ?? Date()
if isDate {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
let components = calendar.dateComponents([.year, .month, .day], from: currentValue)
guard let clippedDate = calendar.date(from: components) else {
return
}
let controller = DateSelectionActionSheetController(
context: component.context,
title: nil,
currentValue: Int32(clippedDate.timeIntervalSince1970),
minimumDate: nil,
maximumDate: nil,
emptyTitle: nil,
applyValue: { [weak self] value in
guard let self else {
return
}
guard let value else {
return
}
let updatedDate = Date(timeIntervalSince1970: Double(value))
let calendar = Calendar.current
var updatedComponents = calendar.dateComponents([.year, .month, .day], from: updatedDate)
let currentComponents = calendar.dateComponents([.hour, .minute], from: currentValue)
updatedComponents.hour = currentComponents.hour
updatedComponents.minute = currentComponents.minute
guard let updatedClippedDate = calendar.date(from: updatedComponents) else {
return
}
if isStartTime {
self.customScheduleStart = updatedClippedDate
} else {
self.customScheduleEnd = updatedClippedDate
}
self.state?.updated(transition: .immediate)
}
)
self.environment?.controller()?.present(controller, in: .window(.root))
} else {
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: currentValue)
let hour = components.hour ?? 0
let minute = components.minute ?? 0
let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(hour * 60 * 60 + minute * 60), applyValue: { [weak self] value in
guard let self else {
return
}
guard let value else {
return
}
let updatedHour = value / (60 * 60)
let updatedMinute = (value % (60 * 60)) / 60
let calendar = Calendar.current
var updatedComponents = calendar.dateComponents([.year, .month, .day], from: currentValue)
updatedComponents.hour = Int(updatedHour)
updatedComponents.minute = Int(updatedMinute)
guard let updatedClippedDate = calendar.date(from: updatedComponents) else {
return
}
if isStartTime {
self.customScheduleStart = updatedClippedDate
} else {
self.customScheduleEnd = updatedClippedDate
}
self.state?.updated(transition: .immediate)
})
self.environment?.controller()?.present(controller, in: .window(.root))
}
}
func update(component: AutomaticBusinessMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.accountPeer = component.initialData.accountPeer
var initialRecipients: TelegramBusinessRecipients?
let shortcutName: String
switch component.mode {
case .greeting:
shortcutName = "hello"
if let greetingMessage = component.initialData.greetingMessage {
self.isOn = true
initialRecipients = greetingMessage.recipients
self.inactivityDays = greetingMessage.inactivityDays
}
case .away:
shortcutName = "away"
if let awayMessage = component.initialData.awayMessage {
self.isOn = true
self.sendWhenOffline = awayMessage.sendWhenOffline
initialRecipients = awayMessage.recipients
switch awayMessage.schedule {
case .always:
self.schedule = .always
case let .custom(beginTimestamp, endTimestamp):
self.schedule = .custom
self.customScheduleStart = Date(timeIntervalSince1970: Double(beginTimestamp))
self.customScheduleEnd = Date(timeIntervalSince1970: Double(endTimestamp))
case .outsideWorkingHours:
if component.initialData.businessHours != nil {
self.schedule = .outsideBusinessHours
} else {
self.schedule = .always
}
}
}
}
if let initialRecipients {
var mappedCategories = Set<AdditionalPeerList.Category>()
if initialRecipients.categories.contains(.existingChats) {
mappedCategories.insert(.existingChats)
}
if initialRecipients.categories.contains(.newChats) {
mappedCategories.insert(.newChats)
}
if initialRecipients.categories.contains(.contacts) {
mappedCategories.insert(.contacts)
}
if initialRecipients.categories.contains(.nonContacts) {
mappedCategories.insert(.nonContacts)
}
var additionalPeers: [AdditionalPeerList.Peer] = []
for peerId in initialRecipients.additionalPeers {
if let peer = component.initialData.additionalPeers[peerId] {
additionalPeers.append(peer)
}
}
self.additionalPeerList = AdditionalPeerList(
categories: mappedCategories,
peers: additionalPeers
)
self.hasAccessToAllChatsByDefault = initialRecipients.exclude
}
self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false)
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
guard let self else {
return
}
let shortcut = shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
if shortcut != self.currentShortcut {
self.currentShortcut = shortcut
self.state?.updated(transition: .immediate)
}
})
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut))
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_TitleGreetingMessage : environment.strings.BusinessMessageSetup_TitleAwayMessage, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(navigationTitleView)
}
}
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: component.mode == .greeting ? "HandWaveEmoji" : "ZzzEmoji"),
loop: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 8.0), size: iconSize)
if let iconView = self.icon.view as? LottieComponent.View {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
iconView.playOnce()
}
transition.setPosition(view: iconView, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 124.0
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(component.mode == .greeting ? environment.strings.BusinessMessageSetup_TextGreetingMessage : environment.strings.BusinessMessageSetup_TextAwayMessage, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { attributes in
return ("URL", "")
}), textAlignment: .center
))
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
text: .plain(subtitleString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.25,
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
},
tapAction: { [weak self] _, _ in
guard let self, let component = self.component else {
return
}
let _ = component
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.scrollView.addSubview(subtitleView)
}
transition.setPosition(view: subtitleView, position: subtitleFrame.center)
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
}
contentHeight += subtitleSize.height
contentHeight += 27.0
var generalSectionItems: [AnyComponentWithIdentity<Empty>] = []
generalSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_ToggleGreetingMessage : environment.strings.BusinessMessageSetup_ToggleAwayMessage,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isOn, action: { [weak self] _ in
guard let self else {
return
}
self.isOn = !self.isOn
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))))
let generalSectionSize = self.generalSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: generalSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize)
if let generalSectionView = self.generalSection.view {
if generalSectionView.superview == nil {
self.scrollView.addSubview(generalSectionView)
}
transition.setFrame(view: generalSectionView, frame: generalSectionFrame)
}
contentHeight += generalSectionSize.height
contentHeight += sectionSpacing
var otherSectionsHeight: CGFloat = 0.0
var messagesSectionItems: [AnyComponentWithIdentity<Empty>] = []
if let currentShortcut = self.currentShortcut {
if let accountPeer = self.accountPeer {
messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
accountPeer: accountPeer,
message: currentShortcut.topMessage,
count: currentShortcut.totalCount,
action: { [weak self] in
guard let self else {
return
}
self.openMessageList()
}
))))
}
} else {
messagesSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_CreateGreetingMessage : environment.strings.BusinessMessageSetup_CreateAwayMessage,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/ComposeIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openMessageList()
}
))))
}
let messagesSectionSize = self.messagesSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? environment.strings.BusinessMessageSetup_GreetingMessageSectionHeader : environment.strings.BusinessMessageSetup_AwayMessageSectionHeader,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: messagesSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let messagesSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: messagesSectionSize)
if let messagesSectionView = self.messagesSection.view {
if messagesSectionView.superview == nil {
messagesSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(messagesSectionView)
}
transition.setFrame(view: messagesSectionView, frame: messagesSectionFrame)
alphaTransition.setAlpha(view: messagesSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += messagesSectionSize.height
otherSectionsHeight += sectionSpacing
if case .away = component.mode {
var scheduleSectionItems: [AnyComponentWithIdentity<Empty>] = []
optionLoop: for i in 0 ..< 3 {
let title: String
let schedule: Schedule
switch i {
case 0:
title = environment.strings.BusinessMessageSetup_ScheduleAlways
schedule = .always
case 1:
if component.initialData.businessHours == nil {
continue optionLoop
}
title = environment.strings.BusinessMessageSetup_ScheduleOutsideBusinessHours
schedule = .outsideBusinessHours
default:
title = environment.strings.BusinessMessageSetup_ScheduleCustom
schedule = .custom
}
let isSelected = self.schedule == schedule
scheduleSectionItems.append(AnyComponentWithIdentity(id: scheduleSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
if self.schedule != schedule {
self.schedule = schedule
self.state?.updated(transition: .immediate)
}
}
))))
}
let scheduleSectionSize = self.scheduleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_ScheduleSectionHeader,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: scheduleSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let scheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: scheduleSectionSize)
if let scheduleSectionView = self.scheduleSection.view {
if scheduleSectionView.superview == nil {
scheduleSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(scheduleSectionView)
}
transition.setFrame(view: scheduleSectionView, frame: scheduleSectionFrame)
alphaTransition.setAlpha(view: scheduleSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += scheduleSectionSize.height
otherSectionsHeight += sectionSpacing
var customScheduleSectionsHeight: CGFloat = 0.0
var customScheduleSectionItems: [AnyComponentWithIdentity<Empty>] = []
for i in 0 ..< 2 {
let title: String
let itemDate: Date?
let isStartTime: Bool
switch i {
case 0:
title = environment.strings.BusinessMessageSetup_ScheduleStartTime
itemDate = self.customScheduleStart
isStartTime = true
default:
title = environment.strings.BusinessMessageSetup_ScheduleEndTime
itemDate = self.customScheduleEnd
isStartTime = false
}
var icon: ListActionItemComponent.Icon?
var accessory: ListActionItemComponent.Accessory?
if let itemDate {
let calendar = Calendar.current
let hours = calendar.component(.hour, from: itemDate)
let minutes = calendar.component(.minute, from: itemDate)
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let timeText = stringForShortTimestamp(hours: Int32(hours), minutes: Int32(minutes), dateTimeFormat: presentationData.dateTimeFormat)
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .none
dateFormatter.dateStyle = .medium
let dateText = stringForCompactDate(timestamp: Int32(itemDate.timeIntervalSince1970), strings: environment.strings, dateTimeFormat: presentationData.dateTimeFormat)
icon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: dateText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true)
},
animateAlpha: true,
animateScale: false
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: timeText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: false)
},
animateAlpha: true,
animateScale: false
)))
], spacing: 4.0))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true)
} else {
icon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_ScheduleTimePlaceholder,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))))
accessory = .arrow
}
customScheduleSectionItems.append(AnyComponentWithIdentity(id: customScheduleSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: icon,
accessory: accessory,
action: itemDate != nil ? nil : { [weak self] _ in
guard let self else {
return
}
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true)
}
))))
}
let customScheduleSectionSize = self.customScheduleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: customScheduleSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let customScheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight + customScheduleSectionsHeight), size: customScheduleSectionSize)
if let customScheduleSectionView = self.customScheduleSection.view {
if customScheduleSectionView.superview == nil {
customScheduleSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(customScheduleSectionView)
}
transition.setFrame(view: customScheduleSectionView, frame: customScheduleSectionFrame)
alphaTransition.setAlpha(view: customScheduleSectionView, alpha: (self.isOn && self.schedule == .custom) ? 1.0 : 0.0)
}
customScheduleSectionsHeight += customScheduleSectionSize.height
customScheduleSectionsHeight += sectionSpacing
if self.schedule == .custom {
otherSectionsHeight += customScheduleSectionsHeight
}
}
if case .away = component.mode {
let sendWhenOfflineSectionSize = self.sendWhenOfflineSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .markdown(
text: environment.strings.BusinessMessageSetup_SendWhenOfflineFooter,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { _ in
return nil
}
)
),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_SendWhenOffline,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: nil,
accessory: .toggle(ListActionItemComponent.Toggle(
style: .regular,
isOn: self.sendWhenOffline,
action: { [weak self] value in
guard let self else {
return
}
self.sendWhenOffline = value
self.state?.updated(transition: .spring(duration: 0.4))
}
)),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let sendWhenOfflineSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: sendWhenOfflineSectionSize)
if let sendWhenOfflineSectionView = self.sendWhenOfflineSection.view {
if sendWhenOfflineSectionView.superview == nil {
sendWhenOfflineSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(sendWhenOfflineSectionView)
}
transition.setFrame(view: sendWhenOfflineSectionView, frame: sendWhenOfflineSectionFrame)
alphaTransition.setAlpha(view: sendWhenOfflineSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += sendWhenOfflineSectionSize.height
otherSectionsHeight += sectionSpacing
}
let accessSectionSize = self.accessSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_RecipientsSectionHeader,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_RecipientsOptionAllExcept,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
if !self.hasAccessToAllChatsByDefault {
self.hasAccessToAllChatsByDefault = true
self.additionalPeerList.categories.removeAll()
self.additionalPeerList.peers.removeAll()
self.state?.updated(transition: .immediate)
}
}
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_RecipientsOptionOnly,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
if self.hasAccessToAllChatsByDefault {
self.hasAccessToAllChatsByDefault = false
self.additionalPeerList.categories.removeAll()
self.additionalPeerList.peers.removeAll()
self.state?.updated(transition: .immediate)
}
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let accessSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: accessSectionSize)
if let accessSectionView = self.accessSection.view {
if accessSectionView.superview == nil {
accessSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(accessSectionView)
}
transition.setFrame(view: accessSectionView, frame: accessSectionFrame)
alphaTransition.setAlpha(view: accessSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += accessSectionSize.height
otherSectionsHeight += sectionSpacing
var excludedSectionItems: [AnyComponentWithIdentity<Empty>] = []
excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_AddExclude : environment.strings.BusinessMessageSetup_Recipients_AddInclude,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/AddIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openAdditionalPeerListSetup()
}
))))
for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) {
let title: String
let icon: String
let color: AvatarBackgroundColor
switch category {
case .newChats:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats
icon = "Chat List/Filters/NewChats"
color = .purple
case .existingChats:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats
icon = "Chat List/Filters/Chats"
color = .purple
case .contacts:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryContacts
icon = "Chat List/Filters/Contact"
color = .blue
case .nonContacts:
title = environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts
icon = "Chat List/Filters/User"
color = .yellow
}
excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
style: .generic,
sideInset: 0.0,
title: title,
avatar: PeerListItemComponent.Avatar(
icon: icon,
color: color,
clipStyle: .roundedRect
),
peer: nil,
subtitle: nil,
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
},
inlineActions: PeerListItemComponent.InlineActionsState(
actions: [PeerListItemComponent.InlineAction(
id: AnyHashable(0),
title: environment.strings.Common_Delete,
color: .destructive,
action: { [weak self] in
guard let self else {
return
}
self.additionalPeerList.categories.remove(category)
self.state?.updated(transition: .spring(duration: 0.4))
}
)]
)
))))
}
for peer in self.additionalPeerList.peers {
excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
style: .generic,
sideInset: 0.0,
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer.peer,
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser,
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
},
inlineActions: PeerListItemComponent.InlineActionsState(
actions: [PeerListItemComponent.InlineAction(
id: AnyHashable(0),
title: environment.strings.Common_Delete,
color: .destructive,
action: { [weak self] in
guard let self else {
return
}
self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id })
self.state?.updated(transition: .spring(duration: 0.4))
}
)]
)
))))
}
let excludedSectionSize = self.excludedSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludedSectionHeader : environment.strings.BusinessMessageSetup_Recipients_IncludedSectionHeader,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .markdown(
text: component.mode == .greeting ? environment.strings.BusinessMessageSetup_Recipients_GreetingMessageFooter : environment.strings.BusinessMessageSetup_Recipients_AwayMessageFooter,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { _ in
return nil
}
)
),
maximumNumberOfLines: 0
)),
items: excludedSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let excludedSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: excludedSectionSize)
if let excludedSectionView = self.excludedSection.view {
if excludedSectionView.superview == nil {
excludedSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(excludedSectionView)
}
transition.setFrame(view: excludedSectionView, frame: excludedSectionFrame)
alphaTransition.setAlpha(view: excludedSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += excludedSectionSize.height
otherSectionsHeight += sectionSpacing
if case .greeting = component.mode {
var selectedInactivityIndex = 0
let valueList: [Int] = [
7,
14,
21,
28
]
for i in 0 ..< valueList.count {
if valueList[i] <= self.inactivityDays {
selectedInactivityIndex = i
}
}
let periodSectionSize = self.periodSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_InactivitySectionHeader,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.BusinessMessageSetup_InactivitySectionFooter,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
theme: environment.theme,
values: valueList.map { item in
return environment.strings.MessageTimer_Days(Int32(item))
},
markPositions: true,
selectedIndex: selectedInactivityIndex,
title: nil,
selectedIndexUpdated: { [weak self] index in
guard let self else {
return
}
let index = max(0, min(valueList.count - 1, index))
self.inactivityDays = valueList[index]
self.state?.updated(transition: .immediate)
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let periodSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: periodSectionSize)
if let periodSectionView = self.periodSection.view {
if periodSectionView.superview == nil {
periodSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(periodSectionView)
}
transition.setFrame(view: periodSectionView, frame: periodSectionFrame)
alphaTransition.setAlpha(view: periodSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += periodSectionSize.height
otherSectionsHeight += sectionSpacing
}
if self.isOn {
contentHeight += otherSectionsHeight
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
self.ignoreScrolling = true
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
self.ignoreScrolling = false
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentContainer {
public final class InitialData: AutomaticBusinessMessageSetupScreenInitialData {
fileprivate let accountPeer: EnginePeer?
fileprivate let shortcutMessageList: ShortcutMessageList
fileprivate let greetingMessage: TelegramBusinessGreetingMessage?
fileprivate let awayMessage: TelegramBusinessAwayMessage?
fileprivate let additionalPeers: [EnginePeer.Id: AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer]
fileprivate let businessHours: TelegramBusinessHours?
fileprivate init(
accountPeer: EnginePeer?,
shortcutMessageList: ShortcutMessageList,
greetingMessage: TelegramBusinessGreetingMessage?,
awayMessage: TelegramBusinessAwayMessage?,
additionalPeers: [EnginePeer.Id: AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer],
businessHours: TelegramBusinessHours?
) {
self.accountPeer = accountPeer
self.shortcutMessageList = shortcutMessageList
self.greetingMessage = greetingMessage
self.awayMessage = awayMessage
self.additionalPeers = additionalPeers
self.businessHours = businessHours
}
}
public enum Mode {
case greeting
case away
}
private let context: AccountContext
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
self.context = context
super.init(context: context, component: AutomaticBusinessMessageSetupScreenComponent(
context: context,
initialData: initialData,
mode: mode
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = ""
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? AutomaticBusinessMessageSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? AutomaticBusinessMessageSetupScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public static func initialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> {
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.BusinessGreetingMessage(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: context.account.peerId)
),
context.engine.accountData.shortcutMessageList(onlyRemote: true)
|> take(1)
)
|> mapToSignal { data, shortcutMessageList -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> in
let (accountPeer, greetingMessage, awayMessage, businessHours) = data
var additionalPeerIds = Set<EnginePeer.Id>()
if let greetingMessage {
additionalPeerIds.formUnion(greetingMessage.recipients.additionalPeers)
}
if let awayMessage {
additionalPeerIds.formUnion(awayMessage.recipients.additionalPeers)
}
return context.engine.data.get(
EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))),
EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:)))
)
|> map { peers, isContacts -> AutomaticBusinessMessageSetupScreenInitialData in
var additionalPeers: [EnginePeer.Id: AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer] = [:]
for id in additionalPeerIds {
guard let peer = peers[id], let peer else {
continue
}
additionalPeers[id] = AutomaticBusinessMessageSetupScreenComponent.AdditionalPeerList.Peer(
peer: peer,
isContact: isContacts[id] ?? false
)
}
return InitialData(
accountPeer: accountPeer,
shortcutMessageList: shortcutMessageList,
greetingMessage: greetingMessage,
awayMessage: awayMessage,
additionalPeers: additionalPeers,
businessHours: businessHours
)
}
}
}
}