[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 makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(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 navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) 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> 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 chatListFilters: [ChatListFilter]?
public var displayAutoremoveTimeout: Bool public var displayAutoremoveTimeout: Bool
public var displayPresence: Bool public var displayPresence: Bool
public var onlyUsers: Bool
public init( public init(
title: String, title: String,
@ -53,7 +54,8 @@ public enum ContactMultiselectionControllerMode {
additionalCategories: ContactMultiselectionControllerAdditionalCategories?, additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
chatListFilters: [ChatListFilter]?, chatListFilters: [ChatListFilter]?,
displayAutoremoveTimeout: Bool = false, displayAutoremoveTimeout: Bool = false,
displayPresence: Bool = false displayPresence: Bool = false,
onlyUsers: Bool = false
) { ) {
self.title = title self.title = title
self.searchPlaceholder = searchPlaceholder self.searchPlaceholder = searchPlaceholder
@ -62,6 +64,7 @@ public enum ContactMultiselectionControllerMode {
self.chatListFilters = chatListFilters self.chatListFilters = chatListFilters
self.displayAutoremoveTimeout = displayAutoremoveTimeout self.displayAutoremoveTimeout = displayAutoremoveTimeout
self.displayPresence = displayPresence self.displayPresence = displayPresence
self.onlyUsers = onlyUsers
} }
} }

View File

@ -3185,7 +3185,7 @@ public final class ChatListNode: ListView {
} }
private func resetFilter() { 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() self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters()
|> map { filters -> ChatListFilter? in |> map { filters -> ChatListFilter? in
for filter in filters { for filter in filters {
@ -4113,14 +4113,16 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
if isContact { if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil) return (strings.ChatList_PeerTypeContact, false, false, nil)
} else { } else {
return (strings.ChatList_PeerTypeNonContact, false, false, nil) //TODO:localize
return ("non-contact", false, false, nil)
} }
} }
} else if case .secretChat = peer { } else if case .secretChat = peer {
if isContact { if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil) return (strings.ChatList_PeerTypeContact, false, false, nil)
} else { } else {
return (strings.ChatList_PeerTypeNonContact, false, false, nil) //TODO:localize
return ("non-contact", false, false, nil)
} }
} else if case .legacyGroup = peer { } else if case .legacyGroup = peer {
return (strings.ChatList_PeerTypeGroup, false, false, nil) 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 !filter.categories.contains(.contacts) && isContact {
if let user = peer as? TelegramUser { if let user = peer as? TelegramUser {
if user.botInfo == nil { if user.botInfo == nil && !user.flags.contains(.isSupport) {
return false return false
} }
} else if let _ = peer as? TelegramSecretChat { } else if let _ = peer as? TelegramSecretChat {
@ -88,7 +88,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
} }
if !filter.categories.contains(.bots) { if !filter.categories.contains(.bots) {
if let user = peer as? TelegramUser { if let user = peer as? TelegramUser {
if user.botInfo != nil { if user.botInfo != nil || user.flags.contains(.isSupport) {
return false return false
} }
} }

View File

@ -722,11 +722,11 @@ public extension CombinedComponent {
updatedChild.view.layer.shadowRadius = 0.0 updatedChild.view.layer.shadowRadius = 0.0
updatedChild.view.layer.shadowOpacity = 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 { guard let viewContext = viewContext else {
return return
} }
viewContext.state.updated(transition: transition) viewContext.state.updated(transition: transition, isLocal: isLocal)
} }
if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide { if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide {

View File

@ -89,15 +89,15 @@ extension UIView {
} }
open class ComponentState { open class ComponentState {
open var _updated: ((Transition) -> Void)? open var _updated: ((Transition, Bool) -> Void)?
var isUpdated: Bool = false var isUpdated: Bool = false
public init() { public init() {
} }
public final func updated(transition: Transition = .immediate) { public final func updated(transition: Transition = .immediate, isLocal: Bool = false) {
self.isUpdated = true 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 colors: [UIColor]
public let cornerRadius: CGFloat public let cornerRadius: CGFloat?
public let gradientDirection: GradientDirection public let gradientDirection: GradientDirection
public let stroke: CGFloat? public let stroke: CGFloat?
public let strokeColor: UIColor? 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) 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.colors = colors
self.cornerRadius = cornerRadius self.cornerRadius = cornerRadius
self.gradientDirection = gradientDirection self.gradientDirection = gradientDirection
@ -49,8 +49,10 @@ public final class RoundedRectangle: Component {
func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize { func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component != component { 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 { 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) UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() { if let context = UIGraphicsGetCurrentContext() {
if let strokeColor = component.strokeColor { 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)) 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() UIGraphicsEndImageContext()
} else if component.colors.count > 1 { } else if component.colors.count > 1 {
let imageSize = availableSize let imageSize = availableSize
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() { 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() context.clip()
let colors = component.colors let colors = component.colors
@ -93,12 +95,12 @@ public final class RoundedRectangle: Component {
if let stroke = component.stroke, stroke > 0.0 { if let stroke = component.stroke, stroke > 0.0 {
context.resetClip() 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.setBlendMode(.clear)
context.fill(CGRect(origin: .zero, size: imageSize)) 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() UIGraphicsEndImageContext()
} }
} }

View File

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

View File

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

View File

@ -187,7 +187,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab
if ["home", "work"].contains(venueType) { 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) 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 { } else {
completion(venue, queryId, resultId, nil, nil) completion(venue, queryId, resultId, venue.venue?.address, nil)
} }
strongSelf.dismiss() strongSelf.dismiss()
}, toggleMapModeSelection: { [weak self] in }, toggleMapModeSelection: { [weak self] in

View File

@ -11,10 +11,41 @@ private func drawBorder(context: CGContext, rect: CGRect) {
context.strokePath() 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 return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds) 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 { if let image = UIImage(bundleImageName: name)?.cgImage {
context.draw(image, in: bounds) context.draw(image, in: bounds)
} }
@ -38,6 +69,7 @@ public struct PresentationResourcesSettings {
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving") public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
public static let stories = renderIcon(name: "Settings/Menu/Stories") public static let stories = renderIcon(name: "Settings/Menu/Stories")
public static let premiumGift = renderIcon(name: "Settings/Menu/Gift") 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 public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -21,9 +21,9 @@ public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat
periodString = "AM" periodString = "AM"
} }
if minutes >= 10 { if minutes >= 10 {
return "\(hourString):\(minutes) \(periodString)" return "\(hourString):\(minutes)\u{00a0}\(periodString)"
} else { } else {
return "\(hourString):0\(minutes) \(periodString)" return "\(hourString):0\(minutes)\u{00a0}\(periodString)"
} }
case .military: case .military:
return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)]) 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/Chat/ChatInlineSearchResultsListComponent",
"//submodules/TelegramUI/Components/Settings/BusinessSetupScreen", "//submodules/TelegramUI/Components/Settings/BusinessSetupScreen",
"//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen", "//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
"//submodules/TelegramUI/Components/Settings/GreetingMessageSetupScreen",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//build-system:ios_sim_arm64": [],

View File

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

View File

@ -7,15 +7,67 @@ import ListSectionComponent
import SwitchNode import SwitchNode
public final class ListActionItemComponent: Component { 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 { public enum Accessory: Equatable {
case arrow 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 theme: PresentationTheme
public let title: AnyComponent<Empty> public let title: AnyComponent<Empty>
public let leftIcon: AnyComponentWithIdentity<Empty>? public let leftIcon: AnyComponentWithIdentity<Empty>?
public let icon: AnyComponentWithIdentity<Empty>? public let icon: Icon?
public let accessory: Accessory? public let accessory: Accessory?
public let action: ((UIView) -> Void)? public let action: ((UIView) -> Void)?
@ -23,7 +75,7 @@ public final class ListActionItemComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
title: AnyComponent<Empty>, title: AnyComponent<Empty>,
leftIcon: AnyComponentWithIdentity<Empty>? = nil, leftIcon: AnyComponentWithIdentity<Empty>? = nil,
icon: AnyComponentWithIdentity<Empty>? = nil, icon: Icon? = nil,
accessory: Accessory? = .arrow, accessory: Accessory? = .arrow,
action: ((UIView) -> Void)? action: ((UIView) -> Void)?
) { ) {
@ -63,7 +115,8 @@ public final class ListActionItemComponent: Component {
private var icon: ComponentView<Empty>? private var icon: ComponentView<Empty>?
private var arrowView: UIImageView? private var arrowView: UIImageView?
private var switchNode: IconSwitchNode? private var switchNode: SwitchNode?
private var iconSwitchNode: IconSwitchNode?
private var component: ListActionItemComponent? private var component: ListActionItemComponent?
@ -83,7 +136,10 @@ public final class ListActionItemComponent: Component {
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.internalHighligthedChanged = { [weak self] isHighlighted in 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 return
} }
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
@ -97,15 +153,23 @@ public final class ListActionItemComponent: Component {
} }
@objc private func pressed() { @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 { func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component let previousComponent = self.component
self.component = component self.component = component
self.isEnabled = component.action != nil
let themeUpdated = component.theme !== previousComponent?.theme let themeUpdated = component.theme !== previousComponent?.theme
let verticalInset: CGFloat = 12.0 let verticalInset: CGFloat = 12.0
@ -118,7 +182,7 @@ public final class ListActionItemComponent: Component {
case .arrow: case .arrow:
contentRightInset = 30.0 contentRightInset = 30.0
case .toggle: case .toggle:
contentRightInset = 42.0 contentRightInset = 76.0
} }
var contentHeight: CGFloat = 0.0 var contentHeight: CGFloat = 0.0
@ -147,7 +211,7 @@ public final class ListActionItemComponent: Component {
contentHeight += verticalInset contentHeight += verticalInset
if let iconValue = component.icon { 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 self.icon = nil
if let iconView = icon.view { if let iconView = icon.view {
transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in
@ -168,17 +232,17 @@ public final class ListActionItemComponent: Component {
let iconSize = icon.update( let iconSize = icon.update(
transition: iconTransition, transition: iconTransition,
component: iconValue.component, component: iconValue.component.component,
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height) 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) 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 let iconView = icon.view {
if iconView.superview == nil { if iconView.superview == nil {
iconView.isUserInteractionEnabled = false
self.addSubview(iconView) self.addSubview(iconView)
transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
} }
iconView.isUserInteractionEnabled = iconValue.allowUserInteraction
iconTransition.setFrame(view: iconView, frame: iconFrame) iconTransition.setFrame(view: iconView, frame: iconFrame)
} }
} else { } else {
@ -263,32 +327,85 @@ public final class ListActionItemComponent: Component {
} }
} }
if case let .toggle(isOn) = component.accessory { if case let .toggle(toggle) = component.accessory {
let switchNode: IconSwitchNode switch toggle.style {
var switchTransition = transition case .regular:
var updateSwitchTheme = themeUpdated let switchNode: SwitchNode
if let current = self.switchNode { var switchTransition = transition
switchNode = current var updateSwitchTheme = themeUpdated
switchNode.setOn(isOn, animated: !transition.animation.isImmediate) if let current = self.switchNode {
} else { switchNode = current
switchTransition = switchTransition.withAnimation(.none) switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate)
updateSwitchTheme = true } else {
switchNode = IconSwitchNode() switchTransition = switchTransition.withAnimation(.none)
switchNode.setOn(isOn, animated: false) updateSwitchTheme = true
self.addSubview(switchNode.view) 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 { } else {
if let switchNode = self.switchNode { if let switchNode = self.switchNode {
self.switchNode = nil 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) itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView() itemView = ItemView()
self.itemViews[itemId] = itemView self.itemViews[itemId] = itemView
itemView.contents.parentState = state
} }
let itemSize = itemView.contents.update( let itemSize = itemView.contents.update(

View File

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

View File

@ -4,23 +4,35 @@ import Display
import ComponentFlow import ComponentFlow
import TelegramPresentationData import TelegramPresentationData
import MultilineTextComponent import MultilineTextComponent
import ListSectionComponent
import PlainButtonComponent
import BundleIconComponent
public final class ListTextFieldItemComponent: Component { public final class ListTextFieldItemComponent: Component {
public let theme: PresentationTheme public let theme: PresentationTheme
public let initialText: String public let initialText: String
public let placeholder: String public let placeholder: String
public let autocapitalizationType: UITextAutocapitalizationType
public let autocorrectionType: UITextAutocorrectionType
public let updated: ((String) -> Void)? public let updated: ((String) -> Void)?
public let tag: AnyObject?
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
initialText: String, initialText: String,
placeholder: String, placeholder: String,
updated: ((String) -> Void)? autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
updated: ((String) -> Void)?,
tag: AnyObject? = nil
) { ) {
self.theme = theme self.theme = theme
self.initialText = initialText self.initialText = initialText
self.placeholder = placeholder self.placeholder = placeholder
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.updated = updated self.updated = updated
self.tag = tag
} }
public static func ==(lhs: ListTextFieldItemComponent, rhs: ListTextFieldItemComponent) -> Bool { public static func ==(lhs: ListTextFieldItemComponent, rhs: ListTextFieldItemComponent) -> Bool {
@ -33,6 +45,12 @@ public final class ListTextFieldItemComponent: Component {
if lhs.placeholder != rhs.placeholder { if lhs.placeholder != rhs.placeholder {
return false return false
} }
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if (lhs.updated == nil) != (rhs.updated == nil) { if (lhs.updated == nil) != (rhs.updated == nil) {
return false 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 textField: TextField
private let placeholder = ComponentView<Empty>() private let placeholder = ComponentView<Empty>()
private let clearButton = ComponentView<Empty>()
private var component: ListTextFieldItemComponent? private var component: ListTextFieldItemComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -63,6 +82,9 @@ public final class ListTextFieldItemComponent: Component {
return self.textField.text ?? "" return self.textField.text ?? ""
} }
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.textField = TextField() self.textField = TextField()
@ -81,6 +103,27 @@ public final class ListTextFieldItemComponent: Component {
if !self.isUpdating { if !self.isUpdating {
self.state?.updated(transition: .immediate) 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 { 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) 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 let themeUpdated = component.theme !== previousComponent?.theme
if themeUpdated { 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)) text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
)), )),
environment: {}, 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 contentHeight: CGFloat = placeholderSize.height + verticalInset * 2.0
let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((contentHeight - placeholderSize.height) * 0.5)), size: placeholderSize) 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))) 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) return CGSize(width: availableSize.width, height: contentHeight)
} }
} }

View File

@ -1098,7 +1098,8 @@ public final class MessageInputPanelComponent: Component {
contents = AnyComponent(PlainButtonComponent( contents = AnyComponent(PlainButtonComponent(
content: AnyComponent(VStack([ content: AnyComponent(VStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( 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( AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor)) 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) interaction.openSettings(.premium)
})) }))
//TODO:localize //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) 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: { 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 content: AnyComponent<Empty>
public let background: AnyComponent<Empty>?
public let effectAlignment: EffectAlignment public let effectAlignment: EffectAlignment
public let minSize: CGSize? public let minSize: CGSize?
public let contentInsets: UIEdgeInsets public let contentInsets: UIEdgeInsets
@ -23,6 +24,7 @@ public final class PlainButtonComponent: Component {
public init( public init(
content: AnyComponent<Empty>, content: AnyComponent<Empty>,
background: AnyComponent<Empty>? = nil,
effectAlignment: EffectAlignment, effectAlignment: EffectAlignment,
minSize: CGSize? = nil, minSize: CGSize? = nil,
contentInsets: UIEdgeInsets = UIEdgeInsets(), contentInsets: UIEdgeInsets = UIEdgeInsets(),
@ -34,6 +36,7 @@ public final class PlainButtonComponent: Component {
tag: AnyObject? = nil tag: AnyObject? = nil
) { ) {
self.content = content self.content = content
self.background = background
self.effectAlignment = effectAlignment self.effectAlignment = effectAlignment
self.minSize = minSize self.minSize = minSize
self.contentInsets = contentInsets self.contentInsets = contentInsets
@ -49,6 +52,9 @@ public final class PlainButtonComponent: Component {
if lhs.content != rhs.content { if lhs.content != rhs.content {
return false return false
} }
if lhs.background != rhs.background {
return false
}
if lhs.effectAlignment != rhs.effectAlignment { if lhs.effectAlignment != rhs.effectAlignment {
return false return false
} }
@ -92,6 +98,7 @@ public final class PlainButtonComponent: Component {
private let contentContainer = UIView() private let contentContainer = UIView()
private let content = ComponentView<Empty>() private let content = ComponentView<Empty>()
private var background: ComponentView<Empty>?
public var contentView: UIView? { public var contentView: UIView? {
return self.content.view 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.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)) 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 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 var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight contentHeight += environment.navigationHeight
contentHeight += 81.0 contentHeight += 16.0
//TODO:localize //TODO:localize
let titleSize = self.title.update( let titleSize = self.title.update(
@ -241,14 +241,22 @@ final class BusinessSetupScreenComponent: Component {
icon: "Settings/Menu/AddAccount", icon: "Settings/Menu/AddAccount",
title: "Location", title: "Location",
subtitle: "Display the location of your business on your account.", 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( items.append(Item(
icon: "Settings/Menu/DataVoice", icon: "Settings/Menu/DataVoice",
title: "Opening Hours", title: "Opening Hours",
subtitle: "Show to your customers when you are open for business.", 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( items.append(Item(
@ -262,7 +270,11 @@ final class BusinessSetupScreenComponent: Component {
icon: "Settings/Menu/Stories", icon: "Settings/Menu/Stories",
title: "Greeting Messages", title: "Greeting Messages",
subtitle: "Create greetings that will be automatically sent to new customers.", 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( items.append(Item(

View File

@ -31,6 +31,10 @@ swift_library(
"//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListTextFieldItemComponent", "//submodules/TelegramUI/Components/ListTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/ShimmerEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//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 BundleIconComponent
import LottieComponent import LottieComponent
import Markdown import Markdown
import PeerListItemComponent
import AvatarNode
private let checkIcon: UIImage = { private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in 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 { final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer() private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView private let scrollView: ScrollView
@ -79,6 +124,18 @@ final class ChatbotSetupScreenComponent: Component {
private var environment: EnvironmentType? private var environment: EnvironmentType?
private var chevronImage: UIImage? 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) { override init(frame: CGRect) {
self.scrollView = ScrollView() 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 { func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
@ -221,7 +456,7 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += 129.0 contentHeight += 129.0
//TODO:localize //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), body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(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), link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
@ -239,7 +474,7 @@ final class ChatbotSetupScreenComponent: Component {
//TODO:localize //TODO:localize
let subtitleSize = self.subtitle.update( let subtitleSize = self.subtitle.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(BalancedTextComponent(
text: .plain(subtitleString), text: .plain(subtitleString),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
@ -273,6 +508,66 @@ final class ChatbotSetupScreenComponent: Component {
contentHeight += subtitleSize.height contentHeight += subtitleSize.height
contentHeight += 27.0 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 //TODO:localize
let nameSectionSize = self.nameSection.update( let nameSectionSize = self.nameSection.update(
transition: transition, transition: transition,
@ -287,15 +582,7 @@ final class ChatbotSetupScreenComponent: Component {
)), )),
maximumNumberOfLines: 0 maximumNumberOfLines: 0
)), )),
items: [ items: nameSectionItems
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
theme: environment.theme,
initialText: "",
placeholder: "Bot Username",
updated: { value in
}
)))
]
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@ -339,11 +626,20 @@ final class ChatbotSetupScreenComponent: Component {
], alignment: .left, spacing: 2.0)), ], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon, image: checkIcon,
tintColor: environment.theme.list.itemAccentColor, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center contentMode: .center
))), ))),
accessory: nil, 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( AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent(
@ -360,11 +656,20 @@ final class ChatbotSetupScreenComponent: Component {
], alignment: .left, spacing: 2.0)), ], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon, image: checkIcon,
tintColor: .clear, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center contentMode: .center
))), ))),
accessory: nil, 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 += accessSectionSize.height
contentHeight += sectionSpacing 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 //TODO:localize
let excludedSectionSize = self.excludedSection.update( let excludedSectionSize = self.excludedSection.update(
transition: transition, transition: transition,
@ -389,42 +783,27 @@ final class ChatbotSetupScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
header: AnyComponent(MultilineTextComponent( header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "EXCLUDED CHATS", string: self.hasAccessToAllChatsByDefault ? "EXCLUDED CHATS" : "INCLUDED CHATS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor textColor: environment.theme.list.freeTextColor
)), )),
maximumNumberOfLines: 0 maximumNumberOfLines: 0
)), )),
footer: AnyComponent(MultilineTextComponent( footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .markdown(
string: "Select chats or entire chat categories which the bot WILL NOT have access to.", 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.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), attributes: MarkdownAttributes(
textColor: environment.theme.list.freeTextColor 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 maximumNumberOfLines: 0
)), )),
items: [ items: excludedSectionItems
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
}
))),
]
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@ -473,7 +852,13 @@ final class ChatbotSetupScreenComponent: Component {
maximumNumberOfLines: 1 maximumNumberOfLines: 1
))), ))),
], alignment: .left, spacing: 2.0)), ], 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 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( AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent(
theme: environment.theme, theme: environment.theme,
title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)), 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, context: component.context,
color: profileColor.flatMap { profileColor in color: profileColor.flatMap { profileColor in
component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main
} ?? environment.theme.list.itemAccentColor, } ?? environment.theme.list.itemAccentColor,
fileId: backgroundFileId, fileId: backgroundFileId,
file: backgroundFileId.flatMap { self.cachedIconFiles[$0] } file: backgroundFileId.flatMap { self.cachedIconFiles[$0] }
))), )))),
action: { [weak self] view in action: { [weak self] view in
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return return
@ -1393,12 +1393,12 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme, theme: environment.theme,
title: AnyComponent(HStack(emojiPackContents, spacing: 6.0)), 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, context: component.context,
color: environment.theme.list.itemAccentColor, color: environment.theme.list.itemAccentColor,
fileId: emojiPack?.thumbnailFileId, fileId: emojiPack?.thumbnailFileId,
file: emojiPackFile file: emojiPackFile
))), )))),
action: { [weak self] view in action: { [weak self] view in
guard let self, let resolvedState = self.resolveState() else { guard let self, let resolvedState = self.resolveState() else {
return return
@ -1457,12 +1457,12 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme, theme: environment.theme,
title: AnyComponent(HStack(emojiStatusContents, spacing: 6.0)), 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, context: component.context,
color: environment.theme.list.itemAccentColor, color: environment.theme.list.itemAccentColor,
fileId: statusFileId, fileId: statusFileId,
file: statusFileId.flatMap { self.cachedIconFiles[$0] } file: statusFileId.flatMap { self.cachedIconFiles[$0] }
))), )))),
action: { [weak self] view in action: { [weak self] view in
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return return
@ -1581,12 +1581,12 @@ final class ChannelAppearanceScreenComponent: Component {
AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent(
theme: environment.theme, theme: environment.theme,
title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)), 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, context: component.context,
color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main,
fileId: replyFileId, fileId: replyFileId,
file: replyFileId.flatMap { self.cachedIconFiles[$0] } file: replyFileId.flatMap { self.cachedIconFiles[$0] }
))), )))),
action: { [weak self] view in action: { [weak self] view in
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return 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/ContextUI",
"//submodules/TextFormat", "//submodules/TextFormat",
"//submodules/PhotoResources", "//submodules/PhotoResources",
"//submodules/TelegramUI/Components/ListSectionComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

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

View File

@ -193,7 +193,7 @@ public final class TextFieldComponent: Component {
private let ellipsisView = ComponentView<Empty>() 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) 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) 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 additionalCategories = chatSelection.additionalCategories
let chatListFilters = chatSelection.chatListFilters 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 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.passthroughPeerSelection = true
chatListNode.disabledPeerSelected = { peer, _, reason in chatListNode.disabledPeerSelected = { peer, _, reason in
attemptDisabledItemSelection?(peer, reason) attemptDisabledItemSelection?(peer, reason)

View File

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