mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Business
This commit is contained in:
parent
71a40dcdb2
commit
d9fec0a500
@ -934,6 +934,9 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeArchiveSettingsController(context: AccountContext) -> ViewController
|
||||
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeBusinessHoursSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>
|
||||
|
@ -45,6 +45,7 @@ public enum ContactMultiselectionControllerMode {
|
||||
public var chatListFilters: [ChatListFilter]?
|
||||
public var displayAutoremoveTimeout: Bool
|
||||
public var displayPresence: Bool
|
||||
public var onlyUsers: Bool
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
@ -53,7 +54,8 @@ public enum ContactMultiselectionControllerMode {
|
||||
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
|
||||
chatListFilters: [ChatListFilter]?,
|
||||
displayAutoremoveTimeout: Bool = false,
|
||||
displayPresence: Bool = false
|
||||
displayPresence: Bool = false,
|
||||
onlyUsers: Bool = false
|
||||
) {
|
||||
self.title = title
|
||||
self.searchPlaceholder = searchPlaceholder
|
||||
@ -62,6 +64,7 @@ public enum ContactMultiselectionControllerMode {
|
||||
self.chatListFilters = chatListFilters
|
||||
self.displayAutoremoveTimeout = displayAutoremoveTimeout
|
||||
self.displayPresence = displayPresence
|
||||
self.onlyUsers = onlyUsers
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3185,7 +3185,7 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
|
||||
private func resetFilter() {
|
||||
if let chatListFilter = self.chatListFilter {
|
||||
if let chatListFilter = self.chatListFilter, chatListFilter.id != Int32.max {
|
||||
self.updatedFilterDisposable.set((self.context.engine.peers.updatedChatListFilters()
|
||||
|> map { filters -> ChatListFilter? in
|
||||
for filter in filters {
|
||||
@ -4113,14 +4113,16 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
|
||||
if isContact {
|
||||
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
||||
} else {
|
||||
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
||||
//TODO:localize
|
||||
return ("non-contact", false, false, nil)
|
||||
}
|
||||
}
|
||||
} else if case .secretChat = peer {
|
||||
if isContact {
|
||||
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
||||
} else {
|
||||
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
||||
//TODO:localize
|
||||
return ("non-contact", false, false, nil)
|
||||
}
|
||||
} else if case .legacyGroup = peer {
|
||||
return (strings.ChatList_PeerTypeGroup, false, false, nil)
|
||||
|
@ -67,7 +67,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
|
||||
}
|
||||
if !filter.categories.contains(.contacts) && isContact {
|
||||
if let user = peer as? TelegramUser {
|
||||
if user.botInfo == nil {
|
||||
if user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||
return false
|
||||
}
|
||||
} else if let _ = peer as? TelegramSecretChat {
|
||||
@ -88,7 +88,7 @@ public func chatListFilterPredicate(filter: ChatListFilterData, accountPeerId: E
|
||||
}
|
||||
if !filter.categories.contains(.bots) {
|
||||
if let user = peer as? TelegramUser {
|
||||
if user.botInfo != nil {
|
||||
if user.botInfo != nil || user.flags.contains(.isSupport) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -722,11 +722,11 @@ public extension CombinedComponent {
|
||||
updatedChild.view.layer.shadowRadius = 0.0
|
||||
updatedChild.view.layer.shadowOpacity = 0.0
|
||||
}
|
||||
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
||||
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition, isLocal in
|
||||
guard let viewContext = viewContext else {
|
||||
return
|
||||
}
|
||||
viewContext.state.updated(transition: transition)
|
||||
viewContext.state.updated(transition: transition, isLocal: isLocal)
|
||||
}
|
||||
|
||||
if let transitionAppearWithGuide = updatedChild.transitionAppearWithGuide {
|
||||
|
@ -89,15 +89,15 @@ extension UIView {
|
||||
}
|
||||
|
||||
open class ComponentState {
|
||||
open var _updated: ((Transition) -> Void)?
|
||||
open var _updated: ((Transition, Bool) -> Void)?
|
||||
var isUpdated: Bool = false
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public final func updated(transition: Transition = .immediate) {
|
||||
public final func updated(transition: Transition = .immediate, isLocal: Bool = false) {
|
||||
self.isUpdated = true
|
||||
self._updated?(transition)
|
||||
self._updated?(transition, isLocal)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,16 +8,16 @@ public final class RoundedRectangle: Component {
|
||||
}
|
||||
|
||||
public let colors: [UIColor]
|
||||
public let cornerRadius: CGFloat
|
||||
public let cornerRadius: CGFloat?
|
||||
public let gradientDirection: GradientDirection
|
||||
public let stroke: CGFloat?
|
||||
public let strokeColor: UIColor?
|
||||
|
||||
public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
|
||||
public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
|
||||
self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor)
|
||||
}
|
||||
|
||||
public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
|
||||
public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) {
|
||||
self.colors = colors
|
||||
self.cornerRadius = cornerRadius
|
||||
self.gradientDirection = gradientDirection
|
||||
@ -49,8 +49,10 @@ public final class RoundedRectangle: Component {
|
||||
|
||||
func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
if self.component != component {
|
||||
let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5
|
||||
|
||||
if component.colors.count == 1, let color = component.colors.first {
|
||||
let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0)
|
||||
let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0)
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
if let strokeColor = component.strokeColor {
|
||||
@ -69,13 +71,13 @@ public final class RoundedRectangle: Component {
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke))
|
||||
}
|
||||
}
|
||||
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
|
||||
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
|
||||
UIGraphicsEndImageContext()
|
||||
} else if component.colors.count > 1 {
|
||||
let imageSize = availableSize
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: component.cornerRadius).cgPath)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath)
|
||||
context.clip()
|
||||
|
||||
let colors = component.colors
|
||||
@ -93,12 +95,12 @@ public final class RoundedRectangle: Component {
|
||||
if let stroke = component.stroke, stroke > 0.0 {
|
||||
context.resetClip()
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: component.cornerRadius).cgPath)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke), cornerRadius: cornerRadius).cgPath)
|
||||
context.setBlendMode(.clear)
|
||||
context.fill(CGRect(origin: .zero, size: imageSize))
|
||||
}
|
||||
}
|
||||
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
|
||||
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius))
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ public final class ComponentHostView<EnvironmentType>: UIView {
|
||||
self.currentComponent = component
|
||||
self.currentContainerSize = containerSize
|
||||
|
||||
componentState._updated = { [weak self] transition in
|
||||
componentState._updated = { [weak self] transition, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -208,11 +208,11 @@ public final class ComponentView<EnvironmentType> {
|
||||
self.currentComponent = component
|
||||
self.currentContainerSize = containerSize
|
||||
|
||||
componentState._updated = { [weak self] transition in
|
||||
componentState._updated = { [weak self] transition, isLocal in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let parentState = strongSelf.parentState {
|
||||
if !isLocal, let parentState = strongSelf.parentState {
|
||||
parentState.updated(transition: transition)
|
||||
} else {
|
||||
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
|
||||
|
@ -567,6 +567,9 @@
|
||||
updateGroupingButtonVisibility();
|
||||
} file:__FILE_NAME__ line:__LINE__]];
|
||||
|
||||
if (_adjustmentsChangedDisposable) {
|
||||
[_adjustmentsChangedDisposable dispose];
|
||||
}
|
||||
_adjustmentsChangedDisposable = [[SMetaDisposable alloc] init];
|
||||
[_adjustmentsChangedDisposable setDisposable:[_editingContext.adjustmentsUpdatedSignal startStrictWithNext:^(__unused NSNumber *next)
|
||||
{
|
||||
@ -583,6 +586,7 @@
|
||||
self.delegate = nil;
|
||||
[_selectionChangedDisposable dispose];
|
||||
[_tooltipDismissDisposable dispose];
|
||||
[_adjustmentsChangedDisposable dispose];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
|
@ -187,7 +187,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab
|
||||
if ["home", "work"].contains(venueType) {
|
||||
completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil)
|
||||
} else {
|
||||
completion(venue, queryId, resultId, nil, nil)
|
||||
completion(venue, queryId, resultId, venue.venue?.address, nil)
|
||||
}
|
||||
strongSelf.dismiss()
|
||||
}, toggleMapModeSelection: { [weak self] in
|
||||
|
@ -11,10 +11,41 @@ private func drawBorder(context: CGContext, rect: CGRect) {
|
||||
context.strokePath()
|
||||
}
|
||||
|
||||
private func renderIcon(name: String) -> UIImage? {
|
||||
private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) {
|
||||
context.saveGState()
|
||||
context.translateBy(x: rect.minX, y: rect.minY)
|
||||
context.scaleBy(x: radius, y: radius)
|
||||
let fw = rect.width / radius
|
||||
let fh = rect.height / radius
|
||||
context.move(to: CGPoint(x: fw, y: fh / 2.0))
|
||||
context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0)
|
||||
context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1)
|
||||
context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1)
|
||||
context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1)
|
||||
context.closePath()
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
private func renderIcon(name: String, backgroundColors: [UIColor]? = nil) -> UIImage? {
|
||||
return generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
if let backgroundColors {
|
||||
addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: 7.0)
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor] = backgroundColors.map(\.cgColor)
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
|
||||
|
||||
context.resetClip()
|
||||
}
|
||||
|
||||
if let image = UIImage(bundleImageName: name)?.cgImage {
|
||||
context.draw(image, in: bounds)
|
||||
}
|
||||
@ -38,6 +69,7 @@ public struct PresentationResourcesSettings {
|
||||
public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving")
|
||||
public static let stories = renderIcon(name: "Settings/Menu/Stories")
|
||||
public static let premiumGift = renderIcon(name: "Settings/Menu/Gift")
|
||||
public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)])
|
||||
|
||||
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
|
@ -21,9 +21,9 @@ public func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat
|
||||
periodString = "AM"
|
||||
}
|
||||
if minutes >= 10 {
|
||||
return "\(hourString):\(minutes) \(periodString)"
|
||||
return "\(hourString):\(minutes)\u{00a0}\(periodString)"
|
||||
} else {
|
||||
return "\(hourString):0\(minutes) \(periodString)"
|
||||
return "\(hourString):0\(minutes)\u{00a0}\(periodString)"
|
||||
}
|
||||
case .military:
|
||||
return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)])
|
||||
|
@ -434,6 +434,9 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent",
|
||||
"//submodules/TelegramUI/Components/Settings/BusinessSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/GreetingMessageSetupScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -57,7 +57,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
|
||||
super.init()
|
||||
|
||||
self.state._updated = { [weak self] transition in
|
||||
self.state._updated = { [weak self] transition, _ in
|
||||
if let self {
|
||||
self.update(transition: transition.containedViewLayoutTransition)
|
||||
}
|
||||
|
@ -7,15 +7,67 @@ import ListSectionComponent
|
||||
import SwitchNode
|
||||
|
||||
public final class ListActionItemComponent: Component {
|
||||
public enum ToggleStyle {
|
||||
case regular
|
||||
case icons
|
||||
}
|
||||
|
||||
public struct Toggle: Equatable {
|
||||
public var style: ToggleStyle
|
||||
public var isOn: Bool
|
||||
public var isInteractive: Bool
|
||||
public var action: ((Bool) -> Void)?
|
||||
|
||||
public init(style: ToggleStyle, isOn: Bool, isInteractive: Bool = true, action: ((Bool) -> Void)? = nil) {
|
||||
self.style = style
|
||||
self.isOn = isOn
|
||||
self.isInteractive = isInteractive
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: Toggle, rhs: Toggle) -> Bool {
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.isOn != rhs.isOn {
|
||||
return false
|
||||
}
|
||||
if lhs.isInteractive != rhs.isInteractive {
|
||||
return false
|
||||
}
|
||||
if (lhs.action == nil) != (rhs.action == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public enum Accessory: Equatable {
|
||||
case arrow
|
||||
case toggle(Bool)
|
||||
case toggle(Toggle)
|
||||
}
|
||||
|
||||
public enum IconInsets: Equatable {
|
||||
case `default`
|
||||
case custom(UIEdgeInsets)
|
||||
}
|
||||
|
||||
public struct Icon: Equatable {
|
||||
public var component: AnyComponentWithIdentity<Empty>
|
||||
public var insets: IconInsets
|
||||
public var allowUserInteraction: Bool
|
||||
|
||||
public init(component: AnyComponentWithIdentity<Empty>, insets: IconInsets = .default, allowUserInteraction: Bool = false) {
|
||||
self.component = component
|
||||
self.insets = insets
|
||||
self.allowUserInteraction = allowUserInteraction
|
||||
}
|
||||
}
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let title: AnyComponent<Empty>
|
||||
public let leftIcon: AnyComponentWithIdentity<Empty>?
|
||||
public let icon: AnyComponentWithIdentity<Empty>?
|
||||
public let icon: Icon?
|
||||
public let accessory: Accessory?
|
||||
public let action: ((UIView) -> Void)?
|
||||
|
||||
@ -23,7 +75,7 @@ public final class ListActionItemComponent: Component {
|
||||
theme: PresentationTheme,
|
||||
title: AnyComponent<Empty>,
|
||||
leftIcon: AnyComponentWithIdentity<Empty>? = nil,
|
||||
icon: AnyComponentWithIdentity<Empty>? = nil,
|
||||
icon: Icon? = nil,
|
||||
accessory: Accessory? = .arrow,
|
||||
action: ((UIView) -> Void)?
|
||||
) {
|
||||
@ -63,7 +115,8 @@ public final class ListActionItemComponent: Component {
|
||||
private var icon: ComponentView<Empty>?
|
||||
|
||||
private var arrowView: UIImageView?
|
||||
private var switchNode: IconSwitchNode?
|
||||
private var switchNode: SwitchNode?
|
||||
private var iconSwitchNode: IconSwitchNode?
|
||||
|
||||
private var component: ListActionItemComponent?
|
||||
|
||||
@ -83,7 +136,10 @@ public final class ListActionItemComponent: Component {
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.internalHighligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self else {
|
||||
guard let self, let component = self.component, component.action != nil else {
|
||||
return
|
||||
}
|
||||
if case .toggle = component.accessory, component.action == nil {
|
||||
return
|
||||
}
|
||||
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
|
||||
@ -97,15 +153,23 @@ public final class ListActionItemComponent: Component {
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.component?.action?(self)
|
||||
guard let component, let action = component.action else {
|
||||
return
|
||||
}
|
||||
action(self)
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let result = super.hitTest(point, with: event) else {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
self.isEnabled = component.action != nil
|
||||
|
||||
let themeUpdated = component.theme !== previousComponent?.theme
|
||||
|
||||
let verticalInset: CGFloat = 12.0
|
||||
@ -118,7 +182,7 @@ public final class ListActionItemComponent: Component {
|
||||
case .arrow:
|
||||
contentRightInset = 30.0
|
||||
case .toggle:
|
||||
contentRightInset = 42.0
|
||||
contentRightInset = 76.0
|
||||
}
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
@ -147,7 +211,7 @@ public final class ListActionItemComponent: Component {
|
||||
contentHeight += verticalInset
|
||||
|
||||
if let iconValue = component.icon {
|
||||
if previousComponent?.icon?.id != iconValue.id, let icon = self.icon {
|
||||
if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon {
|
||||
self.icon = nil
|
||||
if let iconView = icon.view {
|
||||
transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in
|
||||
@ -168,17 +232,17 @@ public final class ListActionItemComponent: Component {
|
||||
|
||||
let iconSize = icon.update(
|
||||
transition: iconTransition,
|
||||
component: iconValue.component,
|
||||
component: iconValue.component.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
|
||||
if let iconView = icon.view {
|
||||
if iconView.superview == nil {
|
||||
iconView.isUserInteractionEnabled = false
|
||||
self.addSubview(iconView)
|
||||
transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
|
||||
}
|
||||
iconView.isUserInteractionEnabled = iconValue.allowUserInteraction
|
||||
iconTransition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
} else {
|
||||
@ -263,32 +327,85 @@ public final class ListActionItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if case let .toggle(isOn) = component.accessory {
|
||||
let switchNode: IconSwitchNode
|
||||
var switchTransition = transition
|
||||
var updateSwitchTheme = themeUpdated
|
||||
if let current = self.switchNode {
|
||||
switchNode = current
|
||||
switchNode.setOn(isOn, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
switchTransition = switchTransition.withAnimation(.none)
|
||||
updateSwitchTheme = true
|
||||
switchNode = IconSwitchNode()
|
||||
switchNode.setOn(isOn, animated: false)
|
||||
self.addSubview(switchNode.view)
|
||||
if case let .toggle(toggle) = component.accessory {
|
||||
switch toggle.style {
|
||||
case .regular:
|
||||
let switchNode: SwitchNode
|
||||
var switchTransition = transition
|
||||
var updateSwitchTheme = themeUpdated
|
||||
if let current = self.switchNode {
|
||||
switchNode = current
|
||||
switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
switchTransition = switchTransition.withAnimation(.none)
|
||||
updateSwitchTheme = true
|
||||
switchNode = SwitchNode()
|
||||
switchNode.setOn(toggle.isOn, animated: false)
|
||||
self.switchNode = switchNode
|
||||
self.addSubview(switchNode.view)
|
||||
|
||||
switchNode.valueUpdated = { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case let .toggle(toggle) = component.accessory, let action = toggle.action {
|
||||
action(value)
|
||||
} else {
|
||||
component.action?(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
switchNode.isUserInteractionEnabled = toggle.isInteractive
|
||||
|
||||
if updateSwitchTheme {
|
||||
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
|
||||
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
|
||||
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
|
||||
}
|
||||
|
||||
let switchSize = CGSize(width: 51.0, height: 31.0)
|
||||
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
|
||||
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
|
||||
case .icons:
|
||||
let switchNode: IconSwitchNode
|
||||
var switchTransition = transition
|
||||
var updateSwitchTheme = themeUpdated
|
||||
if let current = self.iconSwitchNode {
|
||||
switchNode = current
|
||||
switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
switchTransition = switchTransition.withAnimation(.none)
|
||||
updateSwitchTheme = true
|
||||
switchNode = IconSwitchNode()
|
||||
switchNode.setOn(toggle.isOn, animated: false)
|
||||
self.iconSwitchNode = switchNode
|
||||
self.addSubview(switchNode.view)
|
||||
|
||||
switchNode.valueUpdated = { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case let .toggle(toggle) = component.accessory, let action = toggle.action {
|
||||
action(value)
|
||||
} else {
|
||||
component.action?(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
switchNode.isUserInteractionEnabled = toggle.isInteractive
|
||||
|
||||
if updateSwitchTheme {
|
||||
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
|
||||
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
|
||||
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
|
||||
switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor
|
||||
switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor
|
||||
}
|
||||
|
||||
let switchSize = CGSize(width: 51.0, height: 31.0)
|
||||
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
|
||||
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
|
||||
}
|
||||
|
||||
if updateSwitchTheme {
|
||||
switchNode.frameColor = component.theme.list.itemSwitchColors.frameColor
|
||||
switchNode.contentColor = component.theme.list.itemSwitchColors.contentColor
|
||||
switchNode.handleColor = component.theme.list.itemSwitchColors.handleColor
|
||||
switchNode.positiveContentColor = component.theme.list.itemSwitchColors.positiveColor
|
||||
switchNode.negativeContentColor = component.theme.list.itemSwitchColors.negativeColor
|
||||
}
|
||||
|
||||
let switchSize = CGSize(width: 51.0, height: 31.0)
|
||||
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
|
||||
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
|
||||
} else {
|
||||
if let switchNode = self.switchNode {
|
||||
self.switchNode = nil
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -217,6 +217,7 @@ public final class ListSectionComponent: Component {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ItemView()
|
||||
self.itemViews[itemId] = itemView
|
||||
itemView.contents.parentState = state
|
||||
}
|
||||
|
||||
let itemSize = itemView.contents.update(
|
||||
|
@ -14,6 +14,9 @@ swift_library(
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,23 +4,35 @@ import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import ListSectionComponent
|
||||
import PlainButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class ListTextFieldItemComponent: Component {
|
||||
public let theme: PresentationTheme
|
||||
public let initialText: String
|
||||
public let placeholder: String
|
||||
public let autocapitalizationType: UITextAutocapitalizationType
|
||||
public let autocorrectionType: UITextAutocorrectionType
|
||||
public let updated: ((String) -> Void)?
|
||||
public let tag: AnyObject?
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
initialText: String,
|
||||
placeholder: String,
|
||||
updated: ((String) -> Void)?
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
updated: ((String) -> Void)?,
|
||||
tag: AnyObject? = nil
|
||||
) {
|
||||
self.theme = theme
|
||||
self.initialText = initialText
|
||||
self.placeholder = placeholder
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.updated = updated
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
public static func ==(lhs: ListTextFieldItemComponent, rhs: ListTextFieldItemComponent) -> Bool {
|
||||
@ -33,6 +45,12 @@ public final class ListTextFieldItemComponent: Component {
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocorrectionType != rhs.autocorrectionType {
|
||||
return false
|
||||
}
|
||||
if (lhs.updated == nil) != (rhs.updated == nil) {
|
||||
return false
|
||||
}
|
||||
@ -51,9 +69,10 @@ public final class ListTextFieldItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UITextFieldDelegate {
|
||||
public final class View: UIView, UITextFieldDelegate, ListSectionComponent.ChildView, ComponentTaggedView {
|
||||
private let textField: TextField
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
private let clearButton = ComponentView<Empty>()
|
||||
|
||||
private var component: ListTextFieldItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -63,6 +82,9 @@ public final class ListTextFieldItemComponent: Component {
|
||||
return self.textField.text ?? ""
|
||||
}
|
||||
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public private(set) var separatorInset: CGFloat = 0.0
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.textField = TextField()
|
||||
|
||||
@ -81,6 +103,27 @@ public final class ListTextFieldItemComponent: Component {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
self.component?.updated?(self.currentText)
|
||||
}
|
||||
|
||||
public func setText(text: String, updateState: Bool) {
|
||||
self.textField.text = text
|
||||
if updateState {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
self.component?.updated?(self.currentText)
|
||||
} else {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func update(component: ListTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
@ -102,6 +145,13 @@ public final class ListTextFieldItemComponent: Component {
|
||||
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
|
||||
}
|
||||
|
||||
if self.textField.autocapitalizationType != component.autocapitalizationType {
|
||||
self.textField.autocapitalizationType = component.autocapitalizationType
|
||||
}
|
||||
if self.textField.autocorrectionType != component.autocorrectionType {
|
||||
self.textField.autocorrectionType = component.autocorrectionType
|
||||
}
|
||||
|
||||
let themeUpdated = component.theme !== previousComponent?.theme
|
||||
|
||||
if themeUpdated {
|
||||
@ -120,7 +170,7 @@ public final class ListTextFieldItemComponent: Component {
|
||||
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 30.0, height: 100.0)
|
||||
)
|
||||
let contentHeight: CGFloat = placeholderSize.height + verticalInset * 2.0
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((contentHeight - placeholderSize.height) * 0.5)), size: placeholderSize)
|
||||
@ -138,6 +188,37 @@ public final class ListTextFieldItemComponent: Component {
|
||||
|
||||
transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: contentHeight)))
|
||||
|
||||
let clearButtonSize = self.clearButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Components/Search Bar/Clear",
|
||||
tintColor: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setText(text: "", updateState: true)
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
if let clearButtonView = self.clearButton.view {
|
||||
if clearButtonView.superview == nil {
|
||||
self.addSubview(clearButtonView)
|
||||
}
|
||||
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 0.0 - clearButtonSize.width, y: floor((contentHeight - clearButtonSize.height) * 0.5)), size: clearButtonSize))
|
||||
clearButtonView.isHidden = self.currentText.isEmpty
|
||||
}
|
||||
|
||||
self.separatorInset = 16.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
@ -1098,7 +1098,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
contents = AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: title, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.3)))
|
||||
text: .plain(NSAttributedString(string: title, font: Font.regular(13.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.3))),
|
||||
maximumNumberOfLines: 2
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor))
|
||||
|
@ -927,7 +927,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
interaction.openSettings(.premium)
|
||||
}))
|
||||
//TODO:localize
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Telegram Business", icon: PresentationResourcesSettings.chatFolders, action: {
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Telegram Business", icon: PresentationResourcesSettings.business, action: {
|
||||
interaction.openSettings(.businessSetup)
|
||||
}))
|
||||
items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: {
|
||||
|
@ -11,6 +11,7 @@ public final class PlainButtonComponent: Component {
|
||||
}
|
||||
|
||||
public let content: AnyComponent<Empty>
|
||||
public let background: AnyComponent<Empty>?
|
||||
public let effectAlignment: EffectAlignment
|
||||
public let minSize: CGSize?
|
||||
public let contentInsets: UIEdgeInsets
|
||||
@ -23,6 +24,7 @@ public final class PlainButtonComponent: Component {
|
||||
|
||||
public init(
|
||||
content: AnyComponent<Empty>,
|
||||
background: AnyComponent<Empty>? = nil,
|
||||
effectAlignment: EffectAlignment,
|
||||
minSize: CGSize? = nil,
|
||||
contentInsets: UIEdgeInsets = UIEdgeInsets(),
|
||||
@ -34,6 +36,7 @@ public final class PlainButtonComponent: Component {
|
||||
tag: AnyObject? = nil
|
||||
) {
|
||||
self.content = content
|
||||
self.background = background
|
||||
self.effectAlignment = effectAlignment
|
||||
self.minSize = minSize
|
||||
self.contentInsets = contentInsets
|
||||
@ -49,6 +52,9 @@ public final class PlainButtonComponent: Component {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.background != rhs.background {
|
||||
return false
|
||||
}
|
||||
if lhs.effectAlignment != rhs.effectAlignment {
|
||||
return false
|
||||
}
|
||||
@ -92,6 +98,7 @@ public final class PlainButtonComponent: Component {
|
||||
|
||||
private let contentContainer = UIView()
|
||||
private let content = ComponentView<Empty>()
|
||||
private var background: ComponentView<Empty>?
|
||||
|
||||
public var contentView: UIView? {
|
||||
return self.content.view
|
||||
@ -243,6 +250,33 @@ public final class PlainButtonComponent: Component {
|
||||
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setPosition(view: self.contentContainer, position: CGPoint(x: size.width * anchorX, y: size.height * 0.5))
|
||||
|
||||
if let backgroundValue = component.background {
|
||||
var backgroundTransition = transition
|
||||
let background: ComponentView<Empty>
|
||||
if let current = self.background {
|
||||
background = current
|
||||
} else {
|
||||
backgroundTransition = .immediate
|
||||
background = ComponentView()
|
||||
self.background = background
|
||||
}
|
||||
let _ = background.update(
|
||||
transition: backgroundTransition,
|
||||
component: backgroundValue,
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
if let backgroundView = background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.contentContainer.insertSubview(backgroundView, at: 0)
|
||||
}
|
||||
backgroundTransition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
} else if let background = self.background {
|
||||
self.background = nil
|
||||
background.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -182,7 +182,7 @@ final class BusinessSetupScreenComponent: Component {
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
contentHeight += environment.navigationHeight
|
||||
contentHeight += 81.0
|
||||
contentHeight += 16.0
|
||||
|
||||
//TODO:localize
|
||||
let titleSize = self.title.update(
|
||||
@ -241,14 +241,22 @@ final class BusinessSetupScreenComponent: Component {
|
||||
icon: "Settings/Menu/AddAccount",
|
||||
title: "Location",
|
||||
subtitle: "Display the location of your business on your account.",
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.push(component.context.sharedContext.makeBusinessLocationSetupScreen(context: component.context))
|
||||
}
|
||||
))
|
||||
items.append(Item(
|
||||
icon: "Settings/Menu/DataVoice",
|
||||
title: "Opening Hours",
|
||||
subtitle: "Show to your customers when you are open for business.",
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.push(component.context.sharedContext.makeBusinessHoursSetupScreen(context: component.context))
|
||||
}
|
||||
))
|
||||
items.append(Item(
|
||||
@ -262,7 +270,11 @@ final class BusinessSetupScreenComponent: Component {
|
||||
icon: "Settings/Menu/Stories",
|
||||
title: "Greeting Messages",
|
||||
subtitle: "Create greetings that will be automatically sent to new customers.",
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context))
|
||||
}
|
||||
))
|
||||
items.append(Item(
|
||||
|
@ -31,6 +31,10 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ListTextFieldItemComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||
"//submodules/ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -21,6 +21,8 @@ import ListTextFieldItemComponent
|
||||
import BundleIconComponent
|
||||
import LottieComponent
|
||||
import Markdown
|
||||
import PeerListItemComponent
|
||||
import AvatarNode
|
||||
|
||||
private let checkIcon: UIImage = {
|
||||
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
|
||||
@ -60,6 +62,49 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private struct BotResolutionState: Equatable {
|
||||
enum State: Equatable {
|
||||
case searching
|
||||
case notFound
|
||||
case found(peer: EnginePeer, isInstalled: Bool)
|
||||
}
|
||||
|
||||
var query: String
|
||||
var state: State
|
||||
|
||||
init(query: String, state: State) {
|
||||
self.query = query
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
private struct AdditionalPeerList {
|
||||
enum Category: Int {
|
||||
case newChats = 0
|
||||
case existingChats = 1
|
||||
case contacts = 2
|
||||
case nonContacts = 3
|
||||
}
|
||||
|
||||
struct Peer {
|
||||
var peer: EnginePeer
|
||||
var isContact: Bool
|
||||
|
||||
init(peer: EnginePeer, isContact: Bool) {
|
||||
self.peer = peer
|
||||
self.isContact = isContact
|
||||
}
|
||||
}
|
||||
|
||||
var categories: Set<Category>
|
||||
var peers: [Peer]
|
||||
|
||||
init(categories: Set<Category>, peers: [Peer]) {
|
||||
self.categories = categories
|
||||
self.peers = peers
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
@ -79,6 +124,18 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var chevronImage: UIImage?
|
||||
private let textFieldTag = NSObject()
|
||||
|
||||
private var botResolutionState: BotResolutionState?
|
||||
private var botResolutionDisposable: Disposable?
|
||||
|
||||
private var hasAccessToAllChatsByDefault: Bool = true
|
||||
private var additionalPeerList = AdditionalPeerList(
|
||||
categories: Set(),
|
||||
peers: []
|
||||
)
|
||||
|
||||
private var replyToMessages: Bool = true
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
@ -150,6 +207,184 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBotQuery(query: String) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if !query.isEmpty {
|
||||
if self.botResolutionState?.query != query {
|
||||
let previousState = self.botResolutionState?.state
|
||||
self.botResolutionState = BotResolutionState(
|
||||
query: query,
|
||||
state: self.botResolutionState?.state ?? .searching
|
||||
)
|
||||
self.botResolutionDisposable?.dispose()
|
||||
|
||||
if previousState != self.botResolutionState?.state {
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
}
|
||||
|
||||
self.botResolutionDisposable = (component.context.engine.peers.resolvePeerByName(name: query)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case .progress:
|
||||
break
|
||||
case let .result(peer):
|
||||
let previousState = self.botResolutionState?.state
|
||||
if let peer {
|
||||
self.botResolutionState?.state = .found(peer: peer, isInstalled: false)
|
||||
} else {
|
||||
self.botResolutionState?.state = .notFound
|
||||
}
|
||||
if previousState != self.botResolutionState?.state {
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if let botResolutionDisposable = self.botResolutionDisposable {
|
||||
self.botResolutionDisposable = nil
|
||||
botResolutionDisposable.dispose()
|
||||
}
|
||||
if self.botResolutionState != nil {
|
||||
self.botResolutionState = nil
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openAdditionalPeerListSetup() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
enum AdditionalCategoryId: Int {
|
||||
case existingChats
|
||||
case newChats
|
||||
case contacts
|
||||
case nonContacts
|
||||
}
|
||||
|
||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .purple),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple),
|
||||
title: self.hasAccessToAllChatsByDefault ? "Existing Chats" : "New Chats"
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.contacts.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .blue),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
|
||||
title: "Contacts"
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.nonContacts.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
|
||||
title: "Non-Contacts"
|
||||
)
|
||||
]
|
||||
var selectedCategories = Set<Int>()
|
||||
for category in self.additionalPeerList.categories {
|
||||
switch category {
|
||||
case .existingChats:
|
||||
selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue)
|
||||
case .newChats:
|
||||
selectedCategories.insert(AdditionalCategoryId.newChats.rawValue)
|
||||
case .contacts:
|
||||
selectedCategories.insert(AdditionalCategoryId.contacts.rawValue)
|
||||
case .nonContacts:
|
||||
selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
|
||||
title: self.hasAccessToAllChatsByDefault ? "Exclude Chats" : "Include Chats",
|
||||
searchPlaceholder: "Search chats",
|
||||
selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)),
|
||||
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
|
||||
chatListFilters: nil,
|
||||
onlyUsers: true
|
||||
)), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in
|
||||
}))
|
||||
controller.navigationPresentation = .modal
|
||||
|
||||
let _ = (controller.result
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in
|
||||
guard let self, let component = self.component, case let .result(rawPeerIds, additionalCategoryIds) = result else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in
|
||||
switch id {
|
||||
case let .peer(id):
|
||||
return id
|
||||
case .deviceContact:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let _ = (component.context.engine.data.get(
|
||||
EngineDataMap(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
|
||||
),
|
||||
EngineDataMap(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:))
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peerMap, isContactMap in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in
|
||||
switch item {
|
||||
case AdditionalCategoryId.existingChats.rawValue:
|
||||
return .existingChats
|
||||
case AdditionalCategoryId.newChats.rawValue:
|
||||
return .newChats
|
||||
case AdditionalCategoryId.contacts.rawValue:
|
||||
return .contacts
|
||||
case AdditionalCategoryId.nonContacts.rawValue:
|
||||
return .nonContacts
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
self.additionalPeerList.categories = Set(mappedCategories)
|
||||
|
||||
self.additionalPeerList.peers.removeAll()
|
||||
for id in peerIds {
|
||||
guard let maybePeer = peerMap[id], let peer = maybePeer else {
|
||||
continue
|
||||
}
|
||||
self.additionalPeerList.peers.append(AdditionalPeerList.Peer(
|
||||
peer: peer,
|
||||
isContact: isContactMap[id] ?? false
|
||||
))
|
||||
}
|
||||
self.additionalPeerList.peers.sort(by: { lhs, rhs in
|
||||
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
|
||||
})
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
controller?.dismiss()
|
||||
})
|
||||
})
|
||||
|
||||
self.environment?.controller()?.push(controller)
|
||||
}
|
||||
|
||||
func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -221,7 +456,7 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
contentHeight += 129.0
|
||||
|
||||
//TODO:localize
|
||||
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More>]()", attributes: MarkdownAttributes(
|
||||
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More]()", attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
|
||||
@ -239,7 +474,7 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
//TODO:localize
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(subtitleString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
@ -273,6 +508,66 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
contentHeight += subtitleSize.height
|
||||
contentHeight += 27.0
|
||||
|
||||
var nameSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
nameSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
|
||||
theme: environment.theme,
|
||||
initialText: "",
|
||||
placeholder: "Bot Username",
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
updated: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateBotQuery(query: value)
|
||||
},
|
||||
tag: self.textFieldTag
|
||||
))))
|
||||
if let botResolutionState = self.botResolutionState {
|
||||
let mappedContent: ChatbotSearchResultItemComponent.Content
|
||||
switch botResolutionState.state {
|
||||
case .searching:
|
||||
mappedContent = .searching
|
||||
case .notFound:
|
||||
mappedContent = .notFound
|
||||
case let .found(peer, isInstalled):
|
||||
mappedContent = .found(peer: peer, isInstalled: isInstalled)
|
||||
}
|
||||
nameSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ChatbotSearchResultItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
content: mappedContent,
|
||||
installAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if var botResolutionState = self.botResolutionState, case let .found(peer, isInstalled) = botResolutionState.state, !isInstalled {
|
||||
botResolutionState.state = .found(peer: peer, isInstalled: true)
|
||||
self.botResolutionState = botResolutionState
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
},
|
||||
removeAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let botResolutionState = self.botResolutionState, case let .found(_, isInstalled) = botResolutionState.state, isInstalled {
|
||||
self.botResolutionState = nil
|
||||
if let botResolutionDisposable = self.botResolutionDisposable {
|
||||
self.botResolutionDisposable = nil
|
||||
botResolutionDisposable.dispose()
|
||||
}
|
||||
|
||||
if let textFieldView = self.nameSection.findTaggedView(tag: self.textFieldTag) as? ListTextFieldItemComponent.View {
|
||||
textFieldView.setText(text: "", updateState: false)
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
}
|
||||
))))
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let nameSectionSize = self.nameSection.update(
|
||||
transition: transition,
|
||||
@ -287,15 +582,7 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListTextFieldItemComponent(
|
||||
theme: environment.theme,
|
||||
initialText: "",
|
||||
placeholder: "Bot Username",
|
||||
updated: { value in
|
||||
}
|
||||
)))
|
||||
]
|
||||
items: nameSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
@ -339,11 +626,20 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: environment.theme.list.itemAccentColor,
|
||||
tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
accessory: nil,
|
||||
action: { _ in
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.hasAccessToAllChatsByDefault {
|
||||
self.hasAccessToAllChatsByDefault = true
|
||||
self.additionalPeerList.categories.removeAll()
|
||||
self.additionalPeerList.peers.removeAll()
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent(
|
||||
@ -360,11 +656,20 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: .clear,
|
||||
tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
accessory: nil,
|
||||
action: { _ in
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.hasAccessToAllChatsByDefault {
|
||||
self.hasAccessToAllChatsByDefault = false
|
||||
self.additionalPeerList.categories.removeAll()
|
||||
self.additionalPeerList.peers.removeAll()
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
)))
|
||||
]
|
||||
@ -382,6 +687,95 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
contentHeight += accessSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
|
||||
var excludedSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: self.hasAccessToAllChatsByDefault ? "Exclude Chats..." : "Select Chats...",
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemAccentColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/AddIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openAdditionalPeerListSetup()
|
||||
}
|
||||
))))
|
||||
for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) {
|
||||
let title: String
|
||||
let icon: String
|
||||
let color: AvatarBackgroundColor
|
||||
//TODO:localize
|
||||
switch category {
|
||||
case .newChats:
|
||||
title = "New Chats"
|
||||
icon = "Chat List/Filters/Contact"
|
||||
color = .purple
|
||||
case .existingChats:
|
||||
title = "Existing Chats"
|
||||
icon = "Chat List/Filters/Contact"
|
||||
color = .purple
|
||||
case .contacts:
|
||||
title = "Contacts"
|
||||
icon = "Chat List/Filters/Contact"
|
||||
color = .blue
|
||||
case .nonContacts:
|
||||
title = "Non-Contacts"
|
||||
icon = "Chat List/Filters/Contact"
|
||||
color = .yellow
|
||||
}
|
||||
excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
style: .generic,
|
||||
sideInset: 0.0,
|
||||
title: title,
|
||||
avatar: PeerListItemComponent.Avatar(
|
||||
icon: icon,
|
||||
color: color,
|
||||
clipStyle: .roundedRect
|
||||
),
|
||||
peer: nil,
|
||||
subtitle: nil,
|
||||
subtitleAccessory: .none,
|
||||
presence: nil,
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { peer, _, _ in
|
||||
}
|
||||
))))
|
||||
}
|
||||
for peer in self.additionalPeerList.peers {
|
||||
excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
style: .generic,
|
||||
sideInset: 0.0,
|
||||
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||
peer: peer.peer,
|
||||
subtitle: peer.isContact ? "contact" : "non-contact",
|
||||
subtitleAccessory: .none,
|
||||
presence: nil,
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { peer, _, _ in
|
||||
}
|
||||
))))
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let excludedSectionSize = self.excludedSection.update(
|
||||
transition: transition,
|
||||
@ -389,42 +783,27 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "EXCLUDED CHATS",
|
||||
string: self.hasAccessToAllChatsByDefault ? "EXCLUDED CHATS" : "INCLUDED CHATS",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Select chats or entire chat categories which the bot WILL NOT have access to.",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
text: .markdown(
|
||||
text: self.hasAccessToAllChatsByDefault ? "Select chats or entire chat categories which the bot **WILL NOT** have access to." : "Select chats or entire chat categories which the bot **WILL** have access to.",
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor),
|
||||
linkAttribute: { _ in
|
||||
return nil
|
||||
}
|
||||
)
|
||||
),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Exclude Chats...",
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemAccentColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/AddIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
accessory: nil,
|
||||
action: { _ in
|
||||
}
|
||||
))),
|
||||
]
|
||||
items: excludedSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
@ -473,7 +852,13 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
accessory: .toggle(true),
|
||||
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.replyToMessages = !self.replyToMessages
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
})),
|
||||
action: nil
|
||||
))),
|
||||
]
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -1264,14 +1264,14 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)),
|
||||
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
context: component.context,
|
||||
color: profileColor.flatMap { profileColor in
|
||||
component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main
|
||||
} ?? environment.theme.list.itemAccentColor,
|
||||
fileId: backgroundFileId,
|
||||
file: backgroundFileId.flatMap { self.cachedIconFiles[$0] }
|
||||
))),
|
||||
)))),
|
||||
action: { [weak self] view in
|
||||
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
|
||||
return
|
||||
@ -1393,12 +1393,12 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(HStack(emojiPackContents, spacing: 6.0)),
|
||||
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
context: component.context,
|
||||
color: environment.theme.list.itemAccentColor,
|
||||
fileId: emojiPack?.thumbnailFileId,
|
||||
file: emojiPackFile
|
||||
))),
|
||||
)))),
|
||||
action: { [weak self] view in
|
||||
guard let self, let resolvedState = self.resolveState() else {
|
||||
return
|
||||
@ -1457,12 +1457,12 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(HStack(emojiStatusContents, spacing: 6.0)),
|
||||
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
context: component.context,
|
||||
color: environment.theme.list.itemAccentColor,
|
||||
fileId: statusFileId,
|
||||
file: statusFileId.flatMap { self.cachedIconFiles[$0] }
|
||||
))),
|
||||
)))),
|
||||
action: { [weak self] view in
|
||||
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
|
||||
return
|
||||
@ -1581,12 +1581,12 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)),
|
||||
icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
context: component.context,
|
||||
color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main,
|
||||
fileId: replyFileId,
|
||||
file: replyFileId.flatMap { self.cachedIconFiles[$0] }
|
||||
))),
|
||||
)))),
|
||||
action: { [weak self] view in
|
||||
guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
|
||||
return
|
||||
|
23
submodules/TelegramUI/Components/SliderComponent/BUILD
Normal file
23
submodules/TelegramUI/Components/SliderComponent/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ swift_library(
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -19,6 +19,7 @@ import ContextUI
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import PhotoResources
|
||||
import ListSectionComponent
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
@ -64,6 +65,18 @@ public final class PeerListItemComponent: Component {
|
||||
case check
|
||||
}
|
||||
|
||||
public struct Avatar: Equatable {
|
||||
public var icon: String
|
||||
public var color: AvatarBackgroundColor
|
||||
public var clipStyle: AvatarNodeClipStyle
|
||||
|
||||
public init(icon: String, color: AvatarBackgroundColor, clipStyle: AvatarNodeClipStyle) {
|
||||
self.icon = icon
|
||||
self.color = color
|
||||
self.clipStyle = clipStyle
|
||||
}
|
||||
}
|
||||
|
||||
public final class Reaction: Equatable {
|
||||
public let reaction: MessageReaction.Reaction
|
||||
public let file: TelegramMediaFile?
|
||||
@ -103,6 +116,7 @@ public final class PeerListItemComponent: Component {
|
||||
let style: Style
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let avatar: Avatar?
|
||||
let peer: EnginePeer?
|
||||
let storyStats: PeerStoryStats?
|
||||
let subtitle: String?
|
||||
@ -127,6 +141,7 @@ public final class PeerListItemComponent: Component {
|
||||
style: Style,
|
||||
sideInset: CGFloat,
|
||||
title: String,
|
||||
avatar: Avatar? = nil,
|
||||
peer: EnginePeer?,
|
||||
storyStats: PeerStoryStats? = nil,
|
||||
subtitle: String?,
|
||||
@ -150,6 +165,7 @@ public final class PeerListItemComponent: Component {
|
||||
self.style = style
|
||||
self.sideInset = sideInset
|
||||
self.title = title
|
||||
self.avatar = avatar
|
||||
self.peer = peer
|
||||
self.storyStats = storyStats
|
||||
self.subtitle = subtitle
|
||||
@ -187,6 +203,9 @@ public final class PeerListItemComponent: Component {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.avatar != rhs.avatar {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
@ -229,7 +248,7 @@ public final class PeerListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: ContextControllerSourceView {
|
||||
public final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
@ -237,6 +256,7 @@ public final class PeerListItemComponent: Component {
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let avatarNode: AvatarNode
|
||||
private var avatarImageView: UIImageView?
|
||||
private let avatarButtonView: HighlightTrackingButton
|
||||
private var avatarIcon: ComponentView<Empty>?
|
||||
|
||||
@ -278,6 +298,9 @@ public final class PeerListItemComponent: Component {
|
||||
|
||||
private var isExtractedToContextMenu: Bool = false
|
||||
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public private(set) var separatorInset: CGFloat = 0.0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
@ -336,6 +359,15 @@ public final class PeerListItemComponent: Component {
|
||||
}
|
||||
component.contextAction?(peer, self.extractedContainerView, gesture)
|
||||
}
|
||||
|
||||
self.containerButton.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
|
||||
customUpdateIsHighlighted(highlighted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -560,6 +592,27 @@ public final class PeerListItemComponent: Component {
|
||||
transition.setFrame(view: self.avatarButtonView, frame: avatarFrame)
|
||||
|
||||
var statusIcon: EmojiStatusComponent.Content?
|
||||
|
||||
if let avatar = component.avatar {
|
||||
let avatarImageView: UIImageView
|
||||
if let current = self.avatarImageView {
|
||||
avatarImageView = current
|
||||
} else {
|
||||
avatarImageView = UIImageView()
|
||||
self.avatarImageView = avatarImageView
|
||||
self.containerButton.addSubview(avatarImageView)
|
||||
}
|
||||
if previousComponent?.avatar != avatar {
|
||||
avatarImageView.image = generateAvatarImage(size: avatarFrame.size, icon: generateTintedImage(image: UIImage(bundleImageName: avatar.icon), color: .white), cornerRadius: 12.0, color: avatar.color)
|
||||
}
|
||||
transition.setFrame(view: avatarImageView, frame: avatarFrame)
|
||||
} else {
|
||||
if let avatarImageView = self.avatarImageView {
|
||||
self.avatarImageView = nil
|
||||
avatarImageView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if let peer = component.peer {
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
@ -596,6 +649,7 @@ public final class PeerListItemComponent: Component {
|
||||
lineWidth: 1.33,
|
||||
inactiveLineWidth: 1.33
|
||||
), transition: transition)
|
||||
self.avatarNode.isHidden = false
|
||||
|
||||
if peer.isScam {
|
||||
statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased())
|
||||
@ -608,6 +662,8 @@ public final class PeerListItemComponent: Component {
|
||||
} else if peer.isPremium {
|
||||
statusIcon = .premium(color: component.theme.list.itemAccentColor)
|
||||
}
|
||||
} else {
|
||||
self.avatarNode.isHidden = true
|
||||
}
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
@ -953,6 +1009,8 @@ public final class PeerListItemComponent: Component {
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
self.separatorInset = leftInset
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
private let ellipsisView = ComponentView<Empty>()
|
||||
|
||||
private var inputState: InputState {
|
||||
public var inputState: InputState {
|
||||
let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
|
||||
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
|
||||
}
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "business_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
196
submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/business_30.pdf
vendored
Normal file
196
submodules/TelegramUI/Images.xcassets/Settings/Menu/Business.imageset/business_30.pdf
vendored
Normal 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
|
BIN
submodules/TelegramUI/Resources/Animations/HandWaveEmoji.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/HandWaveEmoji.tgs
Normal file
Binary file not shown.
@ -127,8 +127,23 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
let additionalCategories = chatSelection.additionalCategories
|
||||
let chatListFilters = chatSelection.chatListFilters
|
||||
|
||||
var chatListFilter: ChatListFilter?
|
||||
if chatSelection.onlyUsers {
|
||||
chatListFilter = .filter(id: Int32.max, title: "", emoticon: nil, data: ChatListFilterData(
|
||||
isShared: false,
|
||||
hasSharedLinks: false,
|
||||
categories: [.contacts, .nonContacts],
|
||||
excludeMuted: false,
|
||||
excludeRead: false,
|
||||
excludeArchived: false,
|
||||
includePeers: ChatListFilterIncludePeers(),
|
||||
excludePeers: [],
|
||||
color: nil
|
||||
))
|
||||
}
|
||||
|
||||
placeholder = placeholderValue
|
||||
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
|
||||
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
|
||||
chatListNode.passthroughPeerSelection = true
|
||||
chatListNode.disabledPeerSelected = { peer, _, reason in
|
||||
attemptDisabledItemSelection?(peer, reason)
|
||||
|
@ -53,6 +53,9 @@ import UndoUI
|
||||
import ChatMessageNotificationItem
|
||||
import BusinessSetupScreen
|
||||
import ChatbotSetupScreen
|
||||
import BusinessLocationSetupScreen
|
||||
import BusinessHoursSetupScreen
|
||||
import GreetingMessageSetupScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -1888,6 +1891,18 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return ChatbotSetupScreen(context: context)
|
||||
}
|
||||
|
||||
public func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController {
|
||||
return BusinessLocationSetupScreen(context: context)
|
||||
}
|
||||
|
||||
public func makeBusinessHoursSetupScreen(context: AccountContext) -> ViewController {
|
||||
return BusinessHoursSetupScreen(context: context)
|
||||
}
|
||||
|
||||
public func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController {
|
||||
return GreetingMessageSetupScreen(context: context)
|
||||
}
|
||||
|
||||
public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController {
|
||||
var modal = true
|
||||
let mappedSource: PremiumSource
|
||||
|
Loading…
x
Reference in New Issue
Block a user