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