[WIP] Business

This commit is contained in:
Isaac 2024-02-27 00:52:17 +04:00
parent 0ba75f81de
commit 729a260626
18 changed files with 517 additions and 217 deletions

View File

@ -850,6 +850,9 @@ public protocol TelegramRootControllerInterface: NavigationController {
public protocol QuickReplySetupScreenInitialData: AnyObject {
}
public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject {
}
public protocol SharedAccountContext: AnyObject {
var sharedContainerPath: String { get }
var basePath: String { get }
@ -940,7 +943,8 @@ public protocol SharedAccountContext: AnyObject {
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController
func makeBusinessHoursSetupScreen(context: AccountContext, initialValue: TelegramBusinessHours?, completion: @escaping (TelegramBusinessHours?) -> Void) -> ViewController
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController
func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController
func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError>
func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController
func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError>
func navigateToChatController(_ params: NavigateToChatControllerParams)

View File

@ -19,6 +19,7 @@ public enum AttachmentButtonType: Equatable {
case gallery
case file
case location
case quickReply
case contact
case poll
case app(AttachMenuBot)
@ -27,54 +28,60 @@ public enum AttachmentButtonType: Equatable {
public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool {
switch lhs {
case .gallery:
if case .gallery = rhs {
return true
} else {
return false
}
case .file:
if case .file = rhs {
return true
} else {
return false
}
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .contact:
if case .contact = rhs {
return true
} else {
return false
}
case .poll:
if case .poll = rhs {
return true
} else {
return false
}
case let .app(lhsBot):
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
return true
} else {
return false
}
case .gift:
if case .gift = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true
} else {
return false
}
case .gallery:
if case .gallery = rhs {
return true
} else {
return false
}
case .file:
if case .file = rhs {
return true
} else {
return false
}
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .quickReply:
if case .quickReply = rhs {
return true
} else {
return false
}
case .contact:
if case .contact = rhs {
return true
} else {
return false
}
case .poll:
if case .poll = rhs {
return true
} else {
return false
}
case let .app(lhsBot):
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
return true
} else {
return false
}
case .gift:
if case .gift = rhs {
return true
} else {
return false
}
case .standalone:
if case .standalone = rhs {
return true
} else {
return false
}
}
}
}

View File

@ -217,6 +217,10 @@ private final class AttachButtonComponent: CombinedComponent {
name = ""
imageName = ""
imageFile = nil
case .quickReply:
//TODO:localize
name = "Reply"
imageName = "Chat/Attach Menu/Location"
}
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
@ -1183,6 +1187,9 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
accessibilityTitle = bot.shortName
case .standalone:
accessibilityTitle = ""
case .quickReply:
//TODO:localize
accessibilityTitle = "Reply"
}
buttonView.isAccessibilityElement = true
buttonView.accessibilityLabel = accessibilityTitle

View File

@ -1156,6 +1156,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarIconComponent: EmojiStatusComponent?
var avatarVideoNode: AvatarVideoNode?
var avatarTapRecognizer: UITapGestureRecognizer?
var avatarMediaNode: AvatarVideoNode?
private var inlineNavigationMarkLayer: SimpleLayer?

View File

@ -2132,9 +2132,23 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: false))
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
})
case .awayMessages:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: true))
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
})
case .chatbots:
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
}

View File

@ -876,7 +876,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = messages[0].0.threadId, !"".isEmpty {
if let threadId = messages[0].0.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1008,7 +1008,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = messages[0].0.threadId, !"".isEmpty {
if let threadId = messages[0].0.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1319,7 +1319,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1394,7 +1394,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1413,7 +1413,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)
@ -1487,7 +1487,7 @@ public final class PendingMessageManager {
var quickReplyShortcut: Api.InputQuickReplyShortcut?
if let quickReply {
if let threadId = message.threadId, !"".isEmpty {
if let threadId = message.threadId {
quickReplyShortcut = .inputQuickReplyShortcutId(shortcutId: Int32(clamping: threadId))
} else {
quickReplyShortcut = .inputQuickReplyShortcut(shortcut: quickReply.shortcut)

View File

@ -755,7 +755,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
var displayTranscribe = false
if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) {
displayTranscribe = false
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
if arguments.associatedData.isPremium {
displayTranscribe = true

View File

@ -46,6 +46,7 @@ swift_library(
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/AttachmentUI",
],
visibility = [
"//visibility:public",

View File

@ -46,13 +46,16 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: AutomaticBusinessMessageSetupScreen.InitialData
let mode: AutomaticBusinessMessageSetupScreen.Mode
init(
context: AccountContext,
initialData: AutomaticBusinessMessageSetupScreen.InitialData,
mode: AutomaticBusinessMessageSetupScreen.Mode
) {
self.context = context
self.initialData = initialData
self.mode = mode
}
@ -130,7 +133,8 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
private var isOn: Bool = false
private var accountPeer: EnginePeer?
private var messages: [EngineMessage] = []
private var currentShortcut: ShortcutMessageList.Item?
private var currentShortcutDisposable: Disposable?
private var schedule: Schedule = .always
private var customScheduleStart: Date?
@ -144,8 +148,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
private var replyToMessages: Bool = true
private var messagesDisposable: Disposable?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -172,7 +174,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
deinit {
self.messagesDisposable?.dispose()
self.currentShortcutDisposable?.dispose()
}
func scrollToTop() {
@ -350,10 +352,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
guard let component = self.component else {
return
}
let shortcutName: String
switch component.mode {
case .greeting:
shortcutName = "hello"
case .away:
shortcutName = "away"
}
let contents = AutomaticBusinessMessageSetupChatContents(
context: component.context,
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput,
shortcutId: nil
kind: .quickReplyMessageInput(shortcut: shortcutName),
shortcutId: self.currentShortcut?.id
)
let chatController = component.context.sharedContext.makeChatController(
context: component.context,
@ -364,7 +375,6 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
)
chatController.navigationPresentation = .modal
self.environment?.controller()?.push(chatController)
self.messagesDisposable?.dispose()
}
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
@ -459,14 +469,27 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
if self.component == nil {
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
self.accountPeer = component.initialData.accountPeer
let shortcutName: String
switch component.mode {
case .greeting:
shortcutName = "hello"
case .away:
shortcutName = "away"
}
self.currentShortcut = component.initialData.shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
self.currentShortcutDisposable = (component.context.engine.accountData.shortcutMessageList()
|> deliverOnMainQueue).start(next: { [weak self] shortcutMessageList in
guard let self else {
return
}
self.accountPeer = peer
let shortcut = shortcutMessageList.items.first(where: { $0.shortcut == shortcutName })
if shortcut != self.currentShortcut {
self.currentShortcut = shortcut
self.state?.updated(transition: .immediate)
}
})
}
@ -632,15 +655,15 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
//TODO:localize
var messagesSectionItems: [AnyComponentWithIdentity<Empty>] = []
if let topMessage = self.messages.first {
if let currentShortcut = self.currentShortcut {
if let accountPeer = self.accountPeer {
messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
accountPeer: accountPeer,
message: topMessage,
count: self.messages.count,
message: currentShortcut.topMessage,
count: currentShortcut.totalCount,
action: { [weak self] in
guard let self else {
return
@ -681,7 +704,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? (self.messages.count > 1 ? "GREETING MESSAGES" : "GREETING MESSAGE") : (self.messages.count > 1 ? "AWAY MESSAGES" : "AWAY MESSAGE"),
string: component.mode == .greeting ? "GREETING MESSAGE" : "AWAY MESSAGE",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
@ -1244,6 +1267,19 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
}
public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentContainer {
public final class InitialData: AutomaticBusinessMessageSetupScreenInitialData {
let accountPeer: EnginePeer?
let shortcutMessageList: ShortcutMessageList
init(
accountPeer: EnginePeer?,
shortcutMessageList: ShortcutMessageList
) {
self.accountPeer = accountPeer
self.shortcutMessageList = shortcutMessageList
}
}
public enum Mode {
case greeting
case away
@ -1251,11 +1287,12 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
private let context: AccountContext
public init(context: AccountContext, mode: Mode) {
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
self.context = context
super.init(context: context, component: AutomaticBusinessMessageSetupScreenComponent(
context: context,
initialData: initialData,
mode: mode
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
@ -1293,4 +1330,20 @@ public final class AutomaticBusinessMessageSetupScreen: ViewControllerComponentC
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
public static func initialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> {
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
),
context.engine.accountData.shortcutMessageList()
|> take(1)
)
|> map { accountPeer, shortcutMessageList -> AutomaticBusinessMessageSetupScreenInitialData in
return InitialData(
accountPeer: accountPeer,
shortcutMessageList: shortcutMessageList
)
}
}
}

View File

@ -21,19 +21,23 @@ import QuickReplyNameAlertController
import ChatListHeaderComponent
import PlainButtonComponent
import MultilineTextComponent
import AttachmentUI
final class QuickReplySetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: QuickReplySetupScreen.InitialData
let mode: QuickReplySetupScreen.Mode
init(
context: AccountContext,
initialData: QuickReplySetupScreen.InitialData
initialData: QuickReplySetupScreen.InitialData,
mode: QuickReplySetupScreen.Mode
) {
self.context = context
self.initialData = initialData
self.mode = mode
}
static func ==(lhs: QuickReplySetupScreenComponent, rhs: QuickReplySetupScreenComponent) -> Bool {
@ -516,6 +520,13 @@ final class QuickReplySetupScreenComponent: Component {
return
}
if case let .select(completion) = component.mode {
if let shortcutId {
completion(shortcutId)
}
return
}
if let shortcut {
let contents = AutomaticBusinessMessageSetupChatContents(
context: component.context,
@ -635,7 +646,7 @@ final class QuickReplySetupScreenComponent: Component {
var items: [ActionSheetItem] = []
//TODO:localize
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Shortcut" : "Delete Shortcuts", color: .destructive, action: { [weak self, weak actionSheet] in
items.append(ActionSheetButtonItem(title: ids.count == 1 ? "Delete Quick Reply" : "Delete Quick Replies", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
@ -667,6 +678,7 @@ final class QuickReplySetupScreenComponent: Component {
size: CGSize,
insets: UIEdgeInsets,
statusBarHeight: CGFloat,
isModal: Bool,
transition: Transition,
deferScrollApplication: Bool
) -> CGFloat {
@ -706,14 +718,31 @@ final class QuickReplySetupScreenComponent: Component {
titleText = "Quick Replies"
}
let closeTitle: String
switch component.mode {
case .manage:
closeTitle = strings.Common_Close
case .select:
closeTitle = strings.Common_Cancel
}
let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content(
title: titleText,
navigationBackTitle: nil,
titleComponent: nil,
chatListTitle: nil,
leftButton: nil,
leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent(
content: .text(title: closeTitle, isBold: false),
pressed: { [weak self] _ in
guard let self else {
return
}
if self.attemptNavigation(complete: {}) {
self.environment?.controller()?.dismiss()
}
}
))) : nil,
rightButtons: rightButtons,
backTitle: "Back",
backTitle: isModal ? nil : "Back",
backPressed: { [weak self] in
guard let self else {
return
@ -896,6 +925,19 @@ final class QuickReplySetupScreenComponent: Component {
}
}
var isModal = false
if let controller = environment.controller(), controller.navigationPresentation == .modal {
isModal = true
}
if case .select = component.mode {
isModal = true
}
var statusBarHeight = environment.statusBarHeight
if isModal {
statusBarHeight = max(statusBarHeight, 1.0)
}
var listBottomInset = environment.safeInsets.bottom
let navigationHeight = self.updateNavigationBar(
component: component,
@ -903,7 +945,8 @@ final class QuickReplySetupScreenComponent: Component {
strings: environment.strings,
size: availableSize,
insets: environment.safeInsets,
statusBarHeight: environment.statusBarHeight,
statusBarHeight: statusBarHeight,
isModal: isModal,
transition: transition,
deferScrollApplication: true
)
@ -1013,7 +1056,12 @@ final class QuickReplySetupScreenComponent: Component {
var entries: [ContentEntry] = []
if let shortcutMessageList = self.shortcutMessageList, let accountPeer = self.accountPeer {
entries.append(.add)
switch component.mode {
case .manage:
entries.append(.add)
case .select:
break
}
for item in shortcutMessageList.items {
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count, isEditing: self.isEditing, isSelected: self.selectedIds.contains(item.id)))
}
@ -1046,7 +1094,7 @@ final class QuickReplySetupScreenComponent: Component {
}
}
public final class QuickReplySetupScreen: ViewControllerComponentContainer {
public final class QuickReplySetupScreen: ViewControllerComponentContainer, AttachmentContainable {
public final class InitialData: QuickReplySetupScreenInitialData {
let accountPeer: EnginePeer?
let shortcutMessageList: ShortcutMessageList
@ -1060,14 +1108,36 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
}
}
public enum Mode {
case manage
case select(completion: (Int32) -> Void)
}
private let context: AccountContext
public init(context: AccountContext, initialData: InitialData) {
public var requestAttachmentMenuExpansion: () -> Void = {
}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in
}
public var cancelPanGesture: () -> Void = {
}
public var isContainerPanning: () -> Bool = {
return false
}
public var isContainerExpanded: () -> Bool = {
return false
}
public var mediaPickerContext: AttachmentMediaPickerContext?
public init(context: AccountContext, initialData: InitialData, mode: Mode) {
self.context = context
super.init(context: context, component: QuickReplySetupScreenComponent(
context: context,
initialData: initialData
initialData: initialData,
mode: mode
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
self.scrollToTop = { [weak self] in
@ -1116,4 +1186,21 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer {
)
}
}
public func isContainerPanningUpdated(_ panning: Bool) {
}
public func resetForReuse() {
}
public func prepareForReuse() {
}
public func requestDismiss(completion: @escaping () -> Void) {
completion()
}
public func shouldDismissImmediately() -> Bool {
return true
}
}

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/LocationUI",
"//submodules/AppBundle",
"//submodules/Geocoding",
],
visibility = [
"//visibility:public",

View File

@ -20,6 +20,8 @@ import BundleIconComponent
import LottieComponent
import Markdown
import LocationUI
import CoreLocation
import Geocoding
final class BusinessLocationSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -74,7 +76,11 @@ final class BusinessLocationSetupScreenComponent: Component {
private let textFieldTag = NSObject()
private var resetAddressText: String?
private var isLoadingGeocodedAddress: Bool = false
private var geocodeAddressState: (address: String, disposable: Disposable)?
private var mapCoordinates: TelegramBusinessLocation.Coordinates?
private var mapCoordinatesManuallySet: Bool = false
override init(frame: CGRect) {
self.scrollView = ScrollView()
@ -102,6 +108,7 @@ final class BusinessLocationSetupScreenComponent: Component {
}
deinit {
self.geocodeAddressState?.disposable.dispose()
}
func scrollToTop() {
@ -183,12 +190,18 @@ final class BusinessLocationSetupScreenComponent: Component {
return
}
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, completion: { [weak self] location, _, _, address, _ in
var initialLocation: CLLocationCoordinate2D?
if let mapCoordinates = self.mapCoordinates {
initialLocation = CLLocationCoordinate2D(latitude: mapCoordinates.latitude, longitude: mapCoordinates.longitude)
}
let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in
guard let self else {
return
}
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.latitude, longitude: location.longitude)
self.mapCoordinatesManuallySet = true
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View, textView.currentText.isEmpty {
self.resetAddressText = address
}
@ -198,6 +211,43 @@ final class BusinessLocationSetupScreenComponent: Component {
self.environment?.controller()?.push(controller)
}
private func updateGeocodedAddress(string: String) {
let addressValue: String?
if self.mapCoordinates != nil && self.mapCoordinatesManuallySet {
addressValue = nil
} else if string.count < 3 {
addressValue = nil
} else {
addressValue = string
}
if let current = self.geocodeAddressState, current.address == addressValue {
} else {
self.geocodeAddressState?.disposable.dispose()
self.geocodeAddressState = nil
if let addressValue {
let disposable = MetaDisposable()
self.geocodeAddressState = (string, disposable)
disposable.set((
geocodeLocation(address: addressValue, locale: Locale.current)
|> delay(0.4, queue: .mainQueue())
|> deliverOnMainQueue
).start(next: { [weak self] result in
guard let self else {
return
}
if let location = result?.first?.location, !self.mapCoordinatesManuallySet {
self.mapCoordinates = TelegramBusinessLocation.Coordinates(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
self.state?.updated(transition: .immediate)
}
}))
}
}
}
func update(component: BusinessLocationSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
@ -207,6 +257,9 @@ final class BusinessLocationSetupScreenComponent: Component {
if self.component == nil {
if let initialValue = component.initialValue {
self.mapCoordinates = initialValue.coordinates
if self.mapCoordinates != nil {
self.mapCoordinatesManuallySet = true
}
self.resetAddressText = initialValue.address
}
}
@ -333,12 +386,7 @@ final class BusinessLocationSetupScreenComponent: Component {
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 64,
updated: { [weak self] value in
guard let self else {
return
}
let _ = self
let _ = value
updated: { _ in
},
tag: self.textFieldTag
))))
@ -390,6 +438,7 @@ final class BusinessLocationSetupScreenComponent: Component {
self.openLocationPicker()
} else {
self.mapCoordinates = nil
self.mapCoordinatesManuallySet = false
self.state?.updated(transition: .spring(duration: 0.4))
}
}

View File

@ -1125,7 +1125,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatFilterTag = value
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
var standalone = false
if case .customChatContents = strongSelf.chatLocation {
standalone = true
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
self?.chatDisplayNode.dismissInput()
}, present: { c, a in
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
@ -9519,7 +9524,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.push(self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData))
let controller = self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData)
controller.navigationPresentation = .modal
self.push(controller)
})
}, sendBotStart: { [weak self] payload in
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
@ -9542,9 +9549,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
strongSelf.dismissAllTooltips()
@ -9554,32 +9558,34 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
var bannedMediaInput = false
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel {
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
return
}
}
}
}

View File

@ -29,6 +29,7 @@ import ChatEntityKeyboardInputNode
import PremiumUI
import PremiumGiftAttachmentScreen
import TelegramCallsUI
import AutomaticBusinessMessageSetupScreen
extension ChatControllerImpl {
enum AttachMenuSubject {
@ -131,8 +132,11 @@ extension ChatControllerImpl {
let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError>
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
buttons = self.context.engine.messages.attachMenuBots()
|> map { attachMenuBots in
buttons = combineLatest(
self.context.engine.messages.attachMenuBots(),
self.context.engine.accountData.shortcutMessageList() |> take(1)
)
|> map { attachMenuBots, shortcutMessageList in
var buttons = availableButtons
var allButtons = availableButtons
var initialButton: AttachmentButtonType?
@ -166,6 +170,19 @@ extension ChatControllerImpl {
allButtons.insert(button, at: 1)
}
if let user = peer as? TelegramUser, user.botInfo == nil {
if let index = buttons.firstIndex(where: { $0 == .location }) {
buttons.insert(.quickReply, at: index + 1)
} else {
buttons.append(.quickReply)
}
if let index = allButtons.firstIndex(where: { $0 == .location }) {
allButtons.insert(.quickReply, at: index + 1)
} else {
allButtons.append(.quickReply)
}
}
return (buttons, allButtons, initialButton)
}
} else {
@ -602,6 +619,24 @@ extension ChatControllerImpl {
strongSelf.present(alertController, in: .window(.root))
}
}
case .quickReply:
let _ = (strongSelf.context.sharedContext.makeQuickReplySetupScreenInitialData(context: strongSelf.context)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak strongSelf] initialData in
guard let strongSelf else {
return
}
let controller = QuickReplySetupScreen(context: strongSelf.context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .select(completion: { [weak strongSelf] shortcutId in
guard let strongSelf else {
return
}
strongSelf.attachmentController?.dismiss(animated: true)
strongSelf.interfaceInteraction?.sendShortcut(shortcutId)
}))
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
})
default:
break
}

View File

@ -68,6 +68,8 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
} else {
hasEditRights = true
}
} else if message.id.namespace == Namespaces.Message.QuickReplyCloud {
hasEditRights = true
} else if message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.id.namespace != Namespaces.Message.Cloud {
hasEditRights = false
} else if let author = message.author, author.id == accountPeerId, let peer = message.peers[message.id.peerId] {
@ -601,68 +603,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return .single(ContextController.Items(content: .list(actions)))
}
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
var actions: [ContextMenuItem] = []
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
var messageEntities: [MessageTextEntity]?
var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
}
if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
}
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
f(.default)
})))
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak customChatContents] _, f in
f(.dismissWithoutContent)
guard let customChatContents else {
return
}
customChatContents.deleteMessages(ids: messages.map(\.id))
})))
}
return .single(ContextController.Items(content: .list(actions)))
}
var loadStickerSaveStatus: MediaId?
var loadCopyMediaResource: MediaResource?
var isAction = false
@ -1140,8 +1080,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
})))
}
var messageText: String = ""
for message in messages {
if !message.text.isEmpty {
@ -1164,6 +1102,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
}
for attribute in message.attributes {
if hasExpandedAudioTranscription, let attribute = attribute as? AudioTranscriptionMessageAttribute {
if !messageText.isEmpty {
messageText.append("\n")
}
messageText.append(attribute.text)
break
}
}
var isPoll = false
if messageText.isEmpty {
for media in message.media {
@ -1937,6 +1885,74 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
actions.removeFirst()
}
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
actions.removeAll()
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
if !messageText.isEmpty || (resourceAvailable && isImage) || diceEmoji != nil {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
var messageEntities: [MessageTextEntity]?
var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
}
if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
}
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
f(.default)
})))
}
if message.id.namespace == Namespaces.Message.QuickReplyCloud {
if data.canEdit {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
}
if !actions.isEmpty {
actions.append(.separator)
}
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak customChatContents] _, f in
f(.dismissWithoutContent)
guard let customChatContents else {
return
}
customChatContents.deleteMessages(ids: messages.map(\.id))
})))
}
}
}
return ContextController.Items(content: .list(actions), tip: nil)
}
}

View File

@ -405,8 +405,9 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
options.insert(.LowLatency)
options.insert(.PreferSynchronousResourceLoading)
if firstTime {
self.contentOffsetChangeTransition = .spring(duration: 0.4)
self.contentOffsetChangeTransition = .immediate
self.listBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.listView.bounds.height), size: CGSize(width: self.listView.bounds.width, height: self.listView.bounds.height + 1000.0))
} else {
@ -419,7 +420,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
}
var insets = UIEdgeInsets()
insets.top = topInsetForLayout(size: validLayout.0, hasShortcuts: transition.hasShortcuts)
insets.top = topInsetForLayout(size: validLayout.0)
insets.left = validLayout.1
insets.right = validLayout.2
@ -437,12 +438,8 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
if let topItemOffset = topItemOffset {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
let position = strongSelf.listView.layer.position
strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset))
transition.animateView {
strongSelf.listView.position = position
}
//transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
transition.animatePositionAdditive(layer: strongSelf.listView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
transition.animatePositionAdditive(layer: strongSelf.listBackgroundView.layer, offset: CGPoint(x: 0.0, y: strongSelf.listView.bounds.size.height - topItemOffset))
}
}
})
@ -451,10 +448,31 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
}
}
private func topInsetForLayout(size: CGSize, hasShortcuts: Bool) -> CGFloat {
var minimumItemHeights: CGFloat = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
if hasShortcuts {
minimumItemHeights += VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
private func topInsetForLayout(size: CGSize) -> CGFloat {
var minimumItemHeights: CGFloat = 0.0
if let currentEntries = self.currentEntries, !currentEntries.isEmpty {
let indexLimit = min(4, currentEntries.count - 1)
for i in 0 ... indexLimit {
var itemHeight: CGFloat
switch currentEntries[i].content {
case .editShortcuts:
itemHeight = VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight(style: .round)
case let .command(command):
switch command.command {
case .command:
itemHeight = MentionChatInputPanelItemNode.itemHeight
case .shortcut:
itemHeight = 58.0
}
}
if indexLimit >= 4 && i == indexLimit {
minimumItemHeights += floor(itemHeight * 0.5)
} else {
minimumItemHeights += itemHeight
}
}
} else {
minimumItemHeights = floor(MentionChatInputPanelItemNode.itemHeight * 3.5)
}
return max(size.height - minimumItemHeights, 0.0)
@ -465,16 +483,7 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode {
self.validLayout = (size, leftInset, rightInset, bottomInset)
var insets = UIEdgeInsets()
var hasShortcuts = false
if let currentEntries = self.currentEntries {
hasShortcuts = currentEntries.contains(where: { entry in
if case .editShortcuts = entry.content {
return true
}
return false
})
}
insets.top = self.topInsetForLayout(size: size, hasShortcuts: hasShortcuts)
insets.top = self.topInsetForLayout(size: size)
insets.left = leftInset
insets.right = rightInset

View File

@ -1902,12 +1902,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return BusinessHoursSetupScreen(context: context, initialValue: initialValue, completion: completion)
}
public func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController {
return AutomaticBusinessMessageSetupScreen(context: context, mode: isAwayMode ? .away : .greeting)
public func makeAutomaticBusinessMessageSetupScreen(context: AccountContext, initialData: AutomaticBusinessMessageSetupScreenInitialData, isAwayMode: Bool) -> ViewController {
return AutomaticBusinessMessageSetupScreen(context: context, initialData: initialData as! AutomaticBusinessMessageSetupScreen.InitialData, mode: isAwayMode ? .away : .greeting)
}
public func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError> {
return AutomaticBusinessMessageSetupScreen.initialData(context: context)
}
public func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController {
return QuickReplySetupScreen(context: context, initialData: initialData as! QuickReplySetupScreen.InitialData)
return QuickReplySetupScreen(context: context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .manage)
}
public func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError> {

View File

@ -158,19 +158,23 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
let titleOffsetY: CGFloat
switch item.style {
case .regular:
strongSelf.backgroundColor = item.theme.list.plainBackgroundColor
strongSelf.topSeparatorNode.isHidden = mergedTop
strongSelf.separatorNode.isHidden = !mergedBottom
titleOffsetY = 2.0
case .round:
strongSelf.backgroundColor = nil
strongSelf.topSeparatorNode.isHidden = true
strongSelf.separatorNode.isHidden = !mergedBottom
titleOffsetY = 1.0
}
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size)
strongSelf.topSeparatorNode.isHidden = mergedTop
strongSelf.separatorNode.isHidden = !mergedBottom
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + titleOffsetY), size: titleLayout.size)
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width, height: UIScreenPixel))