[WIP] Business

This commit is contained in:
Isaac 2024-02-13 20:13:40 +04:00
parent 71a40dcdb2
commit d9fec0a500
49 changed files with 5678 additions and 128 deletions

View File

@ -934,6 +934,9 @@ public protocol SharedAccountContext: AnyObject {
func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController
func makeBusinessHoursSetupScreen(context: AccountContext) -> ViewController
func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>

View File

@ -45,6 +45,7 @@ public enum ContactMultiselectionControllerMode {
public var chatListFilters: [ChatListFilter]?
public var displayAutoremoveTimeout: Bool
public var displayPresence: Bool
public var onlyUsers: Bool
public init(
title: String,
@ -53,7 +54,8 @@ public enum ContactMultiselectionControllerMode {
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
chatListFilters: [ChatListFilter]?,
displayAutoremoveTimeout: Bool = false,
displayPresence: Bool = false
displayPresence: Bool = false,
onlyUsers: Bool = false
) {
self.title = title
self.searchPlaceholder = searchPlaceholder
@ -62,6 +64,7 @@ public enum ContactMultiselectionControllerMode {
self.chatListFilters = chatListFilters
self.displayAutoremoveTimeout = displayAutoremoveTimeout
self.displayPresence = displayPresence
self.onlyUsers = onlyUsers
}
}

View File

@ -3185,7 +3185,7 @@ public final class ChatListNode: ListView {
}
private func resetFilter() {
if let chatListFilter = self.chatListFilter {
if let chatListFilter = self.chatListFilter, chatListFilter.id != Int32.max {
self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters()
|> map { filters -> ChatListFilter? in
for filter in filters {
@ -4113,14 +4113,16 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
//TODO:localize
return ("non-contact", false, false, nil)
}
}
} else if case .secretChat = peer {
if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
//TODO:localize
return ("non-contact", false, false, nil)
}
} else if case .legacyGroup = peer {
return (strings.ChatList_PeerTypeGroup, false, false, nil)

View File

@ -67,7 +67,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
}
if !filter.categories.contains(.contacts) && isContact {
if let user = peer as? TelegramUser {
if user.botInfo == nil {
if user.botInfo == nil && !user.flags.contains(.isSupport) {
return false
}
} else if let _ = peer as? TelegramSecretChat {
@ -88,7 +88,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
}
if !filter.categories.contains(.bots) {
if let user = peer as? TelegramUser {
if user.botInfo != nil {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return false
}
}

View File

@ -722,11 +722,11 @@ public extension CombinedComponent {
updatedChild.view.layer.shadowRadius = 0.0
updatedChild.view.layer.shadowOpacity = 0.0
}
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition, isLocal in
guard let viewContext = viewContext else {
return
}
viewContext.state.updated(transition: transition)
viewContext.state.updated(transition: transition, isLocal: isLocal)
}
if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide {

View File

@ -89,15 +89,15 @@ extension UIView {
}
open class ComponentState {
open var _updated: ((Transition) -> Void)?
open var _updated: ((Transition, Bool) -> Void)?
var isUpdated: Bool = false
public init() {
}
public final func updated(transition: Transition = .immediate) {
public final func updated(transition: Transition = .immediate, isLocal: Bool = false) {
self.isUpdated = true
self._updated?(transition)
self._updated?(transition, isLocal)
}
}

View File

@ -8,16 +8,16 @@ public final class RoundedRectangle: Component {
}
public let colors: [UIColor]
public let cornerRadius: CGFloat
public let cornerRadius: CGFloat?
public let gradientDirection: GradientDirection
public let stroke: CGFloat?
public let strokeColor: UIColor?
public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor)
}
public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
self.colors = colors
self.cornerRadius = cornerRadius
self.gradientDirection = gradientDirection
@ -49,8 +49,10 @@ public final class RoundedRectangle: Component {
func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component != component {
let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5
if component.colors.count == 1, let color = component.colors.first {
let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0)
let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
if let strokeColor = component.strokeColor {
@ -69,13 +71,13 @@ public final class RoundedRectangle: Component {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
UIGraphicsEndImageContext()
} else if component.colors.count > 1 {
let imageSize = availableSize
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: component.cornerRadius).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath)
context.clip()
let colors = component.colors
@ -93,12 +95,12 @@ public final class RoundedRectangle: Component {
if let stroke = component.stroke, stroke > 0.0 {
context.resetClip()
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: component.cornerRadius).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: cornerRadius).cgPath)
context.setBlendMode(.clear)
context.fill(CGRect(origin: .zero, size: imageSize))
}
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
UIGraphicsEndImageContext()
}
}

View File

@ -82,7 +82,7 @@ public final class ComponentHostView<EnvironmentType>: UIView {
self.currentComponent = component
self.currentContainerSize = containerSize
componentState._updated = { [weak self] transition in
componentState._updated = { [weak self] transition, _ in
guard let strongSelf = self else {
return
}
@ -208,11 +208,11 @@ public final class ComponentView<EnvironmentType> {
self.currentComponent = component
self.currentContainerSize = containerSize
componentState._updated = { [weak self] transition in
componentState._updated = { [weak self] transition, isLocal in
guard let strongSelf = self else {
return
}
if let parentState = strongSelf.parentState {
if !isLocal, let parentState = strongSelf.parentState {
parentState.updated(transition: transition)
} else {
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {

View File

@ -567,6 +567,9 @@
updateGroupingButtonVisibility();
} file:__FILE_NAME__ line:__LINE__]];
if (_adjustmentsChangedDisposable) {
[_adjustmentsChangedDisposable dispose];
}
_adjustmentsChangedDisposable = [[SMetaDisposable alloc] init];
[_adjustmentsChangedDisposable setDisposable:[_editingContext.adjustmentsUpdatedSignal startStrictWithNext:^(__unused NSNumber *next)
{
@ -583,6 +586,7 @@
self.delegate = nil;
[_selectionChangedDisposable dispose];
[_tooltipDismissDisposable dispose];
[_adjustmentsChangedDisposable dispose];
}
- (void)loadView

View File

@ -187,7 +187,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab
if ["home", "work"].contains(venueType) {
completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil)
} else {
completion(venue, queryId, resultId, nil, nil)
completion(venue, queryId, resultId, venue.venue?.address, nil)
}
strongSelf.dismiss()
}, toggleMapModeSelection: { [weak self] in

View File

@ -11,10 +11,41 @@ private func drawBorder(context: CGContext, rect: CGRect) {
context.strokePath()
}
private func renderIcon(name: String) -> UIImage? {
private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) {
context.saveGState()
context.translateBy(x: rect.minX, y: rect.minY)
context.scaleBy(x: radius, y: radius)
let fw = rect.width / radius
let fh = rect.height / radius
context.move(to: CGPoint(x: fw, y: fh / 2.0))
context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0)
context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1)
context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1)
context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1)
context.closePath()
context.restoreGState()
}
private func renderIcon(name: String, backgroundColors: [UIColor]? = nil) -> UIImage? {
return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
if let backgroundColors {
addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: 7.0)
context.clip()
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = backgroundColors.map(\.cgColor)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
context.resetClip()
}
if let image = UIImage(bundleImageName: name)?.cgImage {
context.draw(image, in: bounds)
}
@ -38,6 +69,7 @@ public struct PresentationResourcesSettings {
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
public static let stories = renderIcon(name: "Settings/Menu/Stories")
public static let premiumGift = renderIcon(name: "Settings/Menu/Gift")
public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)])
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -21,9 +21,9 @@ public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat
periodString = "AM"
}
if minutes >= 10 {
return "\(hourString):\(minutes) \(periodString)"
return "\(hourString):\(minutes)\u{00a0}\(periodString)"
} else {
return "\(hourString):0\(minutes) \(periodString)"
return "\(hourString):0\(minutes)\u{00a0}\(periodString)"
}
case .military:
return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)])

View File

@ -434,6 +434,9 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent",
"//submodules/TelegramUI/Components/Settings/BusinessSetupScreen",
"//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
"//submodules/TelegramUI/Components/Settings/GreetingMessageSetupScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -57,7 +57,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
super.init()
self.state._updated = { [weak self] transition in
self.state._updated = { [weak self] transition, _ in
if let self {
self.update(transition: transition.containedViewLayoutTransition)
}

View File

@ -7,15 +7,67 @@ import ListSectionComponent
import SwitchNode
public final class ListActionItemComponent: Component {
public enum ToggleStyle {
case regular
case icons
}
public struct Toggle: Equatable {
public var style: ToggleStyle
public var isOn: Bool
public var isInteractive: Bool
public var action: ((Bool) -> Void)?
public init(style: ToggleStyle, isOn: Bool, isInteractive: Bool = true, action: ((Bool) -> Void)? = nil) {
self.style = style
self.isOn = isOn
self.isInteractive = isInteractive
self.action = action
}
public static func ==(lhs: Toggle, rhs: Toggle) -> Bool {
if lhs.style != rhs.style {
return false
}
if lhs.isOn != rhs.isOn {
return false
}
if lhs.isInteractive != rhs.isInteractive {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
}
public enum Accessory: Equatable {
case arrow
case toggle(Bool)
case toggle(Toggle)
}
public enum IconInsets: Equatable {
case `default`
case custom(UIEdgeInsets)
}
public struct Icon: Equatable {
public var component: AnyComponentWithIdentity<Empty>
public var insets: IconInsets
public var allowUserInteraction: Bool
public init(component: AnyComponentWithIdentity<Empty>, insets: IconInsets = .default, allowUserInteraction: Bool = false) {
self.component = component
self.insets = insets
self.allowUserInteraction = allowUserInteraction
}
}
public let theme: PresentationTheme
public let title: AnyComponent<Empty>
public let leftIcon: AnyComponentWithIdentity<Empty>?
public let icon: AnyComponentWithIdentity<Empty>?
public let icon: Icon?
public let accessory: Accessory?
public let action: ((UIView) -> Void)?
@ -23,7 +75,7 @@ public final class ListActionItemComponent: Component {
theme: PresentationTheme,
title: AnyComponent<Empty>,
leftIcon: AnyComponentWithIdentity<Empty>? = nil,
icon: AnyComponentWithIdentity<Empty>? = nil,
icon: Icon? = nil,
accessory: Accessory? = .arrow,
action: ((UIView) -> Void)?
) {
@ -63,7 +115,8 @@ public final class ListActionItemComponent: Component {
private var icon: ComponentView<Empty>?
private var arrowView: UIImageView?
private var switchNode: IconSwitchNode?
private var switchNode: SwitchNode?
private var iconSwitchNode: IconSwitchNode?
private var component: ListActionItemComponent?
@ -83,7 +136,10 @@ public final class ListActionItemComponent: Component {
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.internalHighligthedChanged = { [weak self] isHighlighted in
guard let self else {
guard let self, let component = self.component, component.action != nil else {
return
}
if case .toggle = component.accessory, component.action == nil {
return
}
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
@ -97,15 +153,23 @@ public final class ListActionItemComponent: Component {
}
@objc private func pressed() {
self.component?.action?(self)
guard let component, let action = component.action else {
return
}
action(self)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
return result
}
func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component
self.isEnabled = component.action != nil
let themeUpdated = component.theme !== previousComponent?.theme
let verticalInset: CGFloat = 12.0
@ -118,7 +182,7 @@ public final class ListActionItemComponent: Component {
case .arrow:
contentRightInset = 30.0
case .toggle:
contentRightInset = 42.0
contentRightInset = 76.0
}
var contentHeight: CGFloat = 0.0
@ -147,7 +211,7 @@ public final class ListActionItemComponent: Component {
contentHeight += verticalInset
if let iconValue = component.icon {
if previousComponent?.icon?.id != iconValue.id, let icon = self.icon {
if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon {
self.icon = nil
if let iconView = icon.view {
transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in
@ -168,17 +232,17 @@ public final class ListActionItemComponent: Component {
let iconSize = icon.update(
transition: iconTransition,
component: iconValue.component,
component: iconValue.component.component,
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
if let iconView = icon.view {
if iconView.superview == nil {
iconView.isUserInteractionEnabled = false
self.addSubview(iconView)
transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
}
iconView.isUserInteractionEnabled = iconValue.allowUserInteraction
iconTransition.setFrame(view: iconView, frame: iconFrame)
}
} else {
@ -263,32 +327,85 @@ public final class ListActionItemComponent: Component {
}
}
if case let .toggle(isOn) = component.accessory {
let switchNode: IconSwitchNode
var switchTransition = transition
var updateSwitchTheme = themeUpdated
if let current = self.switchNode {
switchNode = current
switchNode.setOn(isOn, animated: !transition.animation.isImmediate)
} else {
switchTransition = switchTransition.withAnimation(.none)
updateSwitchTheme = true
switchNode = IconSwitchNode()
switchNode.setOn(isOn, animated: false)
self.addSubview(switchNode.view)
if case let .toggle(toggle) = component.accessory {
switch toggle.style {
case .regular:
let switchNode: SwitchNode
var switchTransition = transition
var updateSwitchTheme = themeUpdated
if let current = self.switchNode {
switchNode = current
switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate)
} else {
switchTransition = switchTransition.withAnimation(.none)
updateSwitchTheme = true
switchNode = SwitchNode()
switchNode.setOn(toggle.isOn, animated: false)
self.switchNode = switchNode
self.addSubview(switchNode.view)
switchNode.valueUpdated = { [weak self] value in
guard let self, let component = self.component else {
return
}
if case let .toggle(toggle) = component.accessory, let action = toggle.action {
action(value)
} else {
component.action?(self)
}
}
}
switchNode.isUserInteractionEnabled = toggle.isInteractive
if updateSwitchTheme {
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
}
let switchSize = CGSize(width: 51.0, height: 31.0)
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
case .icons:
let switchNode: IconSwitchNode
var switchTransition = transition
var updateSwitchTheme = themeUpdated
if let current = self.iconSwitchNode {
switchNode = current
switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate)
} else {
switchTransition = switchTransition.withAnimation(.none)
updateSwitchTheme = true
switchNode = IconSwitchNode()
switchNode.setOn(toggle.isOn, animated: false)
self.iconSwitchNode = switchNode
self.addSubview(switchNode.view)
switchNode.valueUpdated = { [weak self] value in
guard let self, let component = self.component else {
return
}
if case let .toggle(toggle) = component.accessory, let action = toggle.action {
action(value)
} else {
component.action?(self)
}
}
}
switchNode.isUserInteractionEnabled = toggle.isInteractive
if updateSwitchTheme {
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor
switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor
}
let switchSize = CGSize(width: 51.0, height: 31.0)
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
}
if updateSwitchTheme {
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor
switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor
}
let switchSize = CGSize(width: 51.0, height: 31.0)
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
} else {
if let switchNode = self.switchNode {
self.switchNode = nil

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ListItemSliderSelectorComponent",
module_name = "ListItemSliderSelectorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/SliderComponent,
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,460 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import LegacyUI
import ComponentFlow
import MultilineTextComponent
import SliderComponent
final class ListItemSliderSelectorComponent: Component {
typealias EnvironmentType = Empty
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
let isEnabled: Bool
let trackColor: UIColor?
let displayValue: Bool
let valueUpdated: (Float) -> Void
let isTrackingUpdated: ((Bool) -> Void)?
init(
title: String,
value: Float,
minValue: Float,
maxValue: Float,
startValue: Float,
isEnabled: Bool,
trackColor: UIColor?,
displayValue: Bool,
valueUpdated: @escaping (Float) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil
) {
self.title = title
self.value = value
self.minValue = minValue
self.maxValue = maxValue
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.minValue != rhs.minValue {
return false
}
if lhs.maxValue != rhs.maxValue {
return false
}
if lhs.startValue != rhs.startValue {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.trackColor != rhs.trackColor {
return false
}
if lhs.displayValue != rhs.displayValue {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let title = ComponentView<Empty>()
private let value = ComponentView<Empty>()
private var sliderView: TGPhotoEditorSliderView?
private var component: ListItemSliderSelectorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
var internalIsTrackingUpdated: ((Bool) -> Void)?
if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in
if let self {
if isTracking {
self.sliderView?.bordered = true
} else {
Queue.mainQueue().after(0.1) {
self.sliderView?.bordered = false
}
}
isTrackingUpdated(isTracking)
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
}
if let valueView = self.value.view {
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
let sliderView: TGPhotoEditorSliderView
if let current = self.sliderView {
sliderView = current
sliderView.value = CGFloat(component.value)
} else {
sliderView = TGPhotoEditorSliderView()
sliderView.backgroundColor = .clear
sliderView.startColor = UIColor(rgb: 0xffffff)
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = CGFloat(component.minValue)
sliderView.maximumValue = CGFloat(component.maxValue)
sliderView.startValue = CGFloat(component.startValue)
sliderView.value = CGFloat(component.value)
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
sliderView.layer.allowsGroupOpacity = true
self.sliderView = sliderView
self.addSubview(sliderView)
}
sliderView.interactionBegan = {
internalIsTrackingUpdated?(true)
}
sliderView.interactionEnded = {
internalIsTrackingUpdated?(false)
}
if component.isEnabled {
sliderView.alpha = 1.3
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
sliderView.isUserInteractionEnabled = true
} else {
sliderView.trackColor = UIColor(rgb: 0xffffff)
sliderView.alpha = 0.3
sliderView.isUserInteractionEnabled = false
}
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
}
let valueText: String
if component.displayValue {
if component.value > 0.005 {
valueText = String(format: "+%.2f", component.value)
} else if component.value < -0.005 {
valueText = String(format: "%.2f", component.value)
} else {
valueText = ""
}
} else {
valueText = ""
}
let valueSize = self.value.update(
transition: .immediate,
component: AnyComponent(
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xf8d74a))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let valueView = self.value.view {
if valueView.superview == nil {
self.addSubview(valueView)
}
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
}
return CGSize(width: availableSize.width, height: 52.0)
}
@objc private func sliderValueChanged() {
guard let component = self.component, let sliderView = self.sliderView else {
return
}
component.valueUpdated(Float(sliderView.value))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
struct AdjustmentTool: Equatable {
let key: EditorToolKey
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
}
final class AdjustmentsComponent: Component {
typealias EnvironmentType = Empty
let tools: [AdjustmentTool]
let valueUpdated: (EditorToolKey, Float) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
tools: [AdjustmentTool],
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.tools = tools
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
if lhs.tools != rhs.tools {
return false
}
return true
}
final class View: UIView {
private let scrollView = UIScrollView()
private var toolViews: [ComponentView<Empty>] = []
private var component: AdjustmentsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let valueUpdated = component.valueUpdated
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
component.isTrackingUpdated(isTracking)
if let self {
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
if tool.key != trackingTool && i < self.toolViews.count {
if let view = self.toolViews[i].view {
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
}
var sizes: [CGSize] = []
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
let componentView: ComponentView<Empty>
if i >= self.toolViews.count {
componentView = ComponentView<Empty>()
self.toolViews.append(componentView)
} else {
componentView = self.toolViews[i]
}
var valueIsNegative = false
var value = tool.value
if case .enhance = tool.key {
if value < 0.0 {
valueIsNegative = true
}
value = abs(value)
}
let size = componentView.update(
transition: transition,
component: AnyComponent(
ListItemSliderSelectorComponent(
title: tool.title,
value: value,
minValue: tool.minValue,
maxValue: tool.maxValue,
startValue: tool.startValue,
isEnabled: true,
trackColor: nil,
displayValue: true,
valueUpdated: { value in
var updatedValue = value
if valueIsNegative {
updatedValue *= -1.0
}
valueUpdated(tool.key, updatedValue)
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(tool.key, isTracking)
}
)
),
environment: {},
containerSize: availableSize
)
sizes.append(size)
}
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
for i in 0 ..< component.tools.count {
let size = sizes[i]
let componentView = self.toolViews[i]
if let view = componentView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
}
origin = origin.offsetBy(dx: 0.0, dy: size.height)
}
let size = CGSize(width: availableSize.width, height: 180.0)
let contentSize = CGSize(width: availableSize.width, height: origin.y)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class AdjustmentsScreenComponent: Component {
typealias EnvironmentType = Empty
let toggleUneditedPreview: (Bool) -> Void
init(
toggleUneditedPreview: @escaping (Bool) -> Void
) {
self.toggleUneditedPreview = toggleUneditedPreview
}
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var component: AdjustmentsScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.05
self.addGestureRecognizer(longPressGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.toggleUneditedPreview(true)
case .ended, .cancelled:
component.toggleUneditedPreview(false)
default:
break
}
}
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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)
}
}

View File

@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ListMultilineTextFieldItemComponent",
module_name = "ListMultilineTextFieldItemComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/AccountContext",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,249 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import MultilineTextComponent
import ListSectionComponent
import TextFieldComponent
import AccountContext
public final class ListMultilineTextFieldItemComponent: Component {
public final class ResetText: Equatable {
public let value: String
public init(value: String) {
self.value = value
}
public static func ==(lhs: ResetText, rhs: ResetText) -> Bool {
return lhs === rhs
}
}
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let initialText: String
public let resetText: ResetText?
public let placeholder: String
public let autocapitalizationType: UITextAutocapitalizationType
public let autocorrectionType: UITextAutocorrectionType
public let updated: ((String) -> Void)?
public let tag: AnyObject?
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
initialText: String,
resetText: ResetText? = nil,
placeholder: String,
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
updated: ((String) -> Void)?,
tag: AnyObject? = nil
) {
self.context = context
self.theme = theme
self.strings = strings
self.initialText = initialText
self.resetText = resetText
self.placeholder = placeholder
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.updated = updated
self.tag = tag
}
public static func ==(lhs: ListMultilineTextFieldItemComponent, rhs: ListMultilineTextFieldItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.initialText != rhs.initialText {
return false
}
if lhs.resetText != rhs.resetText {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if (lhs.updated == nil) != (rhs.updated == nil) {
return false
}
return true
}
private final class TextField: UITextField {
var sideInset: CGFloat = 0.0
override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
}
public final class View: UIView, UITextFieldDelegate, ListSectionComponent.ChildView, ComponentTaggedView {
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private let placeholder = ComponentView<Empty>()
private var component: ListMultilineTextFieldItemComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
public var currentText: String {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
return textFieldView.inputState.inputText.string
} else {
return ""
}
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
super.init(frame: CGRect())
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return true
}
@objc private func textDidChange() {
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
self.component?.updated?(self.currentText)
}
public func setText(text: String, updateState: Bool) {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
//TODO
let _ = textFieldView
}
if updateState {
self.component?.updated?(self.currentText)
} else {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
let verticalInset: CGFloat = 12.0
let sideInset: CGFloat = 16.0
let textFieldSize = self.textField.update(
transition: transition,
component: AnyComponent(TextFieldComponent(
context: component.context,
strings: component.strings,
externalState: self.textFieldExternalState,
fontSize: 17.0,
textColor: component.theme.list.itemPrimaryTextColor,
insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0),
hideKeyboard: false,
customInputView: nil,
resetText: component.resetText.flatMap { resetText in
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)
},
isOneLineWhenUnfocused: false,
formatMenuAvailability: .none,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
}
)),
environment: {},
containerSize: availableSize
)
let size = textFieldSize
let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
self.textField.parentState = state
}
transition.setFrame(view: textFieldView, frame: textFieldFrame)
}
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.layer.anchorPoint = CGPoint()
placeholderView.isUserInteractionEnabled = false
self.insertSubview(placeholderView, at: 0)
}
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
placeholderView.isHidden = self.textFieldExternalState.hasText
}
self.separatorInset = 16.0
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -217,6 +217,7 @@ public final class ListSectionComponent: Component {
itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView()
self.itemViews[itemId] = itemView
itemView.contents.parentState = state
}
let itemSize = itemView.contents.update(

View File

@ -14,6 +14,9 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",

View File

@ -4,23 +4,35 @@ import Display
import ComponentFlow
import TelegramPresentationData
import MultilineTextComponent
import ListSectionComponent
import PlainButtonComponent
import BundleIconComponent
public final class ListTextFieldItemComponent: Component {
public let theme: PresentationTheme
public let initialText: String
public let placeholder: String
public let autocapitalizationType: UITextAutocapitalizationType
public let autocorrectionType: UITextAutocorrectionType
public let updated: ((String) -> Void)?
public let tag: AnyObject?
public init(
theme: PresentationTheme,
initialText: String,
placeholder: String,
updated: ((String) -> Void)?
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
updated: ((String) -> Void)?,
tag: AnyObject? = nil
) {
self.theme = theme
self.initialText = initialText
self.placeholder = placeholder
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.updated = updated
self.tag = tag
}
public static func ==(lhs: ListTextFieldItemComponent, rhs: ListTextFieldItemComponent) -> Bool {
@ -33,6 +45,12 @@ public final class ListTextFieldItemComponent: Component {
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if (lhs.updated == nil) != (rhs.updated == nil) {
return false
}
@ -51,9 +69,10 @@ public final class ListTextFieldItemComponent: Component {
}
}
public final class View: UIView, UITextFieldDelegate {
public final class View: UIView, UITextFieldDelegate, ListSectionComponent.ChildView, ComponentTaggedView {
private let textField: TextField
private let placeholder = ComponentView<Empty>()
private let clearButton = ComponentView<Empty>()
private var component: ListTextFieldItemComponent?
private weak var state: EmptyComponentState?
@ -63,6 +82,9 @@ public final class ListTextFieldItemComponent: Component {
return self.textField.text ?? ""
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
self.textField = TextField()
@ -81,6 +103,27 @@ public final class ListTextFieldItemComponent: Component {
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
self.component?.updated?(self.currentText)
}
public func setText(text: String, updateState: Bool) {
self.textField.text = text
if updateState {
self.state?.updated(transition: .immediate, isLocal: true)
self.component?.updated?(self.currentText)
} else {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
func update(component: ListTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
@ -102,6 +145,13 @@ public final class ListTextFieldItemComponent: Component {
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
}
if self.textField.autocapitalizationType != component.autocapitalizationType {
self.textField.autocapitalizationType = component.autocapitalizationType
}
if self.textField.autocorrectionType != component.autocorrectionType {
self.textField.autocorrectionType = component.autocorrectionType
}
let themeUpdated = component.theme !== previousComponent?.theme
if themeUpdated {
@ -120,7 +170,7 @@ public final class ListTextFieldItemComponent: Component {
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 30.0, height: 100.0)
)
let contentHeight: CGFloat = placeholderSize.height + verticalInset * 2.0
let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((contentHeight - placeholderSize.height) * 0.5)), size: placeholderSize)
@ -138,6 +188,37 @@ public final class ListTextFieldItemComponent: Component {
transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: contentHeight)))
let clearButtonSize = self.clearButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Components/Search Bar/Clear",
tintColor: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
)),
effectAlignment: .center,
minSize: CGSize(width: 44.0, height: 44.0),
action: { [weak self] in
guard let self else {
return
}
self.setText(text: "", updateState: true)
},
animateAlpha: false,
animateScale: true
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
if let clearButtonView = self.clearButton.view {
if clearButtonView.superview == nil {
self.addSubview(clearButtonView)
}
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 0.0 - clearButtonSize.width, y: floor((contentHeight - clearButtonSize.height) * 0.5)), size: clearButtonSize))
clearButtonView.isHidden = self.currentText.isEmpty
}
self.separatorInset = 16.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}

View File

@ -1098,7 +1098,8 @@ public final class MessageInputPanelComponent: Component {
contents = AnyComponent(PlainButtonComponent(
content: AnyComponent(VStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.3)))
text: .plain(NSAttributedString(string: title, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.3))),
maximumNumberOfLines: 2
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor))

View File

@ -927,7 +927,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
interaction.openSettings(.premium)
}))
//TODO:localize
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Telegram Business", icon: PresentationResourcesSettings.chatFolders, action: {
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Telegram Business", icon: PresentationResourcesSettings.business, action: {
interaction.openSettings(.businessSetup)
}))
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: {

View File

@ -11,6 +11,7 @@ public final class PlainButtonComponent: Component {
}
public let content: AnyComponent<Empty>
public let background: AnyComponent<Empty>?
public let effectAlignment: EffectAlignment
public let minSize: CGSize?
public let contentInsets: UIEdgeInsets
@ -23,6 +24,7 @@ public final class PlainButtonComponent: Component {
public init(
content: AnyComponent<Empty>,
background: AnyComponent<Empty>? = nil,
effectAlignment: EffectAlignment,
minSize: CGSize? = nil,
contentInsets: UIEdgeInsets = UIEdgeInsets(),
@ -34,6 +36,7 @@ public final class PlainButtonComponent: Component {
tag: AnyObject? = nil
) {
self.content = content
self.background = background
self.effectAlignment = effectAlignment
self.minSize = minSize
self.contentInsets = contentInsets
@ -49,6 +52,9 @@ public final class PlainButtonComponent: Component {
if lhs.content != rhs.content {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.effectAlignment != rhs.effectAlignment {
return false
}
@ -92,6 +98,7 @@ public final class PlainButtonComponent: Component {
private let contentContainer = UIView()
private let content = ComponentView<Empty>()
private var background: ComponentView<Empty>?
public var contentView: UIView? {
return self.content.view
@ -243,6 +250,33 @@ public final class PlainButtonComponent: Component {
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size))
transition.setPosition(view: self.contentContainer, position: CGPoint(x: size.width * anchorX, y: size.height * 0.5))
if let backgroundValue = component.background {
var backgroundTransition = transition
let background: ComponentView<Empty>
if let current = self.background {
background = current
} else {
backgroundTransition = .immediate
background = ComponentView()
self.background = background
}
let _ = background.update(
transition: backgroundTransition,
component: backgroundValue,
environment: {},
containerSize: size
)
if let backgroundView = background.view {
if backgroundView.superview == nil {
self.contentContainer.insertSubview(backgroundView, at: 0)
}
backgroundTransition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size))
}
} else if let background = self.background {
self.background = nil
background.view?.removeFromSuperview()
}
return size
}
}

View File

@ -0,0 +1,41 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BusinessHoursSetupScreen",
module_name = "BusinessHoursSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/LocationUI",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,631 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import ListSectionComponent
import ListActionItemComponent
import BundleIconComponent
import LottieComponent
import Markdown
import LocationUI
import TelegramStringFormatting
import PlainButtonComponent
final class BusinessDaySetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let dayIndex: Int
let day: BusinessHoursSetupScreenComponent.Day
init(
context: AccountContext,
dayIndex: Int,
day: BusinessHoursSetupScreenComponent.Day
) {
self.context = context
self.dayIndex = dayIndex
self.day = day
}
static func ==(lhs: BusinessDaySetupScreenComponent, rhs: BusinessDaySetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.dayIndex != rhs.dayIndex {
return false
}
if lhs.day != rhs.day {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let generalSection = ComponentView<Empty>()
private var rangeSections: [Int: ComponentView<Empty>] = [:]
private let addSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: BusinessDaySetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private(set) var isOpen: Bool = false
private(set) var ranges: [BusinessHoursSetupScreenComponent.WorkingHourRange] = []
private var nextRangeId: Int = 0
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 {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
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 openRangeDateSetup(rangeId: Int, isStartTime: Bool) {
guard let component = self.component else {
return
}
guard let range = self.ranges.first(where: { $0.id == rangeId }) else {
return
}
let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(isStartTime ? range.startTime : range.endTime), applyValue: { [weak self] value in
guard let self else {
return
}
guard let value else {
return
}
if let index = self.ranges.firstIndex(where: { $0.id == rangeId }) {
if isStartTime {
self.ranges[index].startTime = Int(value)
} else {
self.ranges[index].endTime = Int(value)
}
self.state?.updated(transition: .immediate)
}
})
self.environment?.controller()?.present(controller, in: .window(.root))
}
func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
if self.component == nil {
self.isOpen = component.day.ranges != nil
self.ranges = component.day.ranges ?? []
self.nextRangeId = (self.ranges.map(\.id).max() ?? 0) + 1
}
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let title: String
switch component.dayIndex {
case 0:
title = "Monday"
case 1:
title = "Tuesday"
case 2:
title = "Wednesday"
case 3:
title = "Thursday"
case 4:
title = "Friday"
case 5:
title = "Saturday"
case 6:
title = "Sunday"
default:
title = " "
}
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(navigationTitleView)
}
}
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let _ = bottomContentInset
let _ = sectionSpacing
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += 16.0
//TODO:localize
let generalSectionSize = self.generalSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Open On This Day",
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.isOpen, action: { [weak self] _ in
guard let self else {
return
}
self.isOpen = !self.isOpen
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize)
if let generalSectionView = self.generalSection.view {
if generalSectionView.superview == nil {
self.scrollView.addSubview(generalSectionView)
}
transition.setFrame(view: generalSectionView, frame: generalSectionFrame)
}
contentHeight += generalSectionSize.height
contentHeight += sectionSpacing
var rangesSectionsHeight: CGFloat = 0.0
for range in self.ranges {
let rangeId = range.id
var rangeSectionTransition = transition
let rangeSection: ComponentView<Empty>
if let current = self.rangeSections[range.id] {
rangeSection = current
} else {
rangeSection = ComponentView()
self.rangeSections[range.id] = rangeSection
rangeSectionTransition = rangeSectionTransition.withAnimation(.none)
}
let startHours = range.startTime / (60 * 60)
let startMinutes = range.startTime % (60 * 60)
let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat())
let endHours = range.endTime / (60 * 60)
let endMinutes = range.endTime % (60 * 60)
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat())
var rangeSectionItems: [AnyComponentWithIdentity<Empty>] = []
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Opening time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: startText, 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.openRangeDateSetup(rangeId: rangeId, isStartTime: true)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: true)
}
))))
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Closing time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: endText, 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.openRangeDateSetup(rangeId: rangeId, isStartTime: false)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: false)
}
))))
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Remove",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemDestructiveColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.ranges.removeAll(where: { $0.id == rangeId })
self.state?.updated(transition: .spring(duration: 0.4))
}
))))
let rangeSectionSize = rangeSection.update(
transition: rangeSectionTransition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: rangeSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let rangeSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + rangesSectionsHeight), size: rangeSectionSize)
if let rangeSectionView = rangeSection.view {
var animateIn = false
if rangeSectionView.superview == nil {
animateIn = true
rangeSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(rangeSectionView)
}
rangeSectionTransition.setFrame(view: rangeSectionView, frame: rangeSectionFrame)
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
if self.isOpen {
if animateIn {
if !transition.animation.isImmediate {
alphaTransition.animateAlpha(view: rangeSectionView, from: 0.0, to: 1.0)
transition.animateScale(view: rangeSectionView, from: 0.001, to: 1.0)
}
} else {
alphaTransition.setAlpha(view: rangeSectionView, alpha: 1.0)
}
} else {
alphaTransition.setAlpha(view: rangeSectionView, alpha: 0.0)
}
}
rangesSectionsHeight += rangeSectionSize.height
rangesSectionsHeight += sectionSpacing
}
var removeRangeSectionIds: [Int] = []
for (id, rangeSection) in self.rangeSections {
if !self.ranges.contains(where: { $0.id == id }) {
removeRangeSectionIds.append(id)
if let rangeSectionView = rangeSection.view {
if !transition.animation.isImmediate {
Transition.easeInOut(duration: 0.2).setAlpha(view: rangeSectionView, alpha: 0.0, completion: { [weak rangeSectionView] _ in
rangeSectionView?.removeFromSuperview()
})
transition.setScale(view: rangeSectionView, scale: 0.001)
} else {
rangeSectionView.removeFromSuperview()
}
}
}
}
for id in removeRangeSectionIds {
self.rangeSections.removeValue(forKey: id)
}
//TODO:localize
let addSectionSize = self.addSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Specify your working hours during the day.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
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: "Add a Set of Hours",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
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
}
let rangeId = self.nextRangeId
self.nextRangeId += 1
self.ranges.append(BusinessHoursSetupScreenComponent.WorkingHourRange(
id: rangeId, startTime: 9 * (60 * 60), endTime: 18 * (60 * 60)))
self.state?.updated(transition: .spring(duration: 0.4))
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let addSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + rangesSectionsHeight), size: addSectionSize)
if let addSectionView = self.addSection.view {
if addSectionView.superview == nil {
self.scrollView.addSubview(addSectionView)
}
transition.setFrame(view: addSectionView, frame: addSectionFrame)
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
alphaTransition.setAlpha(view: addSectionView, alpha: self.isOpen ? 1.0 : 0.0)
}
rangesSectionsHeight += addSectionSize.height
if self.isOpen {
contentHeight += rangesSectionsHeight
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
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)
}
}
final class BusinessDaySetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void
init(context: AccountContext, dayIndex: Int, day: BusinessHoursSetupScreenComponent.Day, updateDay: @escaping (BusinessHoursSetupScreenComponent.Day) -> Void) {
self.context = context
self.updateDay = updateDay
super.init(context: context, component: BusinessDaySetupScreenComponent(
context: context,
dayIndex: dayIndex,
day: day
), 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? BusinessDaySetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? BusinessDaySetupScreenComponent.View else {
return true
}
self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil))
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)
}
}

View File

@ -0,0 +1,545 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import ListSectionComponent
import ListActionItemComponent
import BundleIconComponent
import LottieComponent
import Markdown
import LocationUI
import TelegramStringFormatting
final class BusinessHoursSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
init(
context: AccountContext
) {
self.context = context
}
static func ==(lhs: BusinessHoursSetupScreenComponent, rhs: BusinessHoursSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
struct WorkingHourRange: Equatable {
var id: Int
var startTime: Int
var endTime: Int
init(id: Int, startTime: Int, endTime: Int) {
self.id = id
self.startTime = startTime
self.endTime = endTime
}
}
struct Day: Equatable {
var ranges: [WorkingHourRange]?
init(ranges: [WorkingHourRange]?) {
self.ranges = ranges
}
}
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 daysSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: BusinessHoursSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var showHours: Bool = false
private var days: [Day] = []
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)
self.days = (0 ..< 7).map { _ in
return Day(ranges: [])
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
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)
}
}
func update(component: BusinessHoursSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Business Hours", 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
let _ = bottomContentInset
let _ = sectionSpacing
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "", font: Font.semibold(90.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
}
transition.setPosition(view: iconView, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Turn this on to show your opening hours schedule to your customers.", 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
))
//TODO:localize
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
//TODO:localize
let generalSectionSize = self.generalSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Show Business Hours",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.showHours, action: { [weak self] _ in
guard let self else {
return
}
self.showHours = !self.showHours
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize)
if let generalSectionView = self.generalSection.view {
if generalSectionView.superview == nil {
self.scrollView.addSubview(generalSectionView)
}
transition.setFrame(view: generalSectionView, frame: generalSectionFrame)
}
contentHeight += generalSectionSize.height
contentHeight += sectionSpacing
var daysSectionItems: [AnyComponentWithIdentity<Empty>] = []
for day in self.days {
let dayIndex = daysSectionItems.count
let title: String
//TODO:localize
switch dayIndex {
case 0:
title = "Monday"
case 1:
title = "Tuesday"
case 2:
title = "Wednesday"
case 3:
title = "Thursday"
case 4:
title = "Friday"
case 5:
title = "Saturday"
case 6:
title = "Sunday"
default:
title = " "
}
let subtitle: String
if let ranges = self.days[dayIndex].ranges {
if ranges.isEmpty {
subtitle = "Open 24 Hours"
} else {
var resultText: String = ""
for range in ranges {
if !resultText.isEmpty {
resultText.append(", ")
}
let startHours = range.startTime / (60 * 60)
let startMinutes = range.startTime % (60 * 60)
let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat())
let endHours = range.endTime / (60 * 60)
let endMinutes = range.endTime % (60 * 60)
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat())
resultText.append("\(startText)\u{00a0}- \(endText)")
}
subtitle = resultText
}
} else {
subtitle = "Closed"
}
daysSectionItems.append(AnyComponentWithIdentity(id: dayIndex, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: subtitle,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 5
)))
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: day.ranges != nil, action: { [weak self] _ in
guard let self else {
return
}
if dayIndex < self.days.count {
if self.days[dayIndex].ranges == nil {
self.days[dayIndex].ranges = []
} else {
self.days[dayIndex].ranges = nil
}
}
self.state?.updated(transition: .immediate)
})),
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
self.environment?.controller()?.push(BusinessDaySetupScreen(
context: component.context,
dayIndex: dayIndex,
day: self.days[dayIndex],
updateDay: { [weak self] day in
guard let self else {
return
}
if self.days[dayIndex] != day {
self.days[dayIndex] = day
self.state?.updated(transition: .immediate)
}
}
))
}
))))
}
let daysSectionSize = self.daysSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "BUSINESS HOURS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: daysSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: daysSectionSize)
if let daysSectionView = self.daysSection.view {
if daysSectionView.superview == nil {
daysSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(daysSectionView)
}
transition.setFrame(view: daysSectionView, frame: daysSectionFrame)
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
alphaTransition.setAlpha(view: daysSectionView, alpha: self.showHours ? 1.0 : 0.0)
}
if self.showHours {
contentHeight += daysSectionSize.height
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
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 BusinessHoursSetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(context: AccountContext) {
self.context = context
super.init(context: context, component: BusinessHoursSetupScreenComponent(
context: context
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = ""
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? BusinessHoursSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? BusinessHoursSetupScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}

View File

@ -0,0 +1,132 @@
import Foundation
import Display
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import AccountContext
import UIKitRuntimeUtils
final class TimeSelectionActionSheet: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
self._ready.set(.single(true))
var updatedValue = currentValue
var items: [ActionSheetItem] = []
items.append(TimeSelectionActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in
updatedValue = value
}))
if let emptyTitle = emptyTitle {
items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in
self?.dismissAnimated()
applyValue(nil)
}))
}
items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in
self?.dismissAnimated()
applyValue(updatedValue)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class TimeSelectionActionSheetItem: ActionSheetItem {
let strings: PresentationStrings
let currentValue: Int32
let valueChanged: (Int32) -> Void
init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.strings = strings
self.currentValue = currentValue
self.valueChanged = valueChanged
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return TimeSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class TimeSelectionActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let valueChanged: (Int32) -> Void
private let pickerView: UIDatePicker
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.valueChanged = valueChanged
UILabel.setDateLabel(theme.primaryTextColor)
self.pickerView = UIDatePicker()
self.pickerView.datePickerMode = .countDownTimer
self.pickerView.datePickerMode = .time
self.pickerView.timeZone = TimeZone(secondsFromGMT: 0)
self.pickerView.date = Date(timeIntervalSince1970: Double(currentValue))
self.pickerView.locale = Locale.current
if #available(iOS 13.4, *) {
self.pickerView.preferredDatePickerStyle = .wheels
}
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
super.init(theme: theme)
self.view.addSubview(self.pickerView)
self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 216.0)
self.pickerView.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func datePickerUpdated() {
self.valueChanged(Int32(self.pickerView.date.timeIntervalSince1970))
}
}

View File

@ -0,0 +1,38 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BusinessLocationSetupScreen",
module_name = "BusinessLocationSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/LocationUI",
"//submodules/AppBundle",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,474 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import ListSectionComponent
import ListActionItemComponent
import ListMultilineTextFieldItemComponent
import BundleIconComponent
import LottieComponent
import Markdown
import LocationUI
final class BusinessLocationSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
init(
context: AccountContext
) {
self.context = context
}
static func ==(lhs: BusinessLocationSetupScreenComponent, rhs: BusinessLocationSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
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 addressSection = ComponentView<Empty>()
private let mapSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: BusinessLocationSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private let textFieldTag = NSObject()
private var resetAddressText: String?
private var mapCoordinates: (Double, Double)?
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 {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
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 openLocationPicker() {
guard let component = self.component else {
return
}
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, completion: { [weak self] location, _, _, address, _ in
guard let self else {
return
}
self.mapCoordinates = (location.latitude, location.longitude)
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.isEmpty {
self.resetAddressText = address
}
self.state?.updated(transition: .immediate)
})
self.environment?.controller()?.push(controller)
}
func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Location", 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
let _ = bottomContentInset
let _ = sectionSpacing
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "🗺", font: Font.semibold(90.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
}
transition.setPosition(view: iconView, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Display the location of your business on your account.", 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
))
//TODO:localize
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 addressSectionItems: [AnyComponentWithIdentity<Empty>] = []
addressSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetAddressText.flatMap { resetAddressText in
return ListMultilineTextFieldItemComponent.ResetText(value: resetAddressText)
},
placeholder: "Enter Address",
autocapitalizationType: .none,
autocorrectionType: .no,
updated: { [weak self] value in
guard let self else {
return
}
let _ = self
let _ = value
},
tag: self.textFieldTag
))))
self.resetAddressText = nil
//TODO:localize
let addressSectionSize = self.addressSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: addressSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let addressSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: addressSectionSize)
if let addressSectionView = self.addressSection.view {
if addressSectionView.superview == nil {
self.scrollView.addSubview(addressSectionView)
self.addressSection.parentState = state
}
transition.setFrame(view: addressSectionView, frame: addressSectionFrame)
}
contentHeight += addressSectionSize.height
contentHeight += sectionSpacing
var mapSectionItems: [AnyComponentWithIdentity<Empty>] = []
//TODO:localize
mapSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Set Location on Map",
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.mapCoordinates != nil, isInteractive: self.mapCoordinates != nil)),
action: { [weak self] _ in
guard let self else {
return
}
if self.mapCoordinates == nil {
self.openLocationPicker()
} else {
self.mapCoordinates = nil
self.state?.updated(transition: .spring(duration: 0.4))
}
}
))))
if let mapCoordinates = self.mapCoordinates {
mapSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(MapPreviewComponent(
theme: environment.theme,
location: MapPreviewComponent.Location(
latitude: mapCoordinates.0,
longitude: mapCoordinates.1
),
action: { [weak self] in
guard let self else {
return
}
self.openLocationPicker()
}
))))
}
//TODO:localize
let mapSectionSize = self.mapSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: mapSectionItems,
displaySeparators: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let mapSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: mapSectionSize)
if let mapSectionView = self.mapSection.view {
if mapSectionView.superview == nil {
self.scrollView.addSubview(mapSectionView)
}
transition.setFrame(view: mapSectionView, frame: mapSectionFrame)
}
contentHeight += mapSectionSize.height
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
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 BusinessLocationSetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(context: AccountContext) {
self.context = context
super.init(context: context, component: BusinessLocationSetupScreenComponent(
context: context
), 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? BusinessLocationSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? BusinessLocationSetupScreenComponent.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)
}
}

View File

@ -0,0 +1,139 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ListSectionComponent
import MapKit
import TelegramPresentationData
import AppBundle
final class MapPreviewComponent: Component {
struct Location: Equatable {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
let theme: PresentationTheme
let location: Location
let action: (() -> Void)?
init(
theme: PresentationTheme,
location: Location,
action: (() -> Void)? = nil
) {
self.theme = theme
self.location = location
self.action = action
}
static func ==(lhs: MapPreviewComponent, rhs: MapPreviewComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.location != rhs.location {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
final class View: HighlightTrackingButton, ListSectionComponent.ChildView {
private var component: MapPreviewComponent?
private weak var componentState: EmptyComponentState?
private var mapView: MKMapView?
private let pinShadowView: UIImageView
private let pinView: UIImageView
private let pinForegroundView: UIImageView
var customUpdateIsHighlighted: ((Bool) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
self.pinShadowView = UIImageView()
self.pinView = UIImageView()
self.pinForegroundView = UIImageView()
super.init(frame: frame)
self.addSubview(self.pinShadowView)
self.addSubview(self.pinView)
self.addSubview(self.pinForegroundView)
self.pinShadowView.image = UIImage(bundleImageName: "Chat/Message/LocationPinShadow")
self.pinView.image = UIImage(bundleImageName: "Chat/Message/LocationPinBackground")?.withRenderingMode(.alwaysTemplate)
self.pinForegroundView.image = UIImage(bundleImageName: "Chat/Message/LocationPinForeground")?.withRenderingMode(.alwaysTemplate)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action?()
}
func update(component: MapPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component
self.componentState = state
self.isEnabled = component.action != nil
let size = CGSize(width: availableSize.width, height: 160.0)
let mapView: MKMapView
if let current = self.mapView {
mapView = current
} else {
mapView = MKMapView()
mapView.isUserInteractionEnabled = false
self.mapView = mapView
self.insertSubview(mapView, at: 0)
}
transition.setFrame(view: mapView, frame: CGRect(origin: CGPoint(), size: size))
let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016)
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: component.location.latitude, longitude: component.location.longitude), span: defaultMapSpan)
if previousComponent?.location != component.location {
mapView.setRegion(region, animated: false)
mapView.setVisibleMapRect(mapView.visibleMapRect, edgePadding: UIEdgeInsets(top: 70.0, left: 0.0, bottom: 0.0, right: 0.0), animated: true)
}
let pinImageSize = self.pinView.image?.size ?? CGSize(width: 62.0, height: 74.0)
let pinFrame = CGRect(origin: CGPoint(x: floor((size.width - pinImageSize.width) * 0.5), y: floor((size.height - pinImageSize.height) * 0.5)), size: pinImageSize)
transition.setFrame(view: self.pinShadowView, frame: pinFrame)
transition.setFrame(view: self.pinView, frame: pinFrame)
self.pinView.tintColor = component.theme.list.itemCheckColors.fillColor
if let image = pinForegroundView.image {
let pinIconFrame = CGRect(origin: CGPoint(x: pinFrame.minX + floor((pinFrame.width - image.size.width) * 0.5), y: pinFrame.minY + 15.0), size: image.size)
transition.setFrame(view: self.pinForegroundView, frame: pinIconFrame)
self.pinForegroundView.tintColor = component.theme.list.itemCheckColors.foregroundColor
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -182,7 +182,7 @@ final class BusinessSetupScreenComponent: Component {
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += 81.0
contentHeight += 16.0
//TODO:localize
let titleSize = self.title.update(
@ -241,14 +241,22 @@ final class BusinessSetupScreenComponent: Component {
icon: "Settings/Menu/AddAccount",
title: "Location",
subtitle: "Display the location of your business on your account.",
action: {
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.push(component.context.sharedContext.makeBusinessLocationSetupScreen(context: component.context))
}
))
items.append(Item(
icon: "Settings/Menu/DataVoice",
title: "Opening Hours",
subtitle: "Show to your customers when you are open for business.",
action: {
action: { [weak self] in
guard let self else {
return
}
self.environment?.controller()?.push(component.context.sharedContext.makeBusinessHoursSetupScreen(context: component.context))
}
))
items.append(Item(
@ -262,7 +270,11 @@ final class BusinessSetupScreenComponent: Component {
icon: "Settings/Menu/Stories",
title: "Greeting Messages",
subtitle: "Create greetings that will be automatically sent to new customers.",
action: {
action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
return
}
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context))
}
))
items.append(Item(

View File

@ -31,6 +31,10 @@ swift_library(
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/ShimmerEffect",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,405 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import AvatarNode
import BundleIconComponent
import TelegramPresentationData
import TelegramCore
import AccountContext
import ListSectionComponent
import PlainButtonComponent
import ShimmerEffect
final class ChatbotSearchResultItemComponent: Component {
enum Content: Equatable {
case searching
case found(peer: EnginePeer, isInstalled: Bool)
case notFound
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let content: Content
let installAction: () -> Void
let removeAction: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
content: Content,
installAction: @escaping () -> Void,
removeAction: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.content = content
self.installAction = installAction
self.removeAction = removeAction
}
static func ==(lhs: ChatbotSearchResultItemComponent, rhs: ChatbotSearchResultItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
final class View: UIView, ListSectionComponent.ChildView {
private var notFoundLabel: ComponentView<Empty>?
private let titleLabel = ComponentView<Empty>()
private let subtitleLabel = ComponentView<Empty>()
private var shimmerEffectNode: ShimmerEffectNode?
private var avatarNode: AvatarNode?
private var addButton: ComponentView<Empty>?
private var removeButton: ComponentView<Empty>?
private var component: ChatbotSearchResultItemComponent?
private weak var state: EmptyComponentState?
var customUpdateIsHighlighted: ((Bool) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatbotSearchResultItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let sideInset: CGFloat = 10.0
let avatarDiameter: CGFloat = 40.0
let avatarTextSpacing: CGFloat = 12.0
let titleSubtitleSpacing: CGFloat = 1.0
let verticalInset: CGFloat = 11.0
let maxTextWidth: CGFloat = availableSize.width - sideInset * 2.0 - avatarDiameter - avatarTextSpacing
var addButtonSize: CGSize?
if case .found(_, false) = component.content {
let addButton: ComponentView<Empty>
var addButtonTransition = transition
if let current = self.addButton {
addButton = current
} else {
addButtonTransition = addButtonTransition.withAnimation(.none)
addButton = ComponentView()
self.addButton = addButton
}
//TODO:localize
addButtonSize = addButton.update(
transition: addButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "ADD", font: Font.semibold(15.0), textColor: component.theme.list.itemCheckColors.foregroundColor))
)),
background: AnyComponent(RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: nil)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 8.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.installAction()
},
animateAlpha: true,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
} else {
if let addButton = self.addButton {
self.addButton = nil
if let addButtonView = addButton.view {
if !transition.animation.isImmediate {
transition.setScale(view: addButtonView, scale: 0.001)
Transition.easeInOut(duration: 0.2).setAlpha(view: addButtonView, alpha: 0.0, completion: { [weak addButtonView] _ in
addButtonView?.removeFromSuperview()
})
} else {
addButtonView.removeFromSuperview()
}
}
}
}
var removeButtonSize: CGSize?
if case .found(_, true) = component.content {
let removeButton: ComponentView<Empty>
var removeButtonTransition = transition
if let current = self.removeButton {
removeButton = current
} else {
removeButtonTransition = removeButtonTransition.withAnimation(.none)
removeButton = ComponentView()
self.removeButton = removeButton
}
//TODO:localize
removeButtonSize = removeButton.update(
transition: removeButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Chat/Message/SideCloseIcon",
tintColor: component.theme.list.controlSecondaryColor
)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.removeAction()
},
animateAlpha: true,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
} else {
if let removeButton = self.removeButton {
self.removeButton = nil
if let removeButtonView = removeButton.view {
if !transition.animation.isImmediate {
transition.setScale(view: removeButtonView, scale: 0.001)
Transition.easeInOut(duration: 0.2).setAlpha(view: removeButtonView, alpha: 0.0, completion: { [weak removeButtonView] _ in
removeButtonView?.removeFromSuperview()
})
} else {
removeButtonView.removeFromSuperview()
}
}
}
}
let titleValue: String
let subtitleValue: String
let isTextVisible: Bool
switch component.content {
case .searching, .notFound:
isTextVisible = false
titleValue = "AAAAAAAAA"
subtitleValue = "bot" //TODO:localize
case let .found(peer, _):
isTextVisible = true
titleValue = peer.displayTitle(strings: component.strings, displayOrder: .firstLast)
subtitleValue = "bot"
}
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleValue, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let subtitleSize = self.subtitleLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitleValue, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)),
maximumNumberOfLines: 1
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let size = CGSize(width: availableSize.width, height: verticalInset * 2.0 + titleSize.height + titleSubtitleSpacing + subtitleSize.height)
let titleFrame = CGRect(origin: CGPoint(x: sideInset + avatarDiameter + avatarTextSpacing, y: verticalInset), size: titleSize)
if let titleView = self.titleLabel.view {
var titleTransition = transition
if titleView.superview == nil {
titleTransition = .immediate
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
if titleView.isHidden != !isTextVisible {
titleTransition = .immediate
}
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
titleTransition.setPosition(view: titleView, position: titleFrame.origin)
titleView.isHidden = !isTextVisible
}
let subtitleFrame = CGRect(origin: CGPoint(x: sideInset + avatarDiameter + avatarTextSpacing, y: verticalInset + titleSize.height + titleSubtitleSpacing), size: subtitleSize)
if let subtitleView = self.subtitleLabel.view {
var subtitleTransition = transition
if subtitleView.superview == nil {
subtitleTransition = .immediate
subtitleView.layer.anchorPoint = CGPoint()
self.addSubview(subtitleView)
}
if subtitleView.isHidden != !isTextVisible {
subtitleTransition = .immediate
}
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
subtitleTransition.setPosition(view: subtitleView, position: subtitleFrame.origin)
subtitleView.isHidden = !isTextVisible
}
let avatarFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - avatarDiameter) * 0.5)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
if case let .found(peer, _) = component.content {
var avatarTransition = transition
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarTransition = .immediate
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
}
avatarTransition.setFrame(view: avatarNode.view, frame: avatarFrame)
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, synchronousLoad: true, displayDimensions: avatarFrame.size)
avatarNode.updateSize(size: avatarFrame.size)
} else {
if let avatarNode = self.avatarNode {
self.avatarNode = nil
avatarNode.view.removeFromSuperview()
}
}
if case .notFound = component.content {
let notFoundLabel: ComponentView<Empty>
if let current = self.notFoundLabel {
notFoundLabel = current
} else {
notFoundLabel = ComponentView()
self.notFoundLabel = notFoundLabel
}
//TODO:localize
let notFoundLabelSize = notFoundLabel.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Chatbot not found", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
)
let notFoundLabelFrame = CGRect(origin: CGPoint(x: floor((size.width - notFoundLabelSize.width) * 0.5), y: floor((size.height - notFoundLabelSize.height) * 0.5)), size: notFoundLabelSize)
if let notFoundLabelView = notFoundLabel.view {
var notFoundLabelTransition = transition
if notFoundLabelView.superview == nil {
notFoundLabelTransition = .immediate
self.addSubview(notFoundLabelView)
}
notFoundLabelTransition.setPosition(view: notFoundLabelView, position: notFoundLabelFrame.center)
notFoundLabelView.bounds = CGRect(origin: CGPoint(), size: notFoundLabelFrame.size)
}
} else {
if let notFoundLabel = self.notFoundLabel {
self.notFoundLabel = nil
notFoundLabel.view?.removeFromSuperview()
}
}
if let addButton = self.addButton, let addButtonSize {
var addButtonTransition = transition
let addButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - addButtonSize.width, y: floor((size.height - addButtonSize.height) * 0.5)), size: addButtonSize)
if let addButtonView = addButton.view {
if addButtonView.superview == nil {
addButtonTransition = addButtonTransition.withAnimation(.none)
self.addSubview(addButtonView)
if !transition.animation.isImmediate {
transition.animateScale(view: addButtonView, from: 0.001, to: 1.0)
Transition.easeInOut(duration: 0.2).animateAlpha(view: addButtonView, from: 0.0, to: 1.0)
}
}
addButtonTransition.setFrame(view: addButtonView, frame: addButtonFrame)
}
}
if let removeButton = self.removeButton, let removeButtonSize {
var removeButtonTransition = transition
let removeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - removeButtonSize.width, y: floor((size.height - removeButtonSize.height) * 0.5)), size: removeButtonSize)
if let removeButtonView = removeButton.view {
if removeButtonView.superview == nil {
removeButtonTransition = removeButtonTransition.withAnimation(.none)
self.addSubview(removeButtonView)
if !transition.animation.isImmediate {
transition.animateScale(view: removeButtonView, from: 0.001, to: 1.0)
Transition.easeInOut(duration: 0.2).animateAlpha(view: removeButtonView, from: 0.0, to: 1.0)
}
}
removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame)
}
}
if case .searching = component.content {
let shimmerEffectNode: ShimmerEffectNode
if let current = self.shimmerEffectNode {
shimmerEffectNode = current
} else {
shimmerEffectNode = ShimmerEffectNode()
self.shimmerEffectNode = shimmerEffectNode
self.addSubview(shimmerEffectNode.view)
}
shimmerEffectNode.frame = CGRect(origin: CGPoint(), size: size)
shimmerEffectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = titleFrame.width
let subtitleLineWidth: CGFloat = subtitleFrame.width
let lineDiameter: CGFloat = 10.0
shapes.append(.circle(avatarFrame))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter))
shimmerEffectNode.update(backgroundColor: component.theme.list.itemBlocksBackgroundColor, foregroundColor: component.theme.list.mediaPlaceholderColor, shimmeringColor: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: size)
} else {
if let shimmerEffectNode = self.shimmerEffectNode {
self.shimmerEffectNode = nil
shimmerEffectNode.view.removeFromSuperview()
}
}
self.separatorInset = 16.0
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -21,6 +21,8 @@ import ListTextFieldItemComponent
import BundleIconComponent
import LottieComponent
import Markdown
import PeerListItemComponent
import AvatarNode
private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
@ -60,6 +62,49 @@ final class ChatbotSetupScreenComponent: Component {
}
}
private struct BotResolutionState: Equatable {
enum State: Equatable {
case searching
case notFound
case found(peer: EnginePeer, isInstalled: Bool)
}
var query: String
var state: State
init(query: String, state: State) {
self.query = query
self.state = state
}
}
private 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
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
@ -79,6 +124,18 @@ final class ChatbotSetupScreenComponent: Component {
private var environment: EnvironmentType?
private var chevronImage: UIImage?
private let textFieldTag = NSObject()
private var botResolutionState: BotResolutionState?
private var botResolutionDisposable: Disposable?
private var hasAccessToAllChatsByDefault: Bool = true
private var additionalPeerList = AdditionalPeerList(
categories: Set(),
peers: []
)
private var replyToMessages: Bool = true
override init(frame: CGRect) {
self.scrollView = ScrollView()
@ -150,6 +207,184 @@ final class ChatbotSetupScreenComponent: Component {
}
}
private func updateBotQuery(query: String) {
guard let component = self.component else {
return
}
if !query.isEmpty {
if self.botResolutionState?.query != query {
let previousState = self.botResolutionState?.state
self.botResolutionState = BotResolutionState(
query: query,
state: self.botResolutionState?.state ?? .searching
)
self.botResolutionDisposable?.dispose()
if previousState != self.botResolutionState?.state {
self.state?.updated(transition: .spring(duration: 0.35))
}
self.botResolutionDisposable = (component.context.engine.peers.resolvePeerByName(name: query)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
switch result {
case .progress:
break
case let .result(peer):
let previousState = self.botResolutionState?.state
if let peer {
self.botResolutionState?.state = .found(peer: peer, isInstalled: false)
} else {
self.botResolutionState?.state = .notFound
}
if previousState != self.botResolutionState?.state {
self.state?.updated(transition: .spring(duration: 0.35))
}
}
})
}
} else {
if let botResolutionDisposable = self.botResolutionDisposable {
self.botResolutionDisposable = nil
botResolutionDisposable.dispose()
}
if self.botResolutionState != nil {
self.botResolutionState = nil
self.state?.updated(transition: .spring(duration: 0.35))
}
}
}
private func openAdditionalPeerListSetup() {
guard let component = self.component 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: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .purple),
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: .purple),
title: self.hasAccessToAllChatsByDefault ? "Existing Chats" : "New Chats"
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.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: .blue),
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: .blue),
title: "Contacts"
),
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: "Non-Contacts"
)
]
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)
}
}
//TODO:localize
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: self.hasAccessToAllChatsByDefault ? "Exclude Chats" : "Include Chats",
searchPlaceholder: "Search chats",
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)
}
func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
@ -221,7 +456,7 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More>]()", attributes: MarkdownAttributes(
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More]()", 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),
@ -239,7 +474,7 @@ final class ChatbotSetupScreenComponent: Component {
//TODO:localize
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
component: AnyComponent(BalancedTextComponent(
text: .plain(subtitleString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
@ -273,6 +508,66 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += subtitleSize.height
contentHeight += 27.0
var nameSectionItems: [AnyComponentWithIdentity<Empty>] = []
nameSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
theme: environment.theme,
initialText: "",
placeholder: "Bot Username",
autocapitalizationType: .none,
autocorrectionType: .no,
updated: { [weak self] value in
guard let self else {
return
}
self.updateBotQuery(query: value)
},
tag: self.textFieldTag
))))
if let botResolutionState = self.botResolutionState {
let mappedContent: ChatbotSearchResultItemComponent.Content
switch botResolutionState.state {
case .searching:
mappedContent = .searching
case .notFound:
mappedContent = .notFound
case let .found(peer, isInstalled):
mappedContent = .found(peer: peer, isInstalled: isInstalled)
}
nameSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ChatbotSearchResultItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
content: mappedContent,
installAction: { [weak self] in
guard let self else {
return
}
if var botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, !isInstalled {
botResolutionState.state = .found(peer: peer, isInstalled: true)
self.botResolutionState = botResolutionState
self.state?.updated(transition: .spring(duration: 0.3))
}
},
removeAction: { [weak self] in
guard let self else {
return
}
if let botResolutionState = self.botResolutionState, case let .found(_, isInstalled) = botResolutionState.state, isInstalled {
self.botResolutionState = nil
if let botResolutionDisposable = self.botResolutionDisposable {
self.botResolutionDisposable = nil
botResolutionDisposable.dispose()
}
if let textFieldView = self.nameSection.findTaggedView(tag: self.textFieldTag) as? ListTextFieldItemComponent.View {
textFieldView.setText(text: "", updateState: false)
}
self.state?.updated(transition: .spring(duration: 0.3))
}
}
))))
}
//TODO:localize
let nameSectionSize = self.nameSection.update(
transition: transition,
@ -287,15 +582,7 @@ final class ChatbotSetupScreenComponent: Component {
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
theme: environment.theme,
initialText: "",
placeholder: "Bot Username",
updated: { value in
}
)))
]
items: nameSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@ -339,11 +626,20 @@ final class ChatbotSetupScreenComponent: Component {
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: environment.theme.list.itemAccentColor,
tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { _ in
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(
@ -360,11 +656,20 @@ final class ChatbotSetupScreenComponent: Component {
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: .clear,
tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { _ in
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)
}
}
)))
]
@ -382,6 +687,95 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += accessSectionSize.height
contentHeight += 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 ? "Exclude Chats..." : "Select Chats...",
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
//TODO:localize
switch category {
case .newChats:
title = "New Chats"
icon = "Chat List/Filters/Contact"
color = .purple
case .existingChats:
title = "Existing Chats"
icon = "Chat List/Filters/Contact"
color = .purple
case .contacts:
title = "Contacts"
icon = "Chat List/Filters/Contact"
color = .blue
case .nonContacts:
title = "Non-Contacts"
icon = "Chat List/Filters/Contact"
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
}
))))
}
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 ? "contact" : "non-contact",
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
}
))))
}
//TODO:localize
let excludedSectionSize = self.excludedSection.update(
transition: transition,
@ -389,42 +783,27 @@ final class ChatbotSetupScreenComponent: Component {
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "EXCLUDED CHATS",
string: self.hasAccessToAllChatsByDefault ? "EXCLUDED CHATS" : "INCLUDED CHATS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Select chats or entire chat categories which the bot WILL NOT have access to.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
text: .markdown(
text: self.hasAccessToAllChatsByDefault ? "Select chats or entire chat categories which the bot **WILL NOT** have access to." : "Select chats or entire chat categories which the bot **WILL** have access to.",
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: "Exclude Chats...",
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: { _ in
}
))),
]
items: excludedSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@ -473,7 +852,13 @@ final class ChatbotSetupScreenComponent: Component {
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(true),
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
guard let self else {
return
}
self.replyToMessages = !self.replyToMessages
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
))),
]

View File

@ -0,0 +1,42 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GreetingMessageSetupScreen",
module_name = "GreetingMessageSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/BackButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/ShimmerEffect",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,882 @@
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
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 GreetingMessageSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
init(
context: AccountContext
) {
self.context = context
}
static func ==(lhs: GreetingMessageSetupScreenComponent, rhs: GreetingMessageSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private struct BotResolutionState: Equatable {
enum State: Equatable {
case searching
case notFound
case found(peer: EnginePeer, isInstalled: Bool)
}
var query: String
var state: State
init(query: String, state: State) {
self.query = query
self.state = state
}
}
private 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
}
}
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 accessSection = ComponentView<Empty>()
private let excludedSection = ComponentView<Empty>()
private let permissionsSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: GreetingMessageSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var chevronImage: UIImage?
private var isOn: Bool = false
private var hasAccessToAllChatsByDefault: Bool = true
private var additionalPeerList = AdditionalPeerList(
categories: Set(),
peers: []
)
private var replyToMessages: Bool = true
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 {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
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 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: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .purple),
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: .purple),
title: self.hasAccessToAllChatsByDefault ? "Existing Chats" : "New Chats"
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.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: .blue),
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: .blue),
title: "Contacts"
),
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: "Non-Contacts"
)
]
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)
}
}
//TODO:localize
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: self.hasAccessToAllChatsByDefault ? "Exclude Chats" : "Include Chats",
searchPlaceholder: "Search chats",
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)
}
func update(component: GreetingMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
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 }
//TODO:localize
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Greeting Message", 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
let _ = bottomContentInset
let _ = sectionSpacing
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "HandWaveEmoji"),
loop: true
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
}
transition.setPosition(view: iconView, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Greet customers when they message you the first time or after a period of no activity.", 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
))
if self.chevronImage == nil {
self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight")
}
if let range = subtitleString.string.range(of: ">"), let chevronImage = self.chevronImage {
subtitleString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: subtitleString.string))
}
//TODO:localize
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: "Send Greeting Message",
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
))))
//TODO:localize
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
//TODO:localize
let accessSectionSize = self.accessSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "RECIPIENTS",
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: "All 1-to-1 Chats Except...",
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: "Only Selected Chats",
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 ? "Exclude Chats..." : "Select Chats...",
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
//TODO:localize
switch category {
case .newChats:
title = "New Chats"
icon = "Chat List/Filters/Contact"
color = .purple
case .existingChats:
title = "Existing Chats"
icon = "Chat List/Filters/Contact"
color = .purple
case .contacts:
title = "Contacts"
icon = "Chat List/Filters/Contact"
color = .blue
case .nonContacts:
title = "Non-Contacts"
icon = "Chat List/Filters/Contact"
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
}
))))
}
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 ? "contact" : "non-contact",
subtitleAccessory: .none,
presence: nil,
selectionState: .none,
hasNext: false,
action: { peer, _, _ in
}
))))
}
//TODO:localize
let excludedSectionSize = self.excludedSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: self.hasAccessToAllChatsByDefault ? "EXCLUDED CHATS" : "INCLUDED CHATS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .markdown(
text: self.hasAccessToAllChatsByDefault ? "Select chats or entire chat categories which the bot **WILL NOT** have access to." : "Select chats or entire chat categories which the bot **WILL** have access to.",
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
//TODO:localize
/*let permissionsSectionSize = self.permissionsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "BOT PERMISSIONS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
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: "Reply to Messages",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
guard let self else {
return
}
self.replyToMessages = !self.replyToMessages
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let permissionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: permissionsSectionSize)
if let permissionsSectionView = self.permissionsSection.view {
if permissionsSectionView.superview == nil {
permissionsSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(permissionsSectionView)
}
transition.setFrame(view: permissionsSectionView, frame: permissionsSectionFrame)
alphaTransition.setAlpha(view: permissionsSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += permissionsSectionSize.height*/
if self.isOn {
contentHeight += otherSectionsHeight
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
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 GreetingMessageSetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(context: AccountContext) {
self.context = context
super.init(context: context, component: GreetingMessageSetupScreenComponent(
context: context
), 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? GreetingMessageSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? GreetingMessageSetupScreenComponent.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)
}
}

View File

@ -1264,14 +1264,14 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)),
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: component.context,
color: profileColor.flatMap { profileColor in
component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main
} ?? environment.theme.list.itemAccentColor,
fileId: backgroundFileId,
file: backgroundFileId.flatMap { self.cachedIconFiles[$0] }
))),
)))),
action: { [weak self] view in
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return
@ -1393,12 +1393,12 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(HStack(emojiPackContents, spacing: 6.0)),
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: component.context,
color: environment.theme.list.itemAccentColor,
fileId: emojiPack?.thumbnailFileId,
file: emojiPackFile
))),
)))),
action: { [weak self] view in
guard let self, let resolvedState = self.resolveState() else {
return
@ -1457,12 +1457,12 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(HStack(emojiStatusContents, spacing: 6.0)),
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: component.context,
color: environment.theme.list.itemAccentColor,
fileId: statusFileId,
file: statusFileId.flatMap { self.cachedIconFiles[$0] }
))),
)))),
action: { [weak self] view in
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return
@ -1581,12 +1581,12 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)),
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: component.context,
color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main,
fileId: replyFileId,
file: replyFileId.flatMap { self.cachedIconFiles[$0] }
))),
)))),
action: { [weak self] view in
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return

View File

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SliderComponent",
module_name = "SliderComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/LegacyUI",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,459 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import LegacyUI
import ComponentFlow
import MultilineTextComponent
final class SliderComponent: Component {
typealias EnvironmentType = Empty
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
let isEnabled: Bool
let trackColor: UIColor?
let displayValue: Bool
let valueUpdated: (Float) -> Void
let isTrackingUpdated: ((Bool) -> Void)?
init(
title: String,
value: Float,
minValue: Float,
maxValue: Float,
startValue: Float,
isEnabled: Bool,
trackColor: UIColor?,
displayValue: Bool,
valueUpdated: @escaping (Float) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil
) {
self.title = title
self.value = value
self.minValue = minValue
self.maxValue = maxValue
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.minValue != rhs.minValue {
return false
}
if lhs.maxValue != rhs.maxValue {
return false
}
if lhs.startValue != rhs.startValue {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.trackColor != rhs.trackColor {
return false
}
if lhs.displayValue != rhs.displayValue {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let title = ComponentView<Empty>()
private let value = ComponentView<Empty>()
private var sliderView: TGPhotoEditorSliderView?
private var component: SliderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
var internalIsTrackingUpdated: ((Bool) -> Void)?
if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in
if let self {
if isTracking {
self.sliderView?.bordered = true
} else {
Queue.mainQueue().after(0.1) {
self.sliderView?.bordered = false
}
}
isTrackingUpdated(isTracking)
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
}
if let valueView = self.value.view {
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
let sliderView: TGPhotoEditorSliderView
if let current = self.sliderView {
sliderView = current
sliderView.value = CGFloat(component.value)
} else {
sliderView = TGPhotoEditorSliderView()
sliderView.backgroundColor = .clear
sliderView.startColor = UIColor(rgb: 0xffffff)
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = CGFloat(component.minValue)
sliderView.maximumValue = CGFloat(component.maxValue)
sliderView.startValue = CGFloat(component.startValue)
sliderView.value = CGFloat(component.value)
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
sliderView.layer.allowsGroupOpacity = true
self.sliderView = sliderView
self.addSubview(sliderView)
}
sliderView.interactionBegan = {
internalIsTrackingUpdated?(true)
}
sliderView.interactionEnded = {
internalIsTrackingUpdated?(false)
}
if component.isEnabled {
sliderView.alpha = 1.3
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
sliderView.isUserInteractionEnabled = true
} else {
sliderView.trackColor = UIColor(rgb: 0xffffff)
sliderView.alpha = 0.3
sliderView.isUserInteractionEnabled = false
}
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
}
let valueText: String
if component.displayValue {
if component.value > 0.005 {
valueText = String(format: "+%.2f", component.value)
} else if component.value < -0.005 {
valueText = String(format: "%.2f", component.value)
} else {
valueText = ""
}
} else {
valueText = ""
}
let valueSize = self.value.update(
transition: .immediate,
component: AnyComponent(
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xf8d74a))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let valueView = self.value.view {
if valueView.superview == nil {
self.addSubview(valueView)
}
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
}
return CGSize(width: availableSize.width, height: 52.0)
}
@objc private func sliderValueChanged() {
guard let component = self.component, let sliderView = self.sliderView else {
return
}
component.valueUpdated(Float(sliderView.value))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
struct AdjustmentTool: Equatable {
let key: EditorToolKey
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
}
final class AdjustmentsComponent: Component {
typealias EnvironmentType = Empty
let tools: [AdjustmentTool]
let valueUpdated: (EditorToolKey, Float) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
tools: [AdjustmentTool],
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.tools = tools
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
if lhs.tools != rhs.tools {
return false
}
return true
}
final class View: UIView {
private let scrollView = UIScrollView()
private var toolViews: [ComponentView<Empty>] = []
private var component: AdjustmentsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let valueUpdated = component.valueUpdated
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
component.isTrackingUpdated(isTracking)
if let self {
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
if tool.key != trackingTool && i < self.toolViews.count {
if let view = self.toolViews[i].view {
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
}
var sizes: [CGSize] = []
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
let componentView: ComponentView<Empty>
if i >= self.toolViews.count {
componentView = ComponentView<Empty>()
self.toolViews.append(componentView)
} else {
componentView = self.toolViews[i]
}
var valueIsNegative = false
var value = tool.value
if case .enhance = tool.key {
if value < 0.0 {
valueIsNegative = true
}
value = abs(value)
}
let size = componentView.update(
transition: transition,
component: AnyComponent(
SliderComponent(
title: tool.title,
value: value,
minValue: tool.minValue,
maxValue: tool.maxValue,
startValue: tool.startValue,
isEnabled: true,
trackColor: nil,
displayValue: true,
valueUpdated: { value in
var updatedValue = value
if valueIsNegative {
updatedValue *= -1.0
}
valueUpdated(tool.key, updatedValue)
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(tool.key, isTracking)
}
)
),
environment: {},
containerSize: availableSize
)
sizes.append(size)
}
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
for i in 0 ..< component.tools.count {
let size = sizes[i]
let componentView = self.toolViews[i]
if let view = componentView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
}
origin = origin.offsetBy(dx: 0.0, dy: size.height)
}
let size = CGSize(width: availableSize.width, height: 180.0)
let contentSize = CGSize(width: availableSize.width, height: origin.y)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class AdjustmentsScreenComponent: Component {
typealias EnvironmentType = Empty
let toggleUneditedPreview: (Bool) -> Void
init(
toggleUneditedPreview: @escaping (Bool) -> Void
) {
self.toggleUneditedPreview = toggleUneditedPreview
}
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var component: AdjustmentsScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.05
self.addGestureRecognizer(longPressGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.toggleUneditedPreview(true)
case .ended, .cancelled:
component.toggleUneditedPreview(false)
default:
break
}
}
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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)
}
}

View File

@ -29,6 +29,7 @@ swift_library(
"//submodules/ContextUI",
"//submodules/TextFormat",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/ListSectionComponent",
],
visibility = [
"//visibility:public",

View File

@ -19,6 +19,7 @@ import ContextUI
import EmojiTextAttachmentView
import TextFormat
import PhotoResources
import ListSectionComponent
private let avatarFont = avatarPlaceholderFont(size: 15.0)
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
@ -64,6 +65,18 @@ public final class PeerListItemComponent: Component {
case check
}
public struct Avatar: Equatable {
public var icon: String
public var color: AvatarBackgroundColor
public var clipStyle: AvatarNodeClipStyle
public init(icon: String, color: AvatarBackgroundColor, clipStyle: AvatarNodeClipStyle) {
self.icon = icon
self.color = color
self.clipStyle = clipStyle
}
}
public final class Reaction: Equatable {
public let reaction: MessageReaction.Reaction
public let file: TelegramMediaFile?
@ -103,6 +116,7 @@ public final class PeerListItemComponent: Component {
let style: Style
let sideInset: CGFloat
let title: String
let avatar: Avatar?
let peer: EnginePeer?
let storyStats: PeerStoryStats?
let subtitle: String?
@ -127,6 +141,7 @@ public final class PeerListItemComponent: Component {
style: Style,
sideInset: CGFloat,
title: String,
avatar: Avatar? = nil,
peer: EnginePeer?,
storyStats: PeerStoryStats? = nil,
subtitle: String?,
@ -150,6 +165,7 @@ public final class PeerListItemComponent: Component {
self.style = style
self.sideInset = sideInset
self.title = title
self.avatar = avatar
self.peer = peer
self.storyStats = storyStats
self.subtitle = subtitle
@ -187,6 +203,9 @@ public final class PeerListItemComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.avatar != rhs.avatar {
return false
}
if lhs.peer != rhs.peer {
return false
}
@ -229,7 +248,7 @@ public final class PeerListItemComponent: Component {
return true
}
public final class View: ContextControllerSourceView {
public final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
private let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton
@ -237,6 +256,7 @@ public final class PeerListItemComponent: Component {
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode
private var avatarImageView: UIImageView?
private let avatarButtonView: HighlightTrackingButton
private var avatarIcon: ComponentView<Empty>?
@ -278,6 +298,9 @@ public final class PeerListItemComponent: Component {
private var isExtractedToContextMenu: Bool = false
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
@ -336,6 +359,15 @@ public final class PeerListItemComponent: Component {
}
component.contextAction?(peer, self.extractedContainerView, gesture)
}
self.containerButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
customUpdateIsHighlighted(highlighted)
}
}
}
required init?(coder: NSCoder) {
@ -560,6 +592,27 @@ public final class PeerListItemComponent: Component {
transition.setFrame(view: self.avatarButtonView, frame: avatarFrame)
var statusIcon: EmojiStatusComponent.Content?
if let avatar = component.avatar {
let avatarImageView: UIImageView
if let current = self.avatarImageView {
avatarImageView = current
} else {
avatarImageView = UIImageView()
self.avatarImageView = avatarImageView
self.containerButton.addSubview(avatarImageView)
}
if previousComponent?.avatar != avatar {
avatarImageView.image = generateAvatarImage(size: avatarFrame.size, icon: generateTintedImage(image: UIImage(bundleImageName: avatar.icon), color: .white), cornerRadius: 12.0, color: avatar.color)
}
transition.setFrame(view: avatarImageView, frame: avatarFrame)
} else {
if let avatarImageView = self.avatarImageView {
self.avatarImageView = nil
avatarImageView.removeFromSuperview()
}
}
if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
@ -596,6 +649,7 @@ public final class PeerListItemComponent: Component {
lineWidth: 1.33,
inactiveLineWidth: 1.33
), transition: transition)
self.avatarNode.isHidden = false
if peer.isScam {
statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased())
@ -608,6 +662,8 @@ public final class PeerListItemComponent: Component {
} else if peer.isPremium {
statusIcon = .premium(color: component.theme.list.itemAccentColor)
}
} else {
self.avatarNode.isHidden = true
}
let previousTitleFrame = self.title.view?.frame
@ -953,6 +1009,8 @@ public final class PeerListItemComponent: Component {
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
self.separatorInset = leftInset
return CGSize(width: availableSize.width, height: height)
}
}

View File

@ -193,7 +193,7 @@ public final class TextFieldComponent: Component {
private let ellipsisView = ComponentView<Empty>()
private var inputState: InputState {
public var inputState: InputState {
let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "business_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,196 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.110840 5.913086 cm
1.000000 1.000000 1.000000 scn
4.498568 19.173828 m
3.994308 19.173828 3.585524 18.765045 3.585524 18.260784 c
3.585524 17.756525 3.994307 17.347740 4.498567 17.347740 c
17.281176 17.347740 l
17.785437 17.347740 18.194220 17.756525 18.194220 18.260784 c
18.194220 18.765045 17.785437 19.173828 17.281176 19.173828 c
4.498568 19.173828 l
h
3.730381 16.434698 m
2.984531 16.434698 2.270933 16.130550 1.754408 15.592503 c
0.442120 14.225536 l
0.115654 13.885468 -0.098236 13.421288 0.045616 12.972363 c
0.335989 12.066183 1.157089 11.412958 2.124654 11.412958 c
3.334878 11.412958 4.315958 12.434917 4.315958 13.695567 c
4.315958 12.434917 5.297039 11.412958 6.507263 11.412958 c
7.717486 11.412958 8.698567 12.434917 8.698567 13.695567 c
8.698567 12.434917 9.679648 11.412958 10.889872 11.412958 c
12.100096 11.412958 13.081176 12.434917 13.081176 13.695567 c
13.081176 12.434917 14.062256 11.412958 15.272481 11.412958 c
16.482704 11.412958 17.463785 12.434917 17.463785 13.695567 c
17.463785 12.434917 18.444864 11.412958 19.655088 11.412958 c
20.622656 11.412958 21.443754 12.066183 21.734129 12.972363 c
21.877979 13.421288 21.664089 13.885468 21.337624 14.225537 c
20.025335 15.592504 l
19.508810 16.130550 18.795212 16.434698 18.049362 16.434698 c
3.730381 16.434698 l
h
3.585524 10.043394 m
4.089784 10.043394 4.498567 9.634610 4.498567 9.130350 c
4.498567 6.370851 l
4.509398 5.875998 4.913934 5.478176 5.411387 5.478176 c
16.367910 5.478176 l
16.872171 5.478176 17.280952 5.886958 17.280952 6.391218 c
17.280952 8.217306 l
17.281176 8.237696 l
17.281176 9.130350 l
17.281176 9.634610 17.689960 10.043394 18.194220 10.043394 c
18.698479 10.043394 19.107264 9.634610 19.107264 9.130350 c
19.107264 6.391220 l
19.107264 1.826002 l
19.107264 0.817484 18.289698 -0.000084 17.281178 -0.000084 c
4.498567 -0.000084 l
3.490047 -0.000084 2.672480 0.817484 2.672480 1.826004 c
2.672480 6.355908 l
2.672257 6.391218 l
2.672257 8.217306 l
2.672480 8.237684 l
2.672480 9.130350 l
2.672480 9.634610 3.081264 10.043394 3.585524 10.043394 c
h
f*
n
Q
endstream
endobj
2 0 obj
2129
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 18.799999 m
0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
18.799999 30.000000 l
22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
30.000000 11.200001 l
30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
11.200000 0.000000 l
7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
0.000000 18.799999 l
h
f
n
Q
endstream
endobj
4 0 obj
944
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000002387 00000 n
0000002410 00000 n
0000003602 00000 n
0000003624 00000 n
0000003922 00000 n
0000004024 00000 n
0000004045 00000 n
0000004218 00000 n
0000004292 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
4352
%%EOF

View File

@ -127,8 +127,23 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
let additionalCategories = chatSelection.additionalCategories
let chatListFilters = chatSelection.chatListFilters
var chatListFilter: ChatListFilter?
if chatSelection.onlyUsers {
chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData(
isShared: false,
hasSharedLinks: false,
categories: [.contacts, .nonContacts],
excludeMuted: false,
excludeRead: false,
excludeArchived: false,
includePeers: ChatListFilterIncludePeers(),
excludePeers: [],
color: nil
))
}
placeholder = placeholderValue
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
chatListNode.passthroughPeerSelection = true
chatListNode.disabledPeerSelected = { peer, _, reason in
attemptDisabledItemSelection?(peer, reason)

View File

@ -53,6 +53,9 @@ import UndoUI
import ChatMessageNotificationItem
import BusinessSetupScreen
import ChatbotSetupScreen
import BusinessLocationSetupScreen
import BusinessHoursSetupScreen
import GreetingMessageSetupScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1888,6 +1891,18 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return ChatbotSetupScreen(context: context)
}
public func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController {
return BusinessLocationSetupScreen(context: context)
}
public func makeBusinessHoursSetupScreen(context: AccountContext) -> ViewController {
return BusinessHoursSetupScreen(context: context)
}
public func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController {
return GreetingMessageSetupScreen(context: context)
}
public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController {
var modal = true
let mappedSource: PremiumSource