From 25d7aaf7e3935ff374394bd1e7d5664376254b15 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 17 Mar 2023 22:55:37 +0400 Subject: [PATCH] Bot start button improvements --- .../Sources/AttachmentPanel.swift | 1 + .../Sources/ChatListController.swift | 2 +- .../ChatPanelInterfaceInteraction.swift | 4 + .../ContextUI/Sources/PinchController.swift | 4 - .../Sources/SolidRoundedButtonNode.swift | 25 +- .../Sources/ChatBotStartInputPanelNode.swift | 116 ++++-- .../TelegramUI/Sources/ChatController.swift | 13 +- .../Sources/ChatControllerNode.swift | 19 +- .../Sources/ChatHistoryListNode.swift | 18 +- .../TelegramUI/Sources/ChatHistoryNode.swift | 1 + .../ChatInterfaceStateInputPanels.swift | 4 +- .../Sources/ChatRecentActionsController.swift | 1 + .../ChatTextInputAudioRecordingTimeNode.swift | 9 +- .../Sources/ChatTextInputPanelNode.swift | 373 +++++++++++++++--- .../Sources/PeerInfo/PeerInfoScreen.swift | 1 + .../Sources/PeerSelectionControllerNode.swift | 1 + .../TooltipUI/Sources/TooltipScreen.swift | 145 ++++++- .../UrlHandling/Sources/UrlHandling.swift | 3 + 18 files changed, 607 insertions(+), 133 deletions(-) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 98cec79c87..bc1b81ada7 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -871,6 +871,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 31209f9e0d..05e9622f3e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1957,7 +1957,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 8.0), size: CGSize()) - parentController.present(TooltipScreen(account: strongSelf.context.account, text: text, icon: .chatListPress, location: .point(location, .bottom), shouldDismissOnTouch: { point in + parentController.present(TooltipScreen(account: strongSelf.context.account, text: text, icon: .chatListPress, location: .point(location, .bottom), shouldDismissOnTouch: { point in guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else { return .dismiss(consume: false) } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 2eb5b669ec..dd73397d2e 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -125,6 +125,7 @@ public final class ChatPanelInterfaceInteraction { public let beginCall: (Bool) -> Void public let toggleMessageStickerStarred: (MessageId) -> Void public let presentController: (ViewController, Any?) -> Void + public let presentControllerInCurrent: (ViewController, Any?) -> Void public let getNavigationController: () -> NavigationController? public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let navigateFeed: () -> Void @@ -228,6 +229,7 @@ public final class ChatPanelInterfaceInteraction { beginCall: @escaping (Bool) -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, + presentControllerInCurrent: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, @@ -330,6 +332,7 @@ public final class ChatPanelInterfaceInteraction { self.beginCall = beginCall self.toggleMessageStickerStarred = toggleMessageStickerStarred self.presentController = presentController + self.presentControllerInCurrent = presentControllerInCurrent self.getNavigationController = getNavigationController self.presentGlobalOverlayController = presentGlobalOverlayController self.navigateFeed = navigateFeed @@ -440,6 +443,7 @@ public final class ChatPanelInterfaceInteraction { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift index 41610a3a71..f08242ed22 100644 --- a/submodules/ContextUI/Sources/PinchController.swift +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -138,7 +138,6 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD private(set) var naturalContentFrame: CGRect? fileprivate let gesture: PinchSourceGesture - fileprivate var panGesture: UIPanGestureRecognizer? public var isPinchGestureEnabled: Bool = true { 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 { return false } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 200aab7e66..6ab27211ad 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -164,7 +164,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { private var fontSize: CGFloat private let gloss: Bool - private let buttonBackgroundNode: ASImageNode + public let buttonBackgroundNode: ASImageNode private var buttonBackgroundAnimationView: UIImageView? private var shimmerView: ShimmerEffectForegroundView? @@ -173,7 +173,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { private var borderShimmerView: ShimmerEffectForegroundView? private let buttonNode: HighlightTrackingButtonNode - private let titleNode: ImmediateTextNode + public let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode private let iconNode: ASImageNode private var animationNode: SimpleAnimationNode? @@ -181,7 +181,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { private var badgeNode: BadgeNode? private let buttonHeight: CGFloat - private let buttonCornerRadius: CGFloat + public let buttonCornerRadius: CGFloat public var pressed: (() -> Void)? public var validLayout: CGFloat? @@ -309,6 +309,23 @@ public final class SolidRoundedButtonNode: ASDisplayNode { 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) { self.theme = theme self.font = font @@ -366,7 +383,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self, strongSelf.isEnabled { + if let strongSelf = self, strongSelf.isEnabled && strongSelf.highlightEnabled { if highlighted { strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonBackgroundNode.alpha = 0.55 diff --git a/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift index 028769b5ec..e0989b05dc 100644 --- a/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatBotStartInputPanelNode.swift @@ -7,10 +7,11 @@ import Postbox import SwiftSignalKit import TelegramPresentationData import ChatPresentationInterfaceState +import SolidRoundedButtonNode +import TooltipUI final class ChatBotStartInputPanelNode: ChatInputPanelNode { - private let button: HighlightableButtonNode - private let activityIndicator: UIActivityIndicatorView + private let button: SolidRoundedButtonNode private var statusDisposable: Disposable? @@ -23,15 +24,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { if let startingBot = self.interfaceInteraction?.statuses?.startingBot { self.statusDisposable = (startingBot |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { - if value != !strongSelf.activityIndicator.isHidden { - if value { - strongSelf.activityIndicator.isHidden = false - strongSelf.activityIndicator.startAnimating() - } else { - strongSelf.activityIndicator.isHidden = true - strongSelf.activityIndicator.stopAnimating() - } - } + strongSelf.inProgress = value } }) } @@ -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 strings: PresentationStrings + private var tooltipController: TooltipScreen? + private var tooltipDismissed = false + init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme self.strings = strings - self.button = HighlightableButtonNode() - self.activityIndicator = UIActivityIndicatorView(style: .gray) - self.activityIndicator.isHidden = true + self.button = SolidRoundedButtonNode(title: self.strings.Bot_Start, theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 11.0, gloss: true) + self.button.progressType = .embedded super.init() 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.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) + + self.button.pressed = { [weak self] in + self?.buttonPressed() + } } deinit { self.statusDisposable?.dispose() + self.tooltipController?.dismiss() } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { @@ -69,44 +77,84 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.theme = theme self.strings = strings - self.button.setAttributedTitle(NSAttributedString(string: strings.Bot_Start, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - return self.button.view - } else { - return nil + self.button.updateTheme(SolidRoundedButtonTheme(theme: theme)) } } @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 } 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 { if 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 - self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width - 12.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + if !self.tooltipDismissed, let context = self.context { + 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 } override func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { - return defaultHeight(metrics: metrics) + return defaultHeight(metrics: metrics) + 27.0 } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e5fc27570f..e43f4c0993 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -233,7 +233,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private let context: AccountContext public let chatLocation: ChatLocation public let subject: ChatControllerSubject? - private let botStart: ChatControllerInitialBotStart? + private var botStart: ChatControllerInitialBotStart? private var attachBotStart: ChatControllerInitialAttachBotStart? 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, { $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 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 return self?.navigationController as? NavigationController }, presentGlobalOverlayController: { [weak self] controller, arguments in @@ -17670,7 +17679,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } - if let controller = controller as? TooltipScreen { + if let controller = controller as? TooltipScreen, !controller.alwaysVisible { controller.dismiss() } return true diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 54aff54c94..0ef6fe43cb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -535,12 +535,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var emptyType: ChatHistoryNodeLoadState.EmptyType? if case let .empty(type) = loadState { - emptyType = type - if case .joined = type { - if strongSelf.didDisplayEmptyGreeting { - emptyType = .generic - } else { - strongSelf.didDisplayEmptyGreeting = true + if case .botInfo = type { + } else { + emptyType = type + if case .joined = type { + if strongSelf.didDisplayEmptyGreeting { + emptyType = .generic + } else { + strongSelf.didDisplayEmptyGreeting = true + } } } } else if case .messages = loadState { @@ -1966,13 +1969,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } if inputPanelNodeHandlesTransition { - inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: .immediate) inputPanelNode.frame = apparentInputPanelFrame inputPanelNode.alpha = 1.0 + inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: .immediate) } else { - inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: transition) transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame) transition.updateAlpha(node: inputPanelNode, alpha: 1.0) + inputPanelNode.updateAbsoluteRect(apparentInputPanelFrame, within: layout.size, transition: transition) } if let viewForOverlayContent = inputPanelNode.viewForOverlayContent { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 1efa37c56b..aedf32089e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2740,14 +2740,19 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { loadState = .empty(.generic) } } else { - loadState = .messages + if transition.historyView.filteredEntries.count == 1, let entry = transition.historyView.filteredEntries.first, case .ChatInfoEntry = entry { + loadState = .empty(.botInfo) + } else { + loadState = .messages + } } if self.loadState != loadState { self.loadState = loadState 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 { self.currentHistoryState = historyState self.historyState.set(historyState) @@ -2925,7 +2930,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if historyView.originalView.isLoadingEarlier && strongSelf.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser { loadState = .loading(true) } else { - loadState = .messages + if historyView.filteredEntries.count == 1, let entry = historyView.filteredEntries.first, case .ChatInfoEntry = entry { + loadState = .empty(.botInfo) + } else { + loadState = .messages + } } } } else { @@ -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._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 { strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) diff --git a/submodules/TelegramUI/Sources/ChatHistoryNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNode.swift index 53e35abf2d..d6029e2296 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNode.swift @@ -12,6 +12,7 @@ public enum ChatHistoryNodeLoadState: Equatable { case joined case clearedHistory case topic + case botInfo } case loading(Bool) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 5c3ace2355..71999c44c0 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -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) { currentPanel.interfaceInteraction = interfaceInteraction 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) { currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return (currentPanel, nil) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index 6a132b0c74..803e1fc426 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -120,6 +120,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift b/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift index ba8dde0a1d..7905048a82 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputAudioRecordingTimeNode.swift @@ -113,7 +113,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { 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() self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size) return size.size @@ -135,7 +135,12 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { if let parameters = parameters as? ChatTextInputAudioRecordingTimeNodeParameters { let currentAudioDurationSeconds = Int(parameters.timestamp) 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) string.draw(at: CGPoint()) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index b7183747f8..99ef6d2eac 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -34,6 +34,8 @@ import UndoUI import PremiumUI import StickerPeekUI import LottieComponent +import SolidRoundedButtonNode +import TooltipUI private let accessoryButtonFont = Font.medium(14.0) 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 menuButtonTextNode: ImmediateTextNode + private let startButton: SolidRoundedButtonNode + let sendAsAvatarButtonNode: HighlightableButtonNode let sendAsAvatarReferenceNode: ContextReferenceContentNode let sendAsAvatarContainerNode: ContextControllerSourceNode @@ -575,6 +579,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return self.actionButtons.micButton } + private let startingBotDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -585,9 +590,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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() +// } +// } + } + } + func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { if let currentState = self.presentationInterfaceState { var updateAccessoryButtons = false @@ -695,6 +719,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private let presentationContext: ChatPresentationContext? + private var tooltipController: TooltipScreen? + init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState self.presentationContext = presentationContext @@ -738,6 +764,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.menuButtonIconNode.customColor = presentationInterfaceState.theme.chat.inputPanel.actionControlForegroundColor 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.sendAsAvatarReferenceNode = ContextReferenceContentNode() 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.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) @@ -920,6 +966,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.clippingNode.addSubnode(self.menuButton) self.clippingNode.addSubnode(self.attachmentButton) self.clippingNode.addSubnode(self.attachmentButtonDisabledNode) + + self.clippingNode.addSubnode(self.startButton) self.clippingNode.addSubnode(self.actionButtons) self.clippingNode.addSubnode(self.counterTextNode) @@ -972,6 +1020,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { deinit { self.statusDisposable.dispose() + self.tooltipController?.dismiss() } func loadTextInputNodeIfNeeded() { @@ -1153,6 +1202,110 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } 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)? override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { @@ -1161,7 +1314,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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) } + + 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 { let previousAdditionalSideInsets = self.validLayout?.4 self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) @@ -1229,6 +1390,127 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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 menuTextSize = self.menuButtonTextNode.frame.size if self.presentationInterfaceState != interfaceState { @@ -1243,6 +1525,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let themeUpdated = previousState?.theme !== interfaceState.theme if themeUpdated { self.menuButtonIconNode.customColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor + self.startButton.updateTheme(SolidRoundedButtonTheme(theme: interfaceState.theme)) } if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty { self.menuButtonIconNode.enqueueState(.close, animated: false) @@ -1526,57 +1809,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } 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 menuButtonHeight: CGFloat = 33.0 let menuCollapsedButtonWidth: CGFloat = isSendAsButton ? menuButtonHeight : 38.0 @@ -1599,17 +1832,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let baseWidth = width - leftInset - leftMenuInset - rightInset let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) - let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) - - let menuButtonFrame = CGRect(x: leftInset + 10.0, y: panelHeight - minimalHeight + floorToScreenPixels((minimalHeight - menuButtonHeight) / 2.0), width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight) + var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + if displayBotStartButton { + panelHeight += 27.0 + } + + 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.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))) - var buttonTitlteTransition = transition + var menuButtonTitleTransition = transition 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.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 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) var composeButtonsOffset: CGFloat = 0.0 @@ -1929,7 +2172,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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) 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) @@ -1991,7 +2234,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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.textInputContainerBackgroundNode, frame: CGRect(origin: CGPoint(), size: textInputFrame.size)) transition.updateAlpha(node: self.textInputContainer, alpha: audioRecordingItemsAlpha) @@ -2025,7 +2268,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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 } else if let contextPlaceholderNode = self.contextPlaceholderNode { self.contextPlaceholderNode = nil @@ -2060,7 +2303,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) button.updateLayout(item: item, size: buttonSize) @@ -2080,7 +2323,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { 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.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)) } } 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 { self.textLockIconNode = nil @@ -2238,7 +2481,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } - + var clippingDelta: CGFloat = 0.0 if case let .media(_, _, focused) = interfaceState.inputMode, focused { clippingDelta = -panelHeight diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 6e48b0f56f..a0390749ad 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -350,6 +350,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index aa279516d7..cf986f9507 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -597,6 +597,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index cb87f87530..341d208b88 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit import TelegramPresentationData import AnimatedStickerNode import TelegramAnimatedStickerNode @@ -29,11 +30,93 @@ public enum TooltipActiveTextAction { 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 let tooltipStyle: TooltipScreen.Style private let icon: TooltipScreen.Icon? 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 shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch private let requestDismiss: () -> Void @@ -50,6 +133,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private let arrowContainer: ASDisplayNode private var arrowEffectView: UIView? private let animatedStickerNode: AnimatedStickerNode + private var downArrowsNode: DownArrowsIconNode? private let textNode: ImmediateTextNode private var isArrowInverted: Bool = false @@ -138,6 +222,24 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.arrowEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) 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() 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 @@ -179,7 +281,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } else if case .top = location { self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) self.containerNode.clipsToBounds = true - self.containerNode.cornerRadius = 9.0 + self.containerNode.cornerRadius = 14.0 if #available(iOS 13.0, *) { self.containerNode.layer.cornerCurve = .continuous } @@ -204,6 +306,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { 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.automaticallyLoadFirstFrame = true + case .downArrows: + self.downArrowsNode = DownArrowsIconNode() } super.init() @@ -227,6 +331,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.animatedStickerNode) + if let downArrowsNode = self.downArrowsNode { + self.containerNode.addSubnode(downArrowsNode) + } self.scrollingContainer.addSubnode(self.containerNode) self.addSubnode(self.scrollingContainer) @@ -298,7 +405,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let sideInset: CGFloat = self.inset + layout.safeInsets.left let bottomInset: CGFloat = 10.0 let contentInset: CGFloat = 11.0 - let contentVerticalInset: CGFloat = 11.0 + let contentVerticalInset: CGFloat = 8.0 let animationSize: CGSize let animationInset: CGFloat let animationSpacing: CGFloat @@ -308,6 +415,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { animationSize = CGSize() animationInset = 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: animationSize = CGSize(width: 32.0, height: 32.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.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)) + + 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? { @@ -472,7 +590,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { animationDelay = 0.6 case .info: animationDelay = 0.2 - case .none: + case .none, .downArrows: animationDelay = 0.0 } @@ -527,6 +645,7 @@ public final class TooltipScreen: ViewController { public enum Icon { case info case chatListPress + case downArrows } public enum DismissOnTouch { @@ -548,6 +667,7 @@ public final class TooltipScreen: ViewController { public enum DisplayDuration { case `default` case custom(Double) + case infinite } public enum Style { @@ -562,7 +682,13 @@ public final class TooltipScreen: ViewController { private let style: TooltipScreen.Style private let icon: TooltipScreen.Icon? 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 inset: CGFloat private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch @@ -580,6 +706,8 @@ public final class TooltipScreen: ViewController { 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) { self.account = account self.text = text @@ -615,6 +743,7 @@ public final class TooltipScreen: ViewController { public func resetDismissTimeout(duration: TooltipScreen.DisplayDuration? = nil) { self.dismissTimer?.invalidate() + self.dismissTimer = nil let timeout: Double switch duration ?? self.displayDuration { @@ -622,6 +751,8 @@ public final class TooltipScreen: ViewController { timeout = 5.0 case let .custom(value): timeout = value + case .infinite: + return } final class TimerTarget: NSObject { @@ -658,7 +789,7 @@ public final class TooltipScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if let validLayout = self.validLayout { + if let validLayout = self.validLayout, !self.alwaysVisible { if validLayout.size.width != layout.size.width { self.dismiss() } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 532ea2fe32..b4dd071d1e 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -117,6 +117,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { if !pathComponents.isEmpty { pathComponents.removeFirst() } + if let lastComponent = pathComponents.last, lastComponent.isEmpty { + pathComponents.removeLast() + } if !pathComponents.isEmpty && !pathComponents[0].isEmpty { let peerName: String = pathComponents[0] if pathComponents.count == 1 {