Bot start button improvements

This commit is contained in:
Ilya Laktyushin 2023-03-17 22:55:37 +04:00
parent de8aea109f
commit 25d7aaf7e3
18 changed files with 607 additions and 133 deletions

View File

@ -871,6 +871,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}, beginCall: { _ in }, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in }, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in }, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, getNavigationController: { }, getNavigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, presentGlobalOverlayController: { _, _ in

View File

@ -125,6 +125,7 @@ public final class ChatPanelInterfaceInteraction {
public let beginCall: (Bool) -> Void public let beginCall: (Bool) -> Void
public let toggleMessageStickerStarred: (MessageId) -> Void public let toggleMessageStickerStarred: (MessageId) -> Void
public let presentController: (ViewController, Any?) -> Void public let presentController: (ViewController, Any?) -> Void
public let presentControllerInCurrent: (ViewController, Any?) -> Void
public let getNavigationController: () -> NavigationController? public let getNavigationController: () -> NavigationController?
public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let presentGlobalOverlayController: (ViewController, Any?) -> Void
public let navigateFeed: () -> Void public let navigateFeed: () -> Void
@ -228,6 +229,7 @@ public final class ChatPanelInterfaceInteraction {
beginCall: @escaping (Bool) -> Void, beginCall: @escaping (Bool) -> Void,
toggleMessageStickerStarred: @escaping (MessageId) -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void,
presentController: @escaping (ViewController, Any?) -> Void, presentController: @escaping (ViewController, Any?) -> Void,
presentControllerInCurrent: @escaping (ViewController, Any?) -> Void,
getNavigationController: @escaping () -> NavigationController?, getNavigationController: @escaping () -> NavigationController?,
presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void,
navigateFeed: @escaping () -> Void, navigateFeed: @escaping () -> Void,
@ -330,6 +332,7 @@ public final class ChatPanelInterfaceInteraction {
self.beginCall = beginCall self.beginCall = beginCall
self.toggleMessageStickerStarred = toggleMessageStickerStarred self.toggleMessageStickerStarred = toggleMessageStickerStarred
self.presentController = presentController self.presentController = presentController
self.presentControllerInCurrent = presentControllerInCurrent
self.getNavigationController = getNavigationController self.getNavigationController = getNavigationController
self.presentGlobalOverlayController = presentGlobalOverlayController self.presentGlobalOverlayController = presentGlobalOverlayController
self.navigateFeed = navigateFeed self.navigateFeed = navigateFeed
@ -440,6 +443,7 @@ public final class ChatPanelInterfaceInteraction {
}, beginCall: { _ in }, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in }, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in }, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, getNavigationController: { }, getNavigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, presentGlobalOverlayController: { _, _ in

View File

@ -138,7 +138,6 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
private(set) var naturalContentFrame: CGRect? private(set) var naturalContentFrame: CGRect?
fileprivate let gesture: PinchSourceGesture fileprivate let gesture: PinchSourceGesture
fileprivate var panGesture: UIPanGestureRecognizer?
public var isPinchGestureEnabled: Bool = true { public var isPinchGestureEnabled: Bool = true {
didSet { didSet {
@ -209,9 +208,6 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
} }
} }
@objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false return false
} }

View File

@ -164,7 +164,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
private var fontSize: CGFloat private var fontSize: CGFloat
private let gloss: Bool private let gloss: Bool
private let buttonBackgroundNode: ASImageNode public let buttonBackgroundNode: ASImageNode
private var buttonBackgroundAnimationView: UIImageView? private var buttonBackgroundAnimationView: UIImageView?
private var shimmerView: ShimmerEffectForegroundView? private var shimmerView: ShimmerEffectForegroundView?
@ -173,7 +173,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
private var borderShimmerView: ShimmerEffectForegroundView? private var borderShimmerView: ShimmerEffectForegroundView?
private let buttonNode: HighlightTrackingButtonNode private let buttonNode: HighlightTrackingButtonNode
private let titleNode: ImmediateTextNode public let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
private var animationNode: SimpleAnimationNode? private var animationNode: SimpleAnimationNode?
@ -181,7 +181,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
private var badgeNode: BadgeNode? private var badgeNode: BadgeNode?
private let buttonHeight: CGFloat private let buttonHeight: CGFloat
private let buttonCornerRadius: CGFloat public let buttonCornerRadius: CGFloat
public var pressed: (() -> Void)? public var pressed: (() -> Void)?
public var validLayout: CGFloat? public var validLayout: CGFloat?
@ -309,6 +309,23 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
public var progressType: SolidRoundedButtonProgressType = .fullSize public var progressType: SolidRoundedButtonProgressType = .fullSize
public var highlightEnabled = true {
didSet {
if !self.highlightEnabled {
self.buttonBackgroundNode.alpha = 1.0
self.titleNode.alpha = 1.0
self.subtitleNode.alpha = 1.0
self.iconNode.alpha = 1.0
self.animationNode?.alpha = 1.0
self.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
self.titleNode.layer.removeAnimation(forKey: "opacity")
self.subtitleNode.layer.removeAnimation(forKey: "opacity")
self.iconNode.layer.removeAnimation(forKey: "opacity")
self.animationNode?.layer.removeAnimation(forKey: "opacity")
}
}
}
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
self.theme = theme self.theme = theme
self.font = font self.font = font
@ -366,7 +383,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self, strongSelf.isEnabled { if let strongSelf = self, strongSelf.isEnabled && strongSelf.highlightEnabled {
if highlighted { if highlighted {
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackgroundNode.alpha = 0.55 strongSelf.buttonBackgroundNode.alpha = 0.55

View File

@ -7,10 +7,11 @@ import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import SolidRoundedButtonNode
import TooltipUI
final class ChatBotStartInputPanelNode: ChatInputPanelNode { final class ChatBotStartInputPanelNode: ChatInputPanelNode {
private let button: HighlightableButtonNode private let button: SolidRoundedButtonNode
private let activityIndicator: UIActivityIndicatorView
private var statusDisposable: Disposable? private var statusDisposable: Disposable?
@ -23,15 +24,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode {
if let startingBot = self.interfaceInteraction?.statuses?.startingBot { if let startingBot = self.interfaceInteraction?.statuses?.startingBot {
self.statusDisposable = (startingBot |> deliverOnMainQueue).start(next: { [weak self] value in self.statusDisposable = (startingBot |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self { if let strongSelf = self {
if value != !strongSelf.activityIndicator.isHidden { strongSelf.inProgress = value
if value {
strongSelf.activityIndicator.isHidden = false
strongSelf.activityIndicator.startAnimating()
} else {
strongSelf.activityIndicator.isHidden = true
strongSelf.activityIndicator.stopAnimating()
}
}
} }
}) })
} }
@ -40,28 +33,43 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode {
} }
} }
private var inProgress = false {
didSet {
if self.inProgress != oldValue {
if self.inProgress {
self.button.transitionToProgress()
} else {
self.button.transitionFromProgress()
}
}
}
}
private var theme: PresentationTheme private var theme: PresentationTheme
private var strings: PresentationStrings private var strings: PresentationStrings
private var tooltipController: TooltipScreen?
private var tooltipDismissed = false
init(theme: PresentationTheme, strings: PresentationStrings) { init(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.button = HighlightableButtonNode() self.button = SolidRoundedButtonNode(title: self.strings.Bot_Start, theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 11.0, gloss: true)
self.activityIndicator = UIActivityIndicatorView(style: .gray) self.button.progressType = .embedded
self.activityIndicator.isHidden = true
super.init() super.init()
self.addSubnode(self.button) self.addSubnode(self.button)
self.view.addSubview(self.activityIndicator)
self.button.setAttributedTitle(NSAttributedString(string: strings.Bot_Start, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) self.button.pressed = { [weak self] in
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) self?.buttonPressed()
}
} }
deinit { deinit {
self.statusDisposable?.dispose() self.statusDisposable?.dispose()
self.tooltipController?.dismiss()
} }
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
@ -69,44 +77,84 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.button.setAttributedTitle(NSAttributedString(string: strings.Bot_Start, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) self.button.updateTheme(SolidRoundedButtonTheme(theme: theme))
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
return self.button.view
} else {
return nil
} }
} }
@objc func buttonPressed() { @objc func buttonPressed() {
guard let _ = self.context, let presentationInterfaceState = self.presentationInterfaceState, let _ = presentationInterfaceState.renderedPeer?.peer else { guard let _ = self.context, let presentationInterfaceState = self.presentationInterfaceState else {
return return
} }
self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload) self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload)
if let tooltipController = self.tooltipController {
self.tooltipDismissed = false
self.tooltipController = nil
tooltipController.dismiss()
} }
}
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
super.updateAbsoluteRect(rect, within: containerSize, transition: transition)
let absoluteFrame = self.button.view.convert(self.button.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
if let tooltipController = self.tooltipController, self.view.window != nil {
tooltipController.location = .point(location, .bottom)
}
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
if self.presentationInterfaceState != interfaceState { if self.presentationInterfaceState != interfaceState {
self.presentationInterfaceState = interfaceState self.presentationInterfaceState = interfaceState
} }
let buttonSize = self.button.measure(CGSize(width: width - 80.0, height: 100.0)) let inset: CGFloat = max(leftInset, 16.0)
let maximumWidth: CGFloat = min(430.0, width)
let proceedHeight = self.button.updateLayout(width: maximumWidth - inset * 2.0, transition: transition)
let buttonSize = CGSize(width: maximumWidth - inset * 2.0, height: proceedHeight)
let panelHeight = defaultHeight(metrics: metrics) let panelHeight = defaultHeight(metrics: metrics) + 27.0
self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) / 2.0)), size: buttonSize) self.button.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: 8.0), size: buttonSize)
let indicatorSize = self.activityIndicator.bounds.size if !self.tooltipDismissed, let context = self.context {
self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) let absoluteFrame = self.button.view.convert(self.button.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
if let tooltipController = self.tooltipController {
if self.view.window != nil {
tooltipController.location = .point(location, .bottom)
}
} else {
let controller = TooltipScreen(account: context.account, text: self.strings.Bot_TapToUse, icon: .downArrows, location: .point(location, .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _ in
return .ignore
})
controller.alwaysVisible = true
self.tooltipController = controller
let delay: Double
if case .regular = metrics.widthClass {
delay = 0.1
} else {
delay = 0.35
}
Queue.mainQueue().after(delay, {
let absoluteFrame = self.button.view.convert(self.button.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
controller.location = .point(location, .bottom)
self.interfaceInteraction?.presentControllerInCurrent(controller, nil)
})
}
}
return panelHeight return panelHeight
} }
override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
return defaultHeight(metrics: metrics) return defaultHeight(metrics: metrics) + 27.0
} }
} }

View File

@ -233,7 +233,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private let context: AccountContext private let context: AccountContext
public let chatLocation: ChatLocation public let chatLocation: ChatLocation
public let subject: ChatControllerSubject? public let subject: ChatControllerSubject?
private let botStart: ChatControllerInitialBotStart? private var botStart: ChatControllerInitialBotStart?
private var attachBotStart: ChatControllerInitialAttachBotStart? private var attachBotStart: ChatControllerInitialAttachBotStart?
private var botAppStart: ChatControllerInitialBotAppStart? private var botAppStart: ChatControllerInitialBotAppStart?
@ -7183,6 +7183,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, {
$0.updatedChatHistoryState(state) $0.updatedChatHistoryState(state)
}) })
if let botStart = strongSelf.botStart, case let .loaded(isEmpty) = state {
strongSelf.botStart = nil
if !isEmpty {
strongSelf.startBot(botStart.payload)
}
}
} }
}) })
@ -9596,6 +9603,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
}, presentController: { [weak self] controller, arguments in }, presentController: { [weak self] controller, arguments in
self?.present(controller, in: .window(.root), with: arguments) self?.present(controller, in: .window(.root), with: arguments)
}, presentControllerInCurrent: { [weak self] controller, arguments in
self?.present(controller, in: .current, with: arguments)
}, getNavigationController: { [weak self] in }, getNavigationController: { [weak self] in
return self?.navigationController as? NavigationController return self?.navigationController as? NavigationController
}, presentGlobalOverlayController: { [weak self] controller, arguments in }, presentGlobalOverlayController: { [weak self] controller, arguments in
@ -17670,7 +17679,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let controller = controller as? UndoOverlayController { if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction() controller.dismissWithCommitAction()
} }
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen, !controller.alwaysVisible {
controller.dismiss() controller.dismiss()
} }
return true return true

View File

@ -535,6 +535,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var emptyType: ChatHistoryNodeLoadState.EmptyType? var emptyType: ChatHistoryNodeLoadState.EmptyType?
if case let .empty(type) = loadState { if case let .empty(type) = loadState {
if case .botInfo = type {
} else {
emptyType = type emptyType = type
if case .joined = type { if case .joined = type {
if strongSelf.didDisplayEmptyGreeting { if strongSelf.didDisplayEmptyGreeting {
@ -543,6 +545,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
strongSelf.didDisplayEmptyGreeting = true strongSelf.didDisplayEmptyGreeting = true
} }
} }
}
} else if case .messages = loadState { } else if case .messages = loadState {
strongSelf.didDisplayEmptyGreeting = true strongSelf.didDisplayEmptyGreeting = true
} }
@ -1966,13 +1969,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
if inputPanelNodeHandlesTransition { if inputPanelNodeHandlesTransition {
inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: .immediate)
inputPanelNode.frame = apparentInputPanelFrame inputPanelNode.frame = apparentInputPanelFrame
inputPanelNode.alpha = 1.0 inputPanelNode.alpha = 1.0
inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: .immediate)
} else { } else {
inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: transition)
transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame) transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame)
transition.updateAlpha(node: inputPanelNode, alpha: 1.0) transition.updateAlpha(node: inputPanelNode, alpha: 1.0)
inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: transition)
} }
if let viewForOverlayContent = inputPanelNode.viewForOverlayContent { if let viewForOverlayContent = inputPanelNode.viewForOverlayContent {

View File

@ -2739,15 +2739,20 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} else { } else {
loadState = .empty(.generic) loadState = .empty(.generic)
} }
} else {
if transition.historyView.filteredEntries.count == 1, let entry = transition.historyView.filteredEntries.first, case .ChatInfoEntry = entry {
loadState = .empty(.botInfo)
} else { } else {
loadState = .messages loadState = .messages
} }
}
if self.loadState != loadState { if self.loadState != loadState {
self.loadState = loadState self.loadState = loadState
self.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion)) self.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion))
} }
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty)
if self.currentHistoryState != historyState { if self.currentHistoryState != historyState {
self.currentHistoryState = historyState self.currentHistoryState = historyState
self.historyState.set(historyState) self.historyState.set(historyState)
@ -2924,10 +2929,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} else { } else {
if historyView.originalView.isLoadingEarlier && strongSelf.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser { if historyView.originalView.isLoadingEarlier && strongSelf.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser {
loadState = .loading(true) loadState = .loading(true)
} else {
if historyView.filteredEntries.count == 1, let entry = historyView.filteredEntries.first, case .ChatInfoEntry = entry {
loadState = .empty(.botInfo)
} else { } else {
loadState = .messages loadState = .messages
} }
} }
}
} else { } else {
loadState = .loading(false) loadState = .loading(false)
} }
@ -2989,7 +2998,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData))) strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData)))
} }
strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty)
if strongSelf.currentHistoryState != historyState { if strongSelf.currentHistoryState != historyState {
strongSelf.currentHistoryState = historyState strongSelf.currentHistoryState = historyState
strongSelf.historyState.set(historyState) strongSelf.historyState.set(historyState)

View File

@ -12,6 +12,7 @@ public enum ChatHistoryNodeLoadState: Equatable {
case joined case joined
case clearedHistory case clearedHistory
case topic case topic
case botInfo
} }
case loading(Bool) case loading(Bool)

View File

@ -86,7 +86,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
} }
} }
if chatPresentationInterfaceState.peerIsBlocked { if chatPresentationInterfaceState.peerIsBlocked, let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo == nil {
if let currentPanel = (currentPanel as? ChatUnblockInputPanelNode) ?? (currentSecondaryPanel as? ChatUnblockInputPanelNode) { if let currentPanel = (currentPanel as? ChatUnblockInputPanelNode) ?? (currentSecondaryPanel as? ChatUnblockInputPanelNode) {
currentPanel.interfaceInteraction = interfaceInteraction currentPanel.interfaceInteraction = interfaceInteraction
currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
@ -311,7 +311,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
} }
} }
if displayBotStartPanel { if displayBotStartPanel, !"".isEmpty {
if let currentPanel = (currentPanel as? ChatBotStartInputPanelNode) ?? (currentSecondaryPanel as? ChatBotStartInputPanelNode) { if let currentPanel = (currentPanel as? ChatBotStartInputPanelNode) ?? (currentSecondaryPanel as? ChatBotStartInputPanelNode) {
currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return (currentPanel, nil) return (currentPanel, nil)

View File

@ -120,6 +120,7 @@ final class ChatRecentActionsController: TelegramBaseController {
}, beginCall: { _ in }, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in }, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in }, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, getNavigationController: { }, getNavigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, presentGlobalOverlayController: { _, _ in

View File

@ -113,7 +113,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let makeLayout = TextNode.asyncLayout(self.textNode) let makeLayout = TextNode.asyncLayout(self.textNode)
let (size, apply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (size, apply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "0:00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 200.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = apply() let _ = apply()
self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size) self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size)
return size.size return size.size
@ -135,7 +135,12 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode {
if let parameters = parameters as? ChatTextInputAudioRecordingTimeNodeParameters { if let parameters = parameters as? ChatTextInputAudioRecordingTimeNodeParameters {
let currentAudioDurationSeconds = Int(parameters.timestamp) let currentAudioDurationSeconds = Int(parameters.timestamp)
let currentAudioDurationMilliseconds = Int(parameters.timestamp * 100.0) % 100 let currentAudioDurationMilliseconds = Int(parameters.timestamp * 100.0) % 100
let text = String(format: "%d:%02d,%02d", currentAudioDurationSeconds / 60, currentAudioDurationSeconds % 60, currentAudioDurationMilliseconds) let text: String
if currentAudioDurationSeconds >= 60 * 60 {
text = String(format: "%d:%02d:%02d,%02d", currentAudioDurationSeconds / 3600, currentAudioDurationSeconds / 60 % 60, currentAudioDurationSeconds % 60, currentAudioDurationMilliseconds)
} else {
text = String(format: "%d:%02d,%02d", currentAudioDurationSeconds / 60, currentAudioDurationSeconds % 60, currentAudioDurationMilliseconds)
}
let string = NSAttributedString(string: text, font: textFont, textColor: parameters.theme.chat.inputPanel.primaryTextColor) let string = NSAttributedString(string: text, font: textFont, textColor: parameters.theme.chat.inputPanel.primaryTextColor)
string.draw(at: CGPoint()) string.draw(at: CGPoint())
} }

View File

@ -34,6 +34,8 @@ import UndoUI
import PremiumUI import PremiumUI
import StickerPeekUI import StickerPeekUI
import LottieComponent import LottieComponent
import SolidRoundedButtonNode
import TooltipUI
private let accessoryButtonFont = Font.medium(14.0) private let accessoryButtonFont = Font.medium(14.0)
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
@ -488,6 +490,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
private let menuButtonIconNode: MenuIconNode private let menuButtonIconNode: MenuIconNode
private let menuButtonTextNode: ImmediateTextNode private let menuButtonTextNode: ImmediateTextNode
private let startButton: SolidRoundedButtonNode
let sendAsAvatarButtonNode: HighlightableButtonNode let sendAsAvatarButtonNode: HighlightableButtonNode
let sendAsAvatarReferenceNode: ContextReferenceContentNode let sendAsAvatarReferenceNode: ContextReferenceContentNode
let sendAsAvatarContainerNode: ContextControllerSourceNode let sendAsAvatarContainerNode: ContextControllerSourceNode
@ -575,6 +579,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return self.actionButtons.micButton return self.actionButtons.micButton
} }
private let startingBotDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable() private let statusDisposable = MetaDisposable()
override var interfaceInteraction: ChatPanelInterfaceInteraction? { override var interfaceInteraction: ChatPanelInterfaceInteraction? {
didSet { didSet {
@ -585,6 +590,25 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self?.updateIsProcessingInlineRequest(value) self?.updateIsProcessingInlineRequest(value)
})) }))
} }
if let startingBot = self.interfaceInteraction?.statuses?.startingBot {
self.startingBotDisposable.set((startingBot |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
strongSelf.startingBotProgress = value
}
}))
}
}
}
private var startingBotProgress = false {
didSet {
// if self.startingBotProgress != oldValue {
// if self.startingBotProgress {
// self.startButton.transitionToProgress()
// } else {
// self.startButton.transitionFromProgress()
// }
// }
} }
} }
@ -695,6 +719,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
private let presentationContext: ChatPresentationContext? private let presentationContext: ChatPresentationContext?
private var tooltipController: TooltipScreen?
init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceState = presentationInterfaceState
self.presentationContext = presentationContext self.presentationContext = presentationContext
@ -738,6 +764,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor
self.menuButtonTextNode = ImmediateTextNode() self.menuButtonTextNode = ImmediateTextNode()
self.startButton = SolidRoundedButtonNode(title: presentationInterfaceState.strings.Bot_Start, theme: SolidRoundedButtonTheme(theme: presentationInterfaceState.theme), height: 50.0, cornerRadius: 11.0, gloss: true)
self.startButton.progressType = .embedded
self.startButton.isHidden = true
self.sendAsAvatarButtonNode = HighlightableButtonNode() self.sendAsAvatarButtonNode = HighlightableButtonNode()
self.sendAsAvatarReferenceNode = ContextReferenceContentNode() self.sendAsAvatarReferenceNode = ContextReferenceContentNode()
self.sendAsAvatarContainerNode = ContextControllerSourceNode() self.sendAsAvatarContainerNode = ContextControllerSourceNode()
@ -820,6 +850,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
} }
} }
self.startButton.pressed = { [weak self] in
guard let self, let presentationInterfaceState = self.presentationInterfaceState else {
return
}
if presentationInterfaceState.peerIsBlocked {
self.interfaceInteraction?.unblockPeer()
} else {
self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload)
}
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
}
self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
@ -921,6 +967,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.clippingNode.addSubnode(self.attachmentButton) self.clippingNode.addSubnode(self.attachmentButton)
self.clippingNode.addSubnode(self.attachmentButtonDisabledNode) self.clippingNode.addSubnode(self.attachmentButtonDisabledNode)
self.clippingNode.addSubnode(self.startButton)
self.clippingNode.addSubnode(self.actionButtons) self.clippingNode.addSubnode(self.actionButtons)
self.clippingNode.addSubnode(self.counterTextNode) self.clippingNode.addSubnode(self.counterTextNode)
@ -972,6 +1020,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
deinit { deinit {
self.statusDisposable.dispose() self.statusDisposable.dispose()
self.tooltipController?.dismiss()
} }
func loadTextInputNodeIfNeeded() { func loadTextInputNodeIfNeeded() {
@ -1154,6 +1203,110 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
return minimalHeight return minimalHeight
} }
private var animatingTransition = false
private func animateBotButtonInFromMenu(transition: ContainedViewLayoutTransition) {
guard !self.animatingTransition else {
return
}
guard let menuIconSnapshotView = self.menuButtonIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) else {
self.startButton.highlightEnabled = true
self.menuButton.isHidden = true
return
}
if transition.isAnimated {
self.animatingTransition = true
self.startButton.highlightEnabled = false
}
self.menuButton.isHidden = true
transition.animateFrame(layer: self.startButton.layer, from: self.menuButton.frame)
transition.animateFrame(layer: self.startButton.buttonBackgroundNode.layer, from: CGRect(origin: .zero, size: self.menuButton.frame.size))
transition.animatePosition(node: self.startButton.titleNode, from: CGPoint(x: self.menuButton.frame.width / 2.0, y: self.menuButton.frame.height / 2.0))
let targetButtonCornerRadius = self.startButton.buttonCornerRadius
self.startButton.buttonBackgroundNode.cornerRadius = self.menuButton.cornerRadius
transition.updateCornerRadius(node: self.startButton.buttonBackgroundNode, cornerRadius: targetButtonCornerRadius)
transition.animateTransformScale(node: self.startButton.titleNode, from: 0.4)
self.startButton.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let menuContentDelta = (self.startButton.frame.width - self.menuButton.frame.width) / 2.0
menuIconSnapshotView.frame = self.menuButtonIconNode.frame.offsetBy(dx: self.menuButton.frame.minX, dy: self.menuButton.frame.minY)
self.view.addSubview(menuIconSnapshotView)
menuIconSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuIconSnapshotView] _ in
menuIconSnapshotView?.removeFromSuperview()
})
transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y))
menuTextSnapshotView.frame = self.menuButtonTextNode.frame.offsetBy(dx: self.menuButton.frame.minX + 19.0, dy: self.menuButton.frame.minY)
self.view.addSubview(menuTextSnapshotView)
menuTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuTextSnapshotView, weak self] _ in
menuTextSnapshotView?.removeFromSuperview()
self?.animatingTransition = false
self?.startButton.highlightEnabled = true
})
transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x + menuContentDelta, y: self.startButton.position.y))
}
func animateBotButtonOutToMenu(transition: ContainedViewLayoutTransition) {
guard !self.animatingTransition else {
return
}
guard let menuIconSnapshotView = self.menuButtonIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = self.menuButtonTextNode.view.snapshotView(afterScreenUpdates: false) else {
self.startButton.highlightEnabled = true
self.menuButton.isHidden = false
return
}
if transition.isAnimated {
self.animatingTransition = true
self.startButton.highlightEnabled = false
}
let sourceButtonFrame = self.startButton.frame
transition.updateFrame(node: self.startButton, frame: self.menuButton.frame)
transition.updateFrame(node: self.startButton.buttonBackgroundNode, frame: CGRect(origin: .zero, size: self.menuButton.frame.size))
let sourceButtonTextPosition = self.startButton.titleNode.position
transition.updatePosition(node: self.startButton.titleNode, position: CGPoint(x: self.menuButton.frame.width / 2.0, y: self.menuButton.frame.height / 2.0))
let sourceButtonCornerRadius = self.startButton.buttonCornerRadius
transition.updateCornerRadius(node: self.startButton.buttonBackgroundNode, cornerRadius: self.menuButton.cornerRadius)
transition.animateTransformScale(layer: self.startButton.titleNode.layer, from: CGPoint(x: 1.0, y: 1.0), to: CGPoint(x: 0.4, y: 0.4))
Queue.mainQueue().justDispatch {
self.startButton.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
let menuContentDelta = (sourceButtonFrame.width - self.menuButton.frame.width) / 2.0
var menuIconSnapshotViewFrame = self.menuButtonIconNode.frame.offsetBy(dx: self.menuButton.frame.minX + menuContentDelta, dy: self.menuButton.frame.minY)
menuIconSnapshotViewFrame.origin.y = self.startButton.position.y - menuIconSnapshotViewFrame.height / 2.0
menuIconSnapshotView.frame = menuIconSnapshotViewFrame
self.view.addSubview(menuIconSnapshotView)
menuIconSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x - menuContentDelta, y: self.menuButton.position.y))
var menuTextSnapshotViewFrame = self.menuButtonTextNode.frame.offsetBy(dx: self.menuButton.frame.minX + 19.0 + menuContentDelta, dy: self.menuButton.frame.minY)
menuTextSnapshotViewFrame.origin.y = self.startButton.position.y - menuTextSnapshotViewFrame.height / 2.0
menuTextSnapshotView.frame = menuTextSnapshotViewFrame
self.view.addSubview(menuTextSnapshotView)
menuTextSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x - menuContentDelta, y: self.menuButton.position.y), completion: { [weak self, weak menuIconSnapshotView, weak menuTextSnapshotView] _ in
self?.animatingTransition = false
menuIconSnapshotView?.removeFromSuperview()
menuTextSnapshotView?.removeFromSuperview()
self?.menuButton.isHidden = false
self?.startButton.isHidden = true
self?.startButton.frame = sourceButtonFrame
self?.startButton.buttonBackgroundNode.frame = CGRect(origin: .zero, size: sourceButtonFrame.size)
self?.startButton.titleNode.position = sourceButtonTextPosition
self?.startButton.titleNode.layer.removeAllAnimations()
self?.startButton.buttonBackgroundNode.cornerRadius = sourceButtonCornerRadius
self?.startButton.highlightEnabled = true
})
}
private var absoluteRect: (CGRect, CGSize)? private var absoluteRect: (CGRect, CGSize)?
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.absoluteRect = (rect, containerSize) self.absoluteRect = (rect, containerSize)
@ -1161,7 +1314,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
if !self.actionButtons.frame.width.isZero { if !self.actionButtons.frame.width.isZero {
self.actionButtons.updateAbsoluteRect(CGRect(origin: rect.origin.offsetBy(dx: self.actionButtons.frame.minX, dy: self.actionButtons.frame.minY), size: self.actionButtons.frame.size), within: containerSize, transition: transition) self.actionButtons.updateAbsoluteRect(CGRect(origin: rect.origin.offsetBy(dx: self.actionButtons.frame.minX, dy: self.actionButtons.frame.minY), size: self.actionButtons.frame.size), within: containerSize, transition: transition)
} }
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
if let tooltipController = self.tooltipController, self.view.window != nil {
tooltipController.location = .point(location, .bottom)
} }
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let previousAdditionalSideInsets = self.validLayout?.4 let previousAdditionalSideInsets = self.validLayout?.4
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded)
@ -1229,6 +1390,127 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled self.textInputNode?.isUserInteractionEnabled = !sendingTextDisabled
var displayBotStartButton = false
if case .scheduledMessages = interfaceState.subject {
} else {
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true) = chatHistoryState {
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartButton = true
}
}
}
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
var hasMenuButton = false
var menuButtonExpanded = false
var isSendAsButton = false
var shouldDisplayMenuButton = false
if interfaceState.hasBotCommands {
shouldDisplayMenuButton = true
} else if case .webView = interfaceState.botMenuButton {
shouldDisplayMenuButton = true
}
let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
hasMenuButton = true
menuButtonExpanded = false
isSendAsButton = true
self.sendAsAvatarNode.isHidden = false
var currentPeer = sendAsPeers.first(where: { $0.peer.id == interfaceState.currentSendAsPeerId})?.peer
if currentPeer == nil {
currentPeer = sendAsPeers.first?.peer
}
if let context = self.context, let peer = currentPeer {
self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: EnginePeer(peer), emptyColor: interfaceState.theme.list.mediaPlaceholderColor)
}
} else if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, shouldDisplayMenuButton && interfaceState.editMessageState == nil {
hasMenuButton = true
if !inputHasText {
switch interfaceState.inputMode {
case .none, .inputButtons:
menuButtonExpanded = true
default:
break
}
}
self.sendAsAvatarNode.isHidden = true
} else {
self.sendAsAvatarNode.isHidden = true
}
if mediaRecordingState != nil {
hasMenuButton = false
}
let buttonInset: CGFloat = max(leftInset, 16.0)
let maximumButtonWidth: CGFloat = min(430.0, width)
let buttonHeight = self.startButton.updateLayout(width: maximumButtonWidth - buttonInset * 2.0, transition: transition)
let buttonSize = CGSize(width: maximumButtonWidth - buttonInset * 2.0, height: buttonHeight)
self.startButton.frame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - rightInset - buttonSize.width) / 2.0), y: 6.0), size: buttonSize)
var hideOffset: CGPoint = .zero
if displayBotStartButton {
if hasMenuButton {
hideOffset = CGPoint(x: width, y: 0.0)
} else {
hideOffset = CGPoint(x: 0.0, y: 80.0)
}
if self.startButton.isHidden {
self.startButton.isHidden = false
if hasMenuButton {
self.animateBotButtonInFromMenu(transition: transition)
} else {
transition.animatePosition(layer: self.startButton.layer, from: CGPoint(x: 0.0, y: 80.0), to: CGPoint(), additive: true)
}
}
if let context = self.context {
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
if let tooltipController = self.tooltipController {
if self.view.window != nil {
tooltipController.location = .point(location, .bottom)
}
} else {
let controller = TooltipScreen(account: context.account, text: interfaceState.strings.Bot_TapToUse, icon: .downArrows, location: .point(location, .bottom), displayDuration: .infinite, shouldDismissOnTouch: { _ in
return .ignore
})
controller.alwaysVisible = true
self.tooltipController = controller
let delay: Double
if case .regular = metrics.widthClass {
delay = 0.1
} else {
delay = 0.35
}
Queue.mainQueue().after(delay, {
let absoluteFrame = self.startButton.view.convert(self.startButton.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 1.0), size: CGSize())
controller.location = .point(location, .bottom)
self.interfaceInteraction?.presentControllerInCurrent(controller, nil)
})
}
}
} else if !self.startButton.isHidden {
if hasMenuButton {
self.animateBotButtonOutToMenu(transition: transition)
} else {
transition.animatePosition(node: self.startButton, to: CGPoint(x: 0.0, y: 80.0), additive: true, completion: { _ in
self.startButton.isHidden = true
})
}
}
var buttonTitleUpdated = false var buttonTitleUpdated = false
var menuTextSize = self.menuButtonTextNode.frame.size var menuTextSize = self.menuButtonTextNode.frame.size
if self.presentationInterfaceState != interfaceState { if self.presentationInterfaceState != interfaceState {
@ -1243,6 +1525,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let themeUpdated = previousState?.theme !== interfaceState.theme let themeUpdated = previousState?.theme !== interfaceState.theme
if themeUpdated { if themeUpdated {
self.menuButtonIconNode.customColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor self.menuButtonIconNode.customColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor
self.startButton.updateTheme(SolidRoundedButtonTheme(theme: interfaceState.theme))
} }
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty { if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty {
self.menuButtonIconNode.enqueueState(.close, animated: false) self.menuButtonIconNode.enqueueState(.close, animated: false)
@ -1527,56 +1810,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.accessoryItemButtons = updatedButtons self.accessoryItemButtons = updatedButtons
} }
let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState
var inputHasText = false
if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 {
inputHasText = true
}
var hasMenuButton = false
var menuButtonExpanded = false
var isSendAsButton = false
var shouldDisplayMenuButton = false
if interfaceState.hasBotCommands {
shouldDisplayMenuButton = true
} else if case .webView = interfaceState.botMenuButton {
shouldDisplayMenuButton = true
}
if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil {
hasMenuButton = true
menuButtonExpanded = false
isSendAsButton = true
self.sendAsAvatarNode.isHidden = false
var currentPeer = sendAsPeers.first(where: { $0.peer.id == interfaceState.currentSendAsPeerId})?.peer
if currentPeer == nil {
currentPeer = sendAsPeers.first?.peer
}
if let context = self.context, let peer = currentPeer {
self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: EnginePeer(peer), emptyColor: interfaceState.theme.list.mediaPlaceholderColor)
}
} else if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, shouldDisplayMenuButton && interfaceState.editMessageState == nil {
hasMenuButton = true
if !inputHasText {
switch interfaceState.inputMode {
case .none, .inputButtons:
menuButtonExpanded = true
default:
break
}
}
self.sendAsAvatarNode.isHidden = true
} else {
self.sendAsAvatarNode.isHidden = true
}
if mediaRecordingState != nil {
hasMenuButton = false
}
let leftMenuInset: CGFloat let leftMenuInset: CGFloat
let menuButtonHeight: CGFloat = 33.0 let menuButtonHeight: CGFloat = 33.0
let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 38.0 let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 38.0
@ -1599,17 +1832,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let baseWidth = width - leftInset - leftMenuInset - rightInset let baseWidth = width - leftInset - leftMenuInset - rightInset
let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics)
let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics)
if displayBotStartButton {
panelHeight += 27.0
}
let menuButtonFrame = CGRect(x: leftInset + 10.0, y: panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0), width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight) let menuButtonOriginY: CGFloat
if displayBotStartButton {
menuButtonOriginY = floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0)
} else {
menuButtonOriginY = panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0)
}
let menuButtonFrame = CGRect(x: leftInset + 10.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight)
transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame) transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame)
transition.updateFrame(node: self.menuButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size)) transition.updateFrame(node: self.menuButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height))) transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height)))
var buttonTitlteTransition = transition var menuButtonTitleTransition = transition
if buttonTitleUpdated { if buttonTitleUpdated {
buttonTitlteTransition = .immediate menuButtonTitleTransition = .immediate
} }
buttonTitlteTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 7.0 - UIScreenPixel), size: menuTextSize)) menuButtonTitleTransition.updateFrame(node: self.menuButtonTextNode, frame: CGRect(origin: CGPoint(x: 16.0, y: 7.0 - UIScreenPixel), size: menuTextSize))
transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0) transition.updateAlpha(node: self.menuButtonTextNode, alpha: menuButtonExpanded ? 1.0 : 0.0)
transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: isSendAsButton ? 1.0 + UIScreenPixel : (4.0 + UIScreenPixel), y: 1.0 + UIScreenPixel, width: 30.0, height: 30.0)) transition.updateFrame(node: self.menuButtonIconNode, frame: CGRect(x: isSendAsButton ? 1.0 + UIScreenPixel : (4.0 + UIScreenPixel), y: 1.0 + UIScreenPixel, width: 30.0, height: 30.0))
@ -1917,7 +2160,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
var leftInset = leftInset var leftInset = leftInset
leftInset += leftMenuInset leftInset += leftMenuInset
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + 2.0 - UIScreenPixel, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)))
transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame) transition.updateFrame(node: self.attachmentButtonDisabledNode, frame: self.attachmentButton.frame)
var composeButtonsOffset: CGFloat = 0.0 var composeButtonsOffset: CGFloat = 0.0
@ -1929,7 +2172,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.updateCounterTextNode(transition: transition) self.updateCounterTextNode(transition: transition)
let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight)) let actionButtonsFrame = CGRect(origin: CGPoint(x: hideOffset.x + width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: hideOffset.y + panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight))
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
if let (rect, containerSize) = self.absoluteRect { if let (rect, containerSize) = self.absoluteRect {
self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition) self.actionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + actionButtonsFrame.origin.x, y: rect.origin.y + actionButtonsFrame.origin.y, width: actionButtonsFrame.width, height: actionButtonsFrame.height), within: containerSize, transition: transition)
@ -1991,7 +2234,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth)
} }
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) let textInputFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size))
transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha)
@ -2025,7 +2268,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let _ = placeholderApply() let _ = placeholderApply()
contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size) transition.updateFrame(node: contextPlaceholderNode, frame: CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: placeholderSize.size))
contextPlaceholderNode.alpha = audioRecordingItemsAlpha contextPlaceholderNode.alpha = audioRecordingItemsAlpha
} else if let contextPlaceholderNode = self.contextPlaceholderNode { } else if let contextPlaceholderNode = self.contextPlaceholderNode {
self.contextPlaceholderNode = nil self.contextPlaceholderNode = nil
@ -2060,7 +2303,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.slowmodePlaceholderNode?.isHidden = true self.slowmodePlaceholderNode?.isHidden = true
} }
var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight) var nextButtonTopRight = CGPoint(x: hideOffset.x + width - rightInset - textFieldInsets.right - accessoryButtonInset, y: hideOffset.y + panelHeight - textFieldInsets.bottom - minimalInputHeight)
for (item, button) in self.accessoryItemButtons.reversed() { for (item, button) in self.accessoryItemButtons.reversed() {
let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight)
button.updateLayout(item: item, size: buttonSize) button.updateLayout(item: item, size: buttonSize)
@ -2080,7 +2323,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
nextButtonTopRight.x -= accessoryButtonSpacing nextButtonTopRight.x -= accessoryButtonSpacing
} }
let textInputBackgroundFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) let textInputBackgroundFrame = CGRect(x: hideOffset.x + leftInset + textFieldInsets.left, y: hideOffset.y + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame) transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputBackgroundFrame)
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha) transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
@ -2105,7 +2348,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size)) textLockIconTransition.updateFrame(node: textLockIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 4.0, y: floor((textPlaceholderFrame.height - image.size.height) / 2.0)), size: image.size))
} }
} else { } else {
textPlaceholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) textPlaceholderFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: hideOffset.y + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
if let textLockIconNode = self.textLockIconNode { if let textLockIconNode = self.textLockIconNode {
self.textLockIconNode = nil self.textLockIconNode = nil

View File

@ -350,6 +350,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, beginCall: { _ in }, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in }, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in }, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, getNavigationController: { }, getNavigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, presentGlobalOverlayController: { _, _ in

View File

@ -597,6 +597,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, beginCall: { _ in }, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in }, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in }, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, getNavigationController: { }, getNavigationController: {
return nil return nil
}, presentGlobalOverlayController: { _, _ in }, presentGlobalOverlayController: { _, _ in

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import AnimatedStickerNode import AnimatedStickerNode
import TelegramAnimatedStickerNode import TelegramAnimatedStickerNode
@ -29,11 +30,93 @@ public enum TooltipActiveTextAction {
case longTap case longTap
} }
private func generateArrowImage() -> UIImage? {
return generateImage(CGSize(width: 14.0, height: 8.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(1.0 + UIScreenPixel)
context.setLineCap(.round)
let arrowBounds = bounds.insetBy(dx: 1.0, dy: 1.0)
context.move(to: arrowBounds.origin)
context.addLine(to: CGPoint(x: arrowBounds.midX, y: arrowBounds.maxY))
context.addLine(to: CGPoint(x: arrowBounds.maxX, y: arrowBounds.minY))
context.strokePath()
})
}
private class DownArrowsIconNode: ASDisplayNode {
private let topArrow: ASImageNode
private let bottomArrow: ASImageNode
override init() {
self.topArrow = ASImageNode()
self.topArrow.displaysAsynchronously = false
self.topArrow.image = generateArrowImage()
self.bottomArrow = ASImageNode()
self.bottomArrow.displaysAsynchronously = false
self.bottomArrow.image = self.topArrow.image
super.init()
self.addSubnode(self.topArrow)
self.addSubnode(self.bottomArrow)
if let image = self.topArrow.image {
self.topArrow.frame = CGRect(origin: .zero, size: image.size)
self.bottomArrow.frame = CGRect(origin: CGPoint(x: 0.0, y: 7.0), size: image.size)
}
}
func setupAnimations() {
guard self.bottomArrow.layer.animation(forKey: "position") == nil else {
return
}
self.supernode?.layer.animateKeyframes(values: [
NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 1.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: -0.5)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 1.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0))
], duration: 1.1, keyPath: "position", additive: true)
self.bottomArrow.layer.animateKeyframes(values: [
NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 4.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: -0.5)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 4.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0))
], duration: 1.1, keyPath: "position", additive: true, completion: { [weak self] _ in
Queue.mainQueue().after(2.9) {
self?.setupAnimations()
}
})
self.topArrow.layer.animateKeyframes(values: [
NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 6.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: -0.5)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 6.0)),
NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0))
], duration: 1.1, keyPath: "position", additive: true)
}
}
private final class TooltipScreenNode: ViewControllerTracingNode { private final class TooltipScreenNode: ViewControllerTracingNode {
private let tooltipStyle: TooltipScreen.Style private let tooltipStyle: TooltipScreen.Style
private let icon: TooltipScreen.Icon? private let icon: TooltipScreen.Icon?
private let customContentNode: TooltipCustomContentNode? private let customContentNode: TooltipCustomContentNode?
private let location: TooltipScreen.Location var location: TooltipScreen.Location {
didSet {
if let layout = self.validLayout {
self.updateLayout(layout: layout, transition: .immediate)
}
}
}
private let displayDuration: TooltipScreen.DisplayDuration private let displayDuration: TooltipScreen.DisplayDuration
private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch
private let requestDismiss: () -> Void private let requestDismiss: () -> Void
@ -50,6 +133,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private let arrowContainer: ASDisplayNode private let arrowContainer: ASDisplayNode
private var arrowEffectView: UIView? private var arrowEffectView: UIView?
private let animatedStickerNode: AnimatedStickerNode private let animatedStickerNode: AnimatedStickerNode
private var downArrowsNode: DownArrowsIconNode?
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private var isArrowInverted: Bool = false private var isArrowInverted: Bool = false
@ -138,6 +222,24 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.arrowEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) self.arrowEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
self.arrowContainer.view.addSubview(self.arrowEffectView!) self.arrowContainer.view.addSubview(self.arrowEffectView!)
let maskLayer = CAShapeLayer()
if let path = try? svgPath("M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ", scale: CGPoint(x: 0.333333, y: 0.333333), offset: CGPoint()) {
maskLayer.path = path.cgPath
}
maskLayer.frame = CGRect(origin: CGPoint(), size: arrowSize)
self.arrowContainer.layer.mask = maskLayer
} else if case .default = style {
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.backgroundContainerNode.clipsToBounds = true
self.backgroundContainerNode.cornerRadius = 14.0
if #available(iOS 13.0, *) {
self.backgroundContainerNode.layer.cornerCurve = .continuous
}
fontSize = 14.0
self.arrowEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.arrowContainer.view.addSubview(self.arrowEffectView!)
let maskLayer = CAShapeLayer() let maskLayer = CAShapeLayer()
if let path = try? svgPath("M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ", scale: CGPoint(x: 0.333333, y: 0.333333), offset: CGPoint()) { if let path = try? svgPath("M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ", scale: CGPoint(x: 0.333333, y: 0.333333), offset: CGPoint()) {
maskLayer.path = path.cgPath maskLayer.path = path.cgPath
@ -179,7 +281,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
} else if case .top = location { } else if case .top = location {
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.containerNode.clipsToBounds = true self.containerNode.clipsToBounds = true
self.containerNode.cornerRadius = 9.0 self.containerNode.cornerRadius = 14.0
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
self.containerNode.layer.cornerCurve = .continuous self.containerNode.layer.cornerCurve = .continuous
} }
@ -204,6 +306,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
case .info: case .info:
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "anim_infotip"), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "anim_infotip"), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.animatedStickerNode.automaticallyLoadFirstFrame = true self.animatedStickerNode.automaticallyLoadFirstFrame = true
case .downArrows:
self.downArrowsNode = DownArrowsIconNode()
} }
super.init() super.init()
@ -227,6 +331,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
} }
self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.textNode)
self.containerNode.addSubnode(self.animatedStickerNode) self.containerNode.addSubnode(self.animatedStickerNode)
if let downArrowsNode = self.downArrowsNode {
self.containerNode.addSubnode(downArrowsNode)
}
self.scrollingContainer.addSubnode(self.containerNode) self.scrollingContainer.addSubnode(self.containerNode)
self.addSubnode(self.scrollingContainer) self.addSubnode(self.scrollingContainer)
@ -298,7 +405,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
let sideInset: CGFloat = self.inset + layout.safeInsets.left let sideInset: CGFloat = self.inset + layout.safeInsets.left
let bottomInset: CGFloat = 10.0 let bottomInset: CGFloat = 10.0
let contentInset: CGFloat = 11.0 let contentInset: CGFloat = 11.0
let contentVerticalInset: CGFloat = 11.0 let contentVerticalInset: CGFloat = 8.0
let animationSize: CGSize let animationSize: CGSize
let animationInset: CGFloat let animationInset: CGFloat
let animationSpacing: CGFloat let animationSpacing: CGFloat
@ -308,6 +415,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
animationSize = CGSize() animationSize = CGSize()
animationInset = 0.0 animationInset = 0.0
animationSpacing = 0.0 animationSpacing = 0.0
case .downArrows:
animationSize = CGSize(width: 24.0, height: 32.0)
animationInset = (40.0 - animationSize.width) / 2.0
animationSpacing = 8.0
case .chatListPress: case .chatListPress:
animationSize = CGSize(width: 32.0, height: 32.0) animationSize = CGSize(width: 32.0, height: 32.0)
animationInset = (70.0 - animationSize.width) / 2.0 animationInset = (70.0 - animationSize.width) / 2.0
@ -412,8 +523,15 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize))
transition.updateFrame(node: self.animatedStickerNode, frame: CGRect(origin: CGPoint(x: contentInset - animationInset, y: contentVerticalInset - animationInset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))) let animationFrame = CGRect(origin: CGPoint(x: contentInset - animationInset, y: contentVerticalInset - animationInset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))
transition.updateFrame(node: self.animatedStickerNode, frame: animationFrame)
self.animatedStickerNode.updateLayout(size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)) self.animatedStickerNode.updateLayout(size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))
if let downArrowsNode = self.downArrowsNode {
let arrowsSize = CGSize(width: 16.0, height: 16.0)
transition.updateFrame(node: downArrowsNode, frame: CGRect(origin: CGPoint(x: animationFrame.midX - arrowsSize.width / 2.0, y: animationFrame.midY - arrowsSize.height / 2.0), size: arrowsSize))
downArrowsNode.setupAnimations()
}
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
@ -472,7 +590,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
animationDelay = 0.6 animationDelay = 0.6
case .info: case .info:
animationDelay = 0.2 animationDelay = 0.2
case .none: case .none, .downArrows:
animationDelay = 0.0 animationDelay = 0.0
} }
@ -527,6 +645,7 @@ public final class TooltipScreen: ViewController {
public enum Icon { public enum Icon {
case info case info
case chatListPress case chatListPress
case downArrows
} }
public enum DismissOnTouch { public enum DismissOnTouch {
@ -548,6 +667,7 @@ public final class TooltipScreen: ViewController {
public enum DisplayDuration { public enum DisplayDuration {
case `default` case `default`
case custom(Double) case custom(Double)
case infinite
} }
public enum Style { public enum Style {
@ -562,7 +682,13 @@ public final class TooltipScreen: ViewController {
private let style: TooltipScreen.Style private let style: TooltipScreen.Style
private let icon: TooltipScreen.Icon? private let icon: TooltipScreen.Icon?
private let customContentNode: TooltipCustomContentNode? private let customContentNode: TooltipCustomContentNode?
private let location: TooltipScreen.Location public var location: TooltipScreen.Location {
didSet {
if self.isNodeLoaded {
self.controllerNode.location = self.location
}
}
}
private let displayDuration: DisplayDuration private let displayDuration: DisplayDuration
private let inset: CGFloat private let inset: CGFloat
private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch
@ -580,6 +706,8 @@ public final class TooltipScreen: ViewController {
private var dismissTimer: Foundation.Timer? private var dismissTimer: Foundation.Timer?
public var alwaysVisible = false
public init(account: Account, text: String, textEntities: [MessageTextEntity] = [], style: TooltipScreen.Style = .default, icon: TooltipScreen.Icon?, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: DisplayDuration = .default, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil) { public init(account: Account, text: String, textEntities: [MessageTextEntity] = [], style: TooltipScreen.Style = .default, icon: TooltipScreen.Icon?, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: DisplayDuration = .default, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil) {
self.account = account self.account = account
self.text = text self.text = text
@ -615,6 +743,7 @@ public final class TooltipScreen: ViewController {
public func resetDismissTimeout(duration: TooltipScreen.DisplayDuration? = nil) { public func resetDismissTimeout(duration: TooltipScreen.DisplayDuration? = nil) {
self.dismissTimer?.invalidate() self.dismissTimer?.invalidate()
self.dismissTimer = nil
let timeout: Double let timeout: Double
switch duration ?? self.displayDuration { switch duration ?? self.displayDuration {
@ -622,6 +751,8 @@ public final class TooltipScreen: ViewController {
timeout = 5.0 timeout = 5.0
case let .custom(value): case let .custom(value):
timeout = value timeout = value
case .infinite:
return
} }
final class TimerTarget: NSObject { final class TimerTarget: NSObject {
@ -658,7 +789,7 @@ public final class TooltipScreen: ViewController {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)
if let validLayout = self.validLayout { if let validLayout = self.validLayout, !self.alwaysVisible {
if validLayout.size.width != layout.size.width { if validLayout.size.width != layout.size.width {
self.dismiss() self.dismiss()
} }

View File

@ -117,6 +117,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
if !pathComponents.isEmpty { if !pathComponents.isEmpty {
pathComponents.removeFirst() pathComponents.removeFirst()
} }
if let lastComponent = pathComponents.last, lastComponent.isEmpty {
pathComponents.removeLast()
}
if !pathComponents.isEmpty && !pathComponents[0].isEmpty { if !pathComponents.isEmpty && !pathComponents[0].isEmpty {
let peerName: String = pathComponents[0] let peerName: String = pathComponents[0]
if pathComponents.count == 1 { if pathComponents.count == 1 {